From d91dae2fc12026a447dac7c773bb4795d6616d6b Mon Sep 17 00:00:00 2001 From: Sumner Evans <sumner.evans@automattic.com> Date: Wed, 12 Feb 2025 11:33:10 -0700 Subject: [PATCH] connector: implement sending edits Signed-off-by: Sumner Evans <sumner.evans@automattic.com> --- ROADMAP.md | 6 +++--- pkg/connector/capabilities.go | 4 ++-- pkg/connector/client.go | 8 ++++++-- pkg/linkedingo/constants.go | 1 + pkg/linkedingo/messages.go | 33 +++++++++++++++++++++++++++++++++ pkg/linkedingo/types/urn.go | 5 +++++ 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b881abf..580ea7b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ * Matrix → LinkedIn * [ ] Message content - * [ ] Text + * [x] Text * [ ] Media * [ ] Files * [ ] Images @@ -12,9 +12,9 @@ * [ ] ~~Stickers~~ (unsupported) * [ ] ~~Formatting~~ (LinkedIn does not support rich formatting) * [ ] Replies - * [ ] Mentions + * [x] Mentions * [ ] Emotes - * [ ] Message edits + * [x] Message edits * [ ] Message redactions * [ ] Message reactions * [ ] Presence diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index e6bc2e1..73e215e 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -48,7 +48,7 @@ var formattingCaps = event.FormattingFeatureMap{ event.FmtSyntaxHighlighting: event.CapLevelDropped, event.FmtBlockquote: event.CapLevelDropped, event.FmtInlineLink: event.CapLevelDropped, - event.FmtUserLink: event.CapLevelDropped, + event.FmtUserLink: event.CapLevelFullySupported, event.FmtUnorderedList: event.CapLevelDropped, event.FmtOrderedList: event.CapLevelDropped, event.FmtListStart: event.CapLevelDropped, @@ -145,7 +145,7 @@ func (*LinkedInClient) GetCapabilities(ctx context.Context, portal *bridgev2.Por MaxTextLength: MaxTextLength, LocationMessage: event.CapLevelDropped, Reply: event.CapLevelDropped, - Edit: event.CapLevelDropped, + Edit: event.CapLevelFullySupported, // TODO note that edits are restricted to specific msgtypes Delete: event.CapLevelDropped, Reaction: event.CapLevelDropped, ReactionCount: 1, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index e911ca6..acb699a 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -48,8 +48,8 @@ type LinkedInClient struct { } var ( - _ bridgev2.NetworkAPI = (*LinkedInClient)(nil) - // _ bridgev2.EditHandlingNetworkAPI = (*LinkedInClient)(nil) + _ bridgev2.NetworkAPI = (*LinkedInClient)(nil) + _ bridgev2.EditHandlingNetworkAPI = (*LinkedInClient)(nil) // _ bridgev2.ReactionHandlingNetworkAPI = (*LinkedInClient)(nil) // _ bridgev2.RedactionHandlingNetworkAPI = (*LinkedInClient)(nil) // _ bridgev2.ReadReceiptHandlingNetworkAPI = (*LinkedInClient)(nil) @@ -432,6 +432,10 @@ func (l *LinkedInClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2. }, nil } +func (l *LinkedInClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { + return l.client.EditMessage(ctx, types.NewURN(string(msg.EditTarget.ID)), matrixfmt.Parse(ctx, l.matrixParser, msg.Content)) +} + func (l *LinkedInClient) IsLoggedIn() bool { return l.userLogin.Metadata.(*UserLoginMetadata).Cookies.GetCookie(linkedingo.LinkedInCookieJSESSIONID) != "" } diff --git a/pkg/linkedingo/constants.go b/pkg/linkedingo/constants.go index 2666157..19bde7d 100644 --- a/pkg/linkedingo/constants.go +++ b/pkg/linkedingo/constants.go @@ -28,6 +28,7 @@ const ( const LinkedInCookieJSESSIONID = "JSESSIONID" const ( + contentTypeJSON = "application/json" contentTypeJSONLinkedInNormalized = "application/vnd.linkedin.normalized+json+2.1" contentTypeGraphQL = "application/graphql" contentTypeTextEventStream = "text/event-stream" diff --git a/pkg/linkedingo/messages.go b/pkg/linkedingo/messages.go index 683da2e..2d364ea 100644 --- a/pkg/linkedingo/messages.go +++ b/pkg/linkedingo/messages.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "github.com/google/uuid" "go.mau.fi/util/random" @@ -70,3 +71,35 @@ func (c *Client) SendMessage(ctx context.Context, payload SendMessagePayload) (* var messageSentResponse MessageSentResponse return &messageSentResponse, json.NewDecoder(resp.Body).Decode(&messageSentResponse) } + +type GraphQLPatchBody struct { + Patch Patch `json:"patch,omitempty"` +} + +// TODO: genericise? +type Patch struct { + Set any `json:"$set,omitempty"` +} + +type EditMessagePayload struct { + Body SendMessageBody `json:"body,omitempty"` +} + +func (c *Client) EditMessage(ctx context.Context, messageURN types.URN, p SendMessageBody) error { + url, err := url.JoinPath(linkedInVoyagerMessagingDashMessengerMessagesURL, messageURN.URLEscaped()) + if err != nil { + return err + } + resp, err := c.newAuthedRequest(http.MethodPost, url). + WithCSRF(). + WithJSONPayload(GraphQLPatchBody{Patch: Patch{Set: EditMessagePayload{Body: p}}}). + WithHeader("accept", contentTypeJSON). + WithRealtimeHeaders(). + Do(ctx) + if err != nil { + return err + } else if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to edit message with urn %s (statusCode=%d)", messageURN, resp.StatusCode) + } + return nil +} diff --git a/pkg/linkedingo/types/urn.go b/pkg/linkedingo/types/urn.go index b58fda4..4a42a6f 100644 --- a/pkg/linkedingo/types/urn.go +++ b/pkg/linkedingo/types/urn.go @@ -3,6 +3,7 @@ package types import ( "encoding/json" "fmt" + "net/url" "strings" ) @@ -32,6 +33,10 @@ func (u URN) String() string { return strings.Join(u.parts, ":") } +func (u URN) URLEscaped() string { + return url.PathEscape(u.String()) +} + func (u URN) IsEmpty() bool { return len(u.parts) == 0 } -- GitLab