Skip to content
Snippets Groups Projects
Unverified Commit 454c8000 authored by Sumner Evans's avatar Sumner Evans
Browse files

connector: implement handling read receipts

parent 1a860980
No related branches found
No related tags found
No related merge requests found
Pipeline #16505 passed
......@@ -48,11 +48,11 @@ type LinkedInClient struct {
}
var (
_ bridgev2.NetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.EditHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.ReactionHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.ReadReceiptHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.NetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.EditHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.ReactionHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.RedactionHandlingNetworkAPI = (*LinkedInClient)(nil)
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.TypingHandlingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.BackfillingNetworkAPI = (*LinkedInClient)(nil)
// _ bridgev2.BackfillingNetworkAPIWithLimits = (*LinkedInClient)(nil)
......
......@@ -116,3 +116,8 @@ func (l *LinkedInClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2
func (l *LinkedInClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
return l.client.RemoveReaction(ctx, types.NewURN(msg.TargetReaction.MessageID), msg.TargetReaction.Emoji)
}
func (l *LinkedInClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridgev2.MatrixReadReceipt) error {
_, err := l.client.MarkConversationRead(ctx, types.NewURN(msg.Portal.ID))
return err
}
......@@ -17,13 +17,14 @@
package linkedingo
const (
linkedInLogoutURL = "https://www.linkedin.com/uas/logout"
linkedInMessagingBaseURL = "https://www.linkedin.com/messaging"
linkedInVoyagerCommonMeURL = "https://www.linkedin.com/voyager/api/me"
linkedInMessagingDashMessengerConversationsURL = "https://www.linkedin.com/voyager/api/voyagerMessagingDashMessengerConversations"
linkedInRealtimeConnectURL = "https://www.linkedin.com/realtime/connect?rc=1"
linkedInRealtimeHeartbeatURL = "https://www.linkedin.com/realtime/realtimeFrontendClientConnectivityTracking?action=sendHeartbeat"
linkedInLogoutURL = "https://www.linkedin.com/uas/logout"
linkedInVoyagerMessagingDashMessengerMessagesURL = "https://www.linkedin.com/voyager/api/voyagerMessagingDashMessengerMessages"
linkedInVoyagerCommonMeURL = "https://www.linkedin.com/voyager/api/me"
linkedInVoyagerMediaUploadMetadataURL = "https://www.linkedin.com/voyager/api/voyagerVideoDashMediaUploadMetadata"
linkedInVoyagerMessagingDashMessengerMessagesURL = "https://www.linkedin.com/voyager/api/voyagerMessagingDashMessengerMessages"
)
const LinkedInCookieJSESSIONID = "JSESSIONID"
......
......@@ -90,7 +90,7 @@ type MediaUploadMetadata struct {
func (c *Client) UploadMedia(ctx context.Context, mediaUploadType MediaUploadType, filename, contentType string, size int, r io.Reader) (types.URN, error) {
resp, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMediaUploadMetadataURL).
WithParam("action", "upload").
WithQueryParam("action", "upload").
WithCSRF().
WithXLIHeaders().
WithHeader("accept", contentTypeJSONLinkedInNormalized).
......
......@@ -81,7 +81,7 @@ func (c *Client) SendMessage(ctx context.Context, conversationURN types.URN, bod
resp, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
WithJSONPayload(payload).
WithParam("action", "createMessage").
WithQueryParam("action", "createMessage").
WithCSRF().
WithContentType(contentTypePlaintextUTF8).
WithXLIHeaders().
......@@ -132,7 +132,7 @@ func (c *Client) EditMessage(ctx context.Context, messageURN types.URN, p SendMe
func (c *Client) RecallMessage(ctx context.Context, messageURN types.URN) error {
resp, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
WithParam("action", "recall").
WithQueryParam("action", "recall").
WithCSRF().
WithXLIHeaders().
WithJSONPayload(map[string]any{"messageUrn": messageURN}).
......
......@@ -18,7 +18,7 @@ func (c *Client) RemoveReaction(ctx context.Context, messageURN types.URN, emoji
func (c *Client) doReactAction(ctx context.Context, messageURN types.URN, emoji, action string) error {
resp, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
WithParam("action", action).
WithQueryParam("action", action).
WithContentType(contentTypePlaintextUTF8).
WithCSRF().
WithHeader("accept", contentTypeJSON).
......
package linkedingo
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"net/url"
"slices"
"strings"
"go.mau.fi/mautrix-linkedin/pkg/linkedingo/types"
"go.mau.fi/mautrix-linkedin/pkg/linkedingoold/routingold/responseold"
)
type MarkMessageReadBody struct {
Read bool `json:"read"`
}
type PatchEntitiesPayload struct {
Entities map[types.URNString]GraphQLPatchBody `json:"entities,omitempty"`
}
func (c *Client) MarkConversationRead(ctx context.Context, convURNs ...types.URN) (*responseold.MarkThreadReadResponse, error) {
return c.doMarkConversationRead(ctx, true, convURNs...)
}
func (c *Client) MarkConversationUnread(ctx context.Context, convURNs ...types.URN) (*responseold.MarkThreadReadResponse, error) {
return c.doMarkConversationRead(ctx, false, convURNs...)
}
func (c *Client) doMarkConversationRead(ctx context.Context, read bool, convURNs ...types.URN) (*responseold.MarkThreadReadResponse, error) {
conversationList := make([]string, len(convURNs))
entities := map[types.URNString]GraphQLPatchBody{}
for i, convURN := range convURNs {
conversationList[i] = url.QueryEscape(convURN.String())
entities[convURN.URNString()] = GraphQLPatchBody{Patch: Patch{Set: MarkMessageReadBody{Read: read}}}
}
resp, err := c.newAuthedRequest(http.MethodPost, linkedInMessagingDashMessengerConversationsURL).
WithRawQuery(fmt.Sprintf("ids=List(%s)", strings.Join(conversationList, ","))). // Using raw query here because escaping the outer ()s makes this break
WithJSONPayload(PatchEntitiesPayload{Entities: entities}).
WithContentType(contentTypePlaintextUTF8).
WithHeader("accept", contentTypeJSON).
WithHeader("origin", "https://www.linkedin.com").
WithCSRF().
WithXLIHeaders().
Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to mark conversation read: %w", err)
} else if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to mark conversation read (statusCode=%d)", resp.StatusCode)
}
var result responseold.MarkThreadReadResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
} else {
return nil, errors.Join(slices.Collect(maps.Values(result.Errors))...)
}
}
......@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"go.mau.fi/util/exerrors"
......@@ -19,11 +20,12 @@ func (c *Client) getCSRFToken() string {
type authedRequest struct {
parseErr error
method string
url *url.URL
header http.Header
params url.Values
body io.Reader
method string
url *url.URL
header http.Header
queryParams url.Values
rawQuery string
body io.Reader
client *Client
}
......@@ -33,9 +35,9 @@ func (c *Client) newAuthedRequest(method, urlStr string) *authedRequest {
ar.url, ar.parseErr = url.Parse(urlStr)
if ar.parseErr != nil {
ar.params = ar.url.Query()
ar.queryParams = ar.url.Query()
} else {
ar.params = url.Values{}
ar.queryParams = url.Values{}
}
// Add default headers for every request
......@@ -54,8 +56,15 @@ func (a *authedRequest) WithHeader(key, value string) *authedRequest {
return a
}
func (a *authedRequest) WithParam(key, value string) *authedRequest {
a.params.Add(key, value)
// WithQueryParam adds a query parameter to the request. If a raw query is set
// with [authedRequest.WithRawQuery], this will be ignored.
func (a *authedRequest) WithQueryParam(key, value string) *authedRequest {
a.queryParams.Add(key, value)
return a
}
func (a *authedRequest) WithRawQuery(raw string) *authedRequest {
a.rawQuery = raw
return a
}
......@@ -65,6 +74,7 @@ func (a *authedRequest) WithCSRF() *authedRequest {
func (a *authedRequest) WithJSONPayload(payload any) *authedRequest {
a.body = bytes.NewReader(exerrors.Must(json.Marshal(payload)))
fmt.Printf("%s\n", exerrors.Must(json.Marshal(payload)))
return a
}
......@@ -116,12 +126,19 @@ func (a *authedRequest) Do(ctx context.Context) (*http.Response, error) {
if a.parseErr != nil {
return nil, a.parseErr
}
a.url.RawQuery = a.params.Encode()
if a.rawQuery != "" {
a.url.RawQuery = a.rawQuery
} else {
a.url.RawQuery = a.queryParams.Encode()
}
req, err := http.NewRequestWithContext(ctx, a.method, a.url.String(), a.body)
if err != nil {
return nil, fmt.Errorf("failed to perform authed request %s %s: %w", a.method, a.url, err)
}
req.Header = a.header
fmt.Printf("%s\n", exerrors.Must(httputil.DumpRequestOut(req, true)))
return a.client.http.Do(req)
}
......@@ -59,7 +59,7 @@ func (c *Client) GetCurrentUserProfile(ctx context.Context) (*UserProfile, error
func (c *Client) Logout(ctx context.Context) error {
_, err := c.newAuthedRequest(http.MethodGet, linkedInLogoutURL).
WithParam("csrfToken", c.getCSRFToken()).
WithQueryParam("csrfToken", c.getCSRFToken()).
Do(ctx)
return err
}
......@@ -7,6 +7,12 @@ import (
"strings"
)
type URNString string
func (u URNString) URN() URN {
return NewURN(string(u))
}
type URN struct {
parts []string
idParts []string
......@@ -29,6 +35,10 @@ func (u URN) ID() string {
return u.idParts[0]
}
func (u URN) URNString() URNString {
return URNString(u.String())
}
func (u URN) String() string {
return strings.Join(u.parts, ":")
}
......
......@@ -3,6 +3,7 @@ package responseold
import (
"encoding/json"
"go.mau.fi/mautrix-linkedin/pkg/linkedingo/types"
"go.mau.fi/mautrix-linkedin/pkg/linkedingoold/routingold/payloadold"
"go.mau.fi/mautrix-linkedin/pkg/linkedingoold/routingold/queryold"
"go.mau.fi/mautrix-linkedin/pkg/linkedingoold/typesold"
......@@ -209,8 +210,8 @@ type MessageSentData struct {
}
type MarkThreadReadResponse struct {
Results map[string]MarkThreadReadResult `json:"results,omitempty"`
Errors any `json:"errors,omitempty"`
Results map[types.URNString]MarkThreadReadResult `json:"results,omitempty"`
Errors map[types.URNString]error `json:"errors,omitempty"`
}
type MarkThreadReadResult struct {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment