Skip to content

Commit dce2af2

Browse files
authored
startchat: support creating group (#43)
1 parent ce44b22 commit dce2af2

File tree

6 files changed

+174
-6
lines changed

6 files changed

+174
-6
lines changed

pkg/connector/capabilities.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,21 @@ import (
2828
)
2929

3030
func (*LinkedInConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
31-
return &bridgev2.NetworkGeneralCapabilities{}
31+
return &bridgev2.NetworkGeneralCapabilities{
32+
Provisioning: bridgev2.ProvisioningCapabilities{
33+
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
34+
CreateDM: true,
35+
},
36+
GroupCreation: map[string]bridgev2.GroupTypeCapabilities{
37+
"group": {
38+
TypeDescription: "a group chat",
39+
Name: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MaxLength: 300},
40+
Topic: bridgev2.GroupFieldCapability{Allowed: true},
41+
Participants: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MinLength: 2},
42+
},
43+
},
44+
},
45+
}
3246
}
3347

3448
func (*LinkedInConnector) GetBridgeInfoVersion() (info, capabilities int) {

pkg/connector/chatinfo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (l *LinkedInClient) conversationToChatInfo(conv linkedingo.Conversation) (c
6969

7070
ci.Type = ptr.Ptr(database.RoomTypeDM)
7171
if conv.GroupChat {
72-
ci.Type = ptr.Ptr(database.RoomTypeGroupDM)
72+
ci.Type = ptr.Ptr(database.RoomTypeDefault)
7373
}
7474

7575
ci.CanBackfill = true

pkg/connector/client.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,8 @@ type LinkedInClient struct {
4848

4949
var (
5050
_ bridgev2.NetworkAPI = (*LinkedInClient)(nil)
51-
// _ bridgev2.IdentifierResolvingNetworkAPI = (*LinkedInClient)(nil)
5251
// _ bridgev2.ContactListingNetworkAPI = (*LinkedInClient)(nil)
5352
// _ bridgev2.UserSearchingNetworkAPI = (*LinkedInClient)(nil)
54-
// _ bridgev2.GroupCreatingNetworkAPI = (*LinkedInClient)(nil)
5553
// _ bridgev2.MuteHandlingNetworkAPI = (*LinkedInClient)(nil)
5654
// _ bridgev2.TagHandlingNetworkAPI = (*LinkedInClient)(nil)
5755
)

pkg/connector/startchat.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"fmt"
7+
8+
"go.mau.fi/util/ptr"
9+
"maunium.net/go/mautrix/bridgev2"
10+
"maunium.net/go/mautrix/bridgev2/database"
11+
"maunium.net/go/mautrix/bridgev2/networkid"
12+
"maunium.net/go/mautrix/event"
13+
14+
"go.mau.fi/mautrix-linkedin/pkg/linkedingo"
15+
)
16+
17+
var (
18+
_ bridgev2.GhostDMCreatingNetworkAPI = (*LinkedInClient)(nil)
19+
_ bridgev2.GroupCreatingNetworkAPI = (*LinkedInClient)(nil)
20+
_ bridgev2.IdentifierValidatingNetwork = (*LinkedInConnector)(nil)
21+
)
22+
23+
func (l *LinkedInConnector) ValidateUserID(uid networkid.UserID) bool {
24+
id := string(uid)
25+
_, err := base64.StdEncoding.DecodeString(id)
26+
return err == nil && len(id) >= 36
27+
}
28+
29+
func (l *LinkedInClient) ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
30+
id := networkid.UserID(identifier)
31+
if !l.main.ValidateUserID(id) {
32+
return nil, fmt.Errorf("invalid identifier: %s", identifier)
33+
}
34+
ghost, err := l.main.Bridge.GetGhostByID(ctx, id)
35+
if err != nil {
36+
return nil, err
37+
}
38+
var chat *bridgev2.CreateChatResponse
39+
if createChat {
40+
portal, _ := ghost.Bridge.GetDMPortal(ctx, l.userLogin.ID, id)
41+
if portal != nil {
42+
chat = &bridgev2.CreateChatResponse{
43+
PortalKey: portal.PortalKey,
44+
}
45+
} else {
46+
chatInfo := &bridgev2.ChatInfo{
47+
Type: ptr.Ptr(database.RoomTypeDM),
48+
Members: &bridgev2.ChatMemberList{
49+
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
50+
},
51+
}
52+
participants := []networkid.UserID{
53+
networkid.UserID(identifier),
54+
}
55+
var err error
56+
chat, err = l.createChat(ctx, chatInfo, participants)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to create dm chat: %w", err)
59+
}
60+
}
61+
}
62+
return &bridgev2.ResolveIdentifierResponse{
63+
UserID: id,
64+
Ghost: ghost,
65+
Chat: chat,
66+
}, nil
67+
}
68+
69+
func (l *LinkedInClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) {
70+
resp, err := l.ResolveIdentifier(ctx, string(ghost.ID), true)
71+
if err != nil {
72+
return nil, err
73+
} else if resp == nil {
74+
return nil, nil
75+
}
76+
return resp.Chat, nil
77+
}
78+
79+
func (l *LinkedInClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
80+
chatInfo := &bridgev2.ChatInfo{
81+
Type: ptr.Ptr(database.RoomTypeDefault),
82+
Name: ptr.Ptr(params.Name.Name),
83+
Members: &bridgev2.ChatMemberList{
84+
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
85+
},
86+
}
87+
chat, err := l.createChat(ctx, chatInfo, params.Participants)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to create group chat: %w", err)
90+
}
91+
return chat, nil
92+
}
93+
94+
func (l *LinkedInClient) createChat(ctx context.Context, chatInfo *bridgev2.ChatInfo, _participants []networkid.UserID) (*bridgev2.CreateChatResponse, error) {
95+
participants := make([]linkedingo.URN, len(_participants))
96+
for i, participant := range _participants {
97+
participants[i] = linkedingo.NewURN(participant).AsFsdProfile()
98+
sender := l.makeSender(linkedingo.MessagingParticipant{
99+
EntityURN: participants[i],
100+
})
101+
chatInfo.Members.MemberMap[sender.Sender] = bridgev2.ChatMember{
102+
EventSender: sender,
103+
Membership: event.MembershipJoin,
104+
}
105+
}
106+
resp, err := l.client.NewChat(ctx, ptr.Val(chatInfo.Name), participants)
107+
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
portalKey := networkid.PortalKey{
113+
ID: networkid.PortalID(resp.Data.ConversationURN.String()),
114+
Receiver: l.userLogin.ID,
115+
}
116+
return &bridgev2.CreateChatResponse{
117+
PortalKey: portalKey,
118+
PortalInfo: chatInfo,
119+
}, nil
120+
}

pkg/linkedingo/messages.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"strconv"
2424
"time"
2525

26+
"github.com/google/uuid"
2627
"github.com/rs/zerolog"
2728
"go.mau.fi/util/jsontime"
2829
"go.mau.fi/util/random"
@@ -41,7 +42,7 @@ type sendMessagePayload struct {
4142
type SendMessage struct {
4243
Body SendMessageBody `json:"body,omitempty"`
4344
RenderContentUnions []SendRenderContent `json:"renderContentUnions,omitempty"`
44-
ConversationURN URN `json:"conversationUrn,omitempty"`
45+
ConversationURN *URN `json:"conversationUrn,omitempty"`
4546
OriginToken string `json:"originToken,omitempty"`
4647
}
4748

@@ -134,6 +135,7 @@ type Message struct {
134135
MessageBodyRenderFormat MessageBodyRenderFormat `json:"messageBodyRenderFormat,omitempty"`
135136
BackendConversationURN URN `json:"backendConversationUrn,omitempty"`
136137
Conversation Conversation `json:"conversation,omitempty"`
138+
ConversationURN URN `json:"conversationUrn,omitempty"`
137139
RenderContent []RenderContent `json:"renderContent,omitempty"`
138140
ReactionSummaries []ReactionSummary `json:"reactionSummaries,omitempty"`
139141
}
@@ -184,7 +186,7 @@ func (c *Client) SendMessage(ctx context.Context, conversationURN URN, body Send
184186
Message: SendMessage{
185187
Body: body,
186188
RenderContentUnions: renderContent,
187-
ConversationURN: conversationURN,
189+
ConversationURN: &conversationURN,
188190
OriginToken: transactionID,
189191
},
190192
MailboxURN: c.userEntityURN.WithPrefix("urn", "li", "fsd_profile"),
@@ -206,6 +208,36 @@ func (c *Client) SendMessage(ctx context.Context, conversationURN URN, body Send
206208
return &messageSentResponse, nil
207209
}
208210

211+
func (c *Client) NewChat(ctx context.Context, title string, participants []URN) (*MessageSentResponse, error) {
212+
transactionID := uuid.NewString()
213+
payload := sendMessagePayload{
214+
Message: SendMessage{
215+
Body: SendMessageBody{
216+
Text: "New chat",
217+
},
218+
OriginToken: transactionID,
219+
},
220+
MailboxURN: c.userEntityURN.AsFsdProfile(),
221+
TrackingID: random.String(16),
222+
HostRecipientURNs: participants,
223+
ConversationTitle: title,
224+
}
225+
226+
var messageSentResponse MessageSentResponse
227+
_, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
228+
WithJSONPayload(payload).
229+
WithQueryParam("action", "createMessage").
230+
WithCSRF().
231+
WithContentType(contentTypePlaintextUTF8).
232+
WithXLIHeaders().
233+
Do(ctx, &messageSentResponse)
234+
if err != nil {
235+
return nil, err
236+
}
237+
238+
return &messageSentResponse, nil
239+
}
240+
209241
type GraphQLPatchBody struct {
210242
Patch Patch `json:"patch,omitempty"`
211243
}

pkg/linkedingo/urn.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ func (u URN) WithPrefix(prefix ...string) (n URN) {
9696
n.id = u.id
9797
return
9898
}
99+
100+
func (u URN) AsFsdProfile() URN {
101+
return u.WithPrefix("urn", "li", "fsd_profile")
102+
}

0 commit comments

Comments
 (0)