Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion pkg/connector/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ import (
)

func (*LinkedInConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
return &bridgev2.NetworkGeneralCapabilities{}
return &bridgev2.NetworkGeneralCapabilities{
Provisioning: bridgev2.ProvisioningCapabilities{
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
},
GroupCreation: map[string]bridgev2.GroupTypeCapabilities{
"group": {
TypeDescription: "a group chat",
Name: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MaxLength: 300},
Topic: bridgev2.GroupFieldCapability{Allowed: true},
Participants: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MinLength: 2},
},
},
},
}
}

func (*LinkedInConnector) GetBridgeInfoVersion() (info, capabilities int) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/connector/chatinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (l *LinkedInClient) conversationToChatInfo(conv linkedingo.Conversation) (c

ci.Type = ptr.Ptr(database.RoomTypeDM)
if conv.GroupChat {
ci.Type = ptr.Ptr(database.RoomTypeGroupDM)
ci.Type = ptr.Ptr(database.RoomTypeDefault)
}

ci.CanBackfill = true
Expand Down
2 changes: 0 additions & 2 deletions pkg/connector/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ type LinkedInClient struct {

var (
_ bridgev2.NetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.IdentifierResolvingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.ContactListingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.UserSearchingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.GroupCreatingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.MuteHandlingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.TagHandlingNetworkAPI = (*LinkedInClient)(nil)
)
Expand Down
120 changes: 120 additions & 0 deletions pkg/connector/startchat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package connector

import (
"context"
"encoding/base64"
"fmt"

"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"

"go.mau.fi/mautrix-linkedin/pkg/linkedingo"
)

var (
_ bridgev2.GhostDMCreatingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.GroupCreatingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.IdentifierValidatingNetwork = (*LinkedInConnector)(nil)
)

func (l *LinkedInConnector) ValidateUserID(uid networkid.UserID) bool {
id := string(uid)
_, err := base64.StdEncoding.DecodeString(id)
return err == nil && len(id) >= 36
}

func (l *LinkedInClient) ResolveIdentifier(ctx context.Context, identifier string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
id := networkid.UserID(identifier)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input can be absolutely anything, so this needs to validate that it looks like a linkedin identifier. Implementing ValidateUserID on the network connector and calling that should be sufficient

if !l.main.ValidateUserID(id) {
return nil, fmt.Errorf("invalid identifier: %s", identifier)
}
ghost, err := l.main.Bridge.GetGhostByID(ctx, id)
if err != nil {
return nil, err
}
var chat *bridgev2.CreateChatResponse
if createChat {
portal, _ := ghost.Bridge.GetDMPortal(ctx, l.userLogin.ID, id)
if portal != nil {
chat = &bridgev2.CreateChatResponse{
PortalKey: portal.PortalKey,
}
} else {
chatInfo := &bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDM),
Members: &bridgev2.ChatMemberList{
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
},
}
participants := []networkid.UserID{
networkid.UserID(identifier),
}
var err error
chat, err = l.createChat(ctx, chatInfo, participants)
if err != nil {
return nil, fmt.Errorf("failed to create dm chat: %w", err)
}
}
}
return &bridgev2.ResolveIdentifierResponse{
UserID: id,
Ghost: ghost,
Chat: chat,
}, nil
}

func (l *LinkedInClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) {
resp, err := l.ResolveIdentifier(ctx, string(ghost.ID), true)
if err != nil {
return nil, err
} else if resp == nil {
return nil, nil
}
return resp.Chat, nil
}

func (l *LinkedInClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
chatInfo := &bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDefault),
Name: ptr.Ptr(params.Name.Name),
Members: &bridgev2.ChatMemberList{
MemberMap: map[networkid.UserID]bridgev2.ChatMember{},
},
}
chat, err := l.createChat(ctx, chatInfo, params.Participants)
if err != nil {
return nil, fmt.Errorf("failed to create group chat: %w", err)
}
return chat, nil
}

func (l *LinkedInClient) createChat(ctx context.Context, chatInfo *bridgev2.ChatInfo, _participants []networkid.UserID) (*bridgev2.CreateChatResponse, error) {
participants := make([]linkedingo.URN, len(_participants))
for i, participant := range _participants {
participants[i] = linkedingo.NewURN(participant).AsFsdProfile()
sender := l.makeSender(linkedingo.MessagingParticipant{
EntityURN: participants[i],
})
chatInfo.Members.MemberMap[sender.Sender] = bridgev2.ChatMember{
EventSender: sender,
Membership: event.MembershipJoin,
}
}
resp, err := l.client.NewChat(ctx, ptr.Val(chatInfo.Name), participants)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we get the proper conversation info from this? I don't like the way this ChatInfo is formed completely separately from the code in chatinfo.go, would be preferable to reuse conversationToChatInfo if we get the canonical info from the server

(if we don't get the info, then this is fine for now)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only get conversation urn

res = {
  value: {
    renderContentUnions: [],
    entityUrn: '',
    backendConversationUrn: '',
    senderUrn: '',
    originToken: '',
    body: { attributes: [], text: 'c' },
    backendUrn: '',
    conversationUrn: '',
    deliveredAt: 1764378815883,
  },
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the current code is fine for now


if err != nil {
return nil, err
}

portalKey := networkid.PortalKey{
ID: networkid.PortalID(resp.Data.ConversationURN.String()),
Receiver: l.userLogin.ID,
}
return &bridgev2.CreateChatResponse{
PortalKey: portalKey,
PortalInfo: chatInfo,
}, nil
}
36 changes: 34 additions & 2 deletions pkg/linkedingo/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strconv"
"time"

"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/jsontime"
"go.mau.fi/util/random"
Expand All @@ -41,7 +42,7 @@ type sendMessagePayload struct {
type SendMessage struct {
Body SendMessageBody `json:"body,omitempty"`
RenderContentUnions []SendRenderContent `json:"renderContentUnions,omitempty"`
ConversationURN URN `json:"conversationUrn,omitempty"`
ConversationURN *URN `json:"conversationUrn,omitempty"`
OriginToken string `json:"originToken,omitempty"`
}

Expand Down Expand Up @@ -134,6 +135,7 @@ type Message struct {
MessageBodyRenderFormat MessageBodyRenderFormat `json:"messageBodyRenderFormat,omitempty"`
BackendConversationURN URN `json:"backendConversationUrn,omitempty"`
Conversation Conversation `json:"conversation,omitempty"`
ConversationURN URN `json:"conversationUrn,omitempty"`
RenderContent []RenderContent `json:"renderContent,omitempty"`
ReactionSummaries []ReactionSummary `json:"reactionSummaries,omitempty"`
}
Expand Down Expand Up @@ -184,7 +186,7 @@ func (c *Client) SendMessage(ctx context.Context, conversationURN URN, body Send
Message: SendMessage{
Body: body,
RenderContentUnions: renderContent,
ConversationURN: conversationURN,
ConversationURN: &conversationURN,
OriginToken: transactionID,
},
MailboxURN: c.userEntityURN.WithPrefix("urn", "li", "fsd_profile"),
Expand All @@ -206,6 +208,36 @@ func (c *Client) SendMessage(ctx context.Context, conversationURN URN, body Send
return &messageSentResponse, nil
}

func (c *Client) NewChat(ctx context.Context, title string, participants []URN) (*MessageSentResponse, error) {
transactionID := uuid.NewString()
payload := sendMessagePayload{
Message: SendMessage{
Body: SendMessageBody{
Text: "New chat",
},
OriginToken: transactionID,
},
MailboxURN: c.userEntityURN.AsFsdProfile(),
TrackingID: random.String(16),
HostRecipientURNs: participants,
ConversationTitle: title,
}

var messageSentResponse MessageSentResponse
_, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
WithJSONPayload(payload).
WithQueryParam("action", "createMessage").
WithCSRF().
WithContentType(contentTypePlaintextUTF8).
WithXLIHeaders().
Do(ctx, &messageSentResponse)
if err != nil {
return nil, err
}

return &messageSentResponse, nil
}

type GraphQLPatchBody struct {
Patch Patch `json:"patch,omitempty"`
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/linkedingo/urn.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ func (u URN) WithPrefix(prefix ...string) (n URN) {
n.id = u.id
return
}

func (u URN) AsFsdProfile() URN {
return u.WithPrefix("urn", "li", "fsd_profile")
}