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

connector: support sending images

parent fbb076f7
No related branches found
No related tags found
No related merge requests found
Pipeline #16465 passed
......@@ -356,103 +356,6 @@ func (l *LinkedInClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost)
return nil, nil
}
func (l *LinkedInClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
conversationURN := types.NewURN(msg.Portal.ID)
if msg.Content.MsgType == event.MsgEmote {
if msg.Content.FormattedBody == "" {
msg.Content.FormattedBody = msg.Content.Body
}
msg.Content.Format = event.FormatHTML
msg.Content.Body = fmt.Sprintf("* %s %s", l.userLogin.RemoteName, msg.Content.Body)
msg.Content.FormattedBody = fmt.Sprintf(`* <a href="https://matrix.to/#/%s">%s</a> %s`, l.userLogin.UserMXID, l.userLogin.RemoteName, msg.Content.FormattedBody)
msg.Content.Mentions = &event.Mentions{UserIDs: []id.UserID{l.userLogin.UserMXID}}
}
sendMessagePayload := linkedingo.SendMessagePayload{
Message: linkedingo.SendMessage{
Body: matrixfmt.Parse(ctx, l.matrixParser, msg.Content),
ConversationURN: conversationURN,
},
}
// if msg.ReplyTo != nil {
// sendMessagePayload.Message.RenderContentUnions = append(
// sendMessagePayload.Message.RenderContentUnions,
// payloadold.RenderContent{
// RepliedMessageContent: &payloadold.RepliedMessageContent{
// OriginalSenderUrn: string(msg.ReplyTo.SenderID),
// OriginalMessageUrn: string(msg.ReplyTo.ID),
// OriginalSendAt: msg.ReplyTo.Timestamp.UnixMilli(),
// //MessageBody: "", // todo add at some point
// },
// },
// )
// }
// content := msg.Content
//
// switch content.MsgType {
// case event.MsgText:
// break
// case event.MsgVideo, event.MsgImage:
// if content.Body == content.FileName {
// sendMessagePayload.Message.Body.Text = ""
// }
//
// file := content.GetFile()
// data, err := lc.connector.br.Bot.DownloadMedia(ctx, file.URL, file)
// if err != nil {
// return nil, err
// }
//
// attachmentType := payloadold.MediaUploadFileAttachment
// if content.MsgType == event.MsgImage {
// attachmentType = payloadold.MediaUploadTypePhotoAttachment
// }
//
// mediaMetadata, err := lc.client.UploadMedia(attachmentType, content.FileName, data, typesold.ContentTypeJSONPlaintextUTF8)
// if err != nil {
// return nil, err
// }
//
// lc.client.Logger.Debug().Any("media_metadata", mediaMetadata).Msg("Successfully uploaded media to LinkedIn's servers")
// sendMessagePayload.Message.RenderContentUnions = append(sendMessagePayload.Message.RenderContentUnions, payloadold.RenderContent{
// File: &payloadold.File{
// AssetUrn: mediaMetadata.Urn,
// Name: content.FileName,
// MediaType: typesold.ContentType(content.Info.MimeType),
// ByteSize: len(data),
// },
// })
// default:
// return nil, fmt.Errorf("%w %s", bridgev2.ErrUnsupportedMessageType, content.MsgType)
// }
//
resp, err := l.client.SendMessage(ctx, sendMessagePayload)
if err != nil {
return nil, err
}
return &bridgev2.MatrixMessageResponse{
DB: &database.Message{
ID: resp.Data.MessageID(),
MXID: msg.Event.ID,
Room: msg.Portal.PortalKey,
SenderID: l.userID,
Timestamp: resp.Data.DeliveredAt.Time,
},
}, nil
}
func (l *LinkedInClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
return l.client.EditMessage(ctx, types.NewURN(msg.EditTarget.ID), matrixfmt.Parse(ctx, l.matrixParser, msg.Content))
}
func (l *LinkedInClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
return l.client.RecallMessage(ctx, types.NewURN(msg.TargetMessage.ID))
}
func (l *LinkedInClient) IsLoggedIn() bool {
return l.userLogin.Metadata.(*UserLoginMetadata).Cookies.GetCookie(linkedingo.LinkedInCookieJSESSIONID) != ""
}
......
package connector
import (
"context"
"fmt"
"os"
"strings"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-linkedin/pkg/connector/matrixfmt"
"go.mau.fi/mautrix-linkedin/pkg/linkedingo"
"go.mau.fi/mautrix-linkedin/pkg/linkedingo/types"
)
func getMediaFilename(content *event.MessageEventContent) (filename string) {
if content.FileName != "" {
filename = content.FileName
} else {
filename = content.Body
}
if filename == "" {
return "image.jpg" // Assume it's a JPEG image
}
if content.MsgType == event.MsgImage && (!strings.HasSuffix(filename, ".jpg") && !strings.HasSuffix(filename, ".jpeg") && !strings.HasSuffix(filename, ".png")) {
if content.Info != nil && content.Info.MimeType != "" {
return filename + strings.TrimPrefix(content.Info.MimeType, "image/")
}
return filename + ".jpg" // Assume it's a JPEG
}
return filename
}
func (l *LinkedInClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
conversationURN := types.NewURN(msg.Portal.ID)
// Handle emotes by adding a "*" and the user's name to the message
if msg.Content.MsgType == event.MsgEmote {
if msg.Content.FormattedBody == "" {
msg.Content.FormattedBody = msg.Content.Body
}
msg.Content.Format = event.FormatHTML
msg.Content.Body = fmt.Sprintf("* %s %s", l.userLogin.RemoteName, msg.Content.Body)
msg.Content.FormattedBody = fmt.Sprintf(`* <a href="https://matrix.to/#/%s">%s</a> %s`, l.userLogin.UserMXID, l.userLogin.RemoteName, msg.Content.FormattedBody)
msg.Content.Mentions = &event.Mentions{UserIDs: []id.UserID{l.userLogin.UserMXID}}
}
var renderContent []linkedingo.SendRenderContent
switch msg.Content.MsgType {
case event.MsgImage:
err := l.main.Bridge.Bot.DownloadMediaToFile(ctx, msg.Content.URL, msg.Content.File, false, func(f *os.File) error {
attachmentType := linkedingo.MediaUploadTypePhotoAttachment
filename := getMediaFilename(msg.Content)
urn, err := l.client.UploadMedia(ctx, attachmentType, filename, msg.Content.Info.MimeType, msg.Content.Info.Size, f)
if err != nil {
return err
}
renderContent = append(renderContent, linkedingo.SendRenderContent{
File: &linkedingo.SendFile{
AssetURN: urn,
Name: filename,
MediaType: msg.Content.Info.MimeType,
ByteSize: msg.Content.Info.Size,
},
})
return nil
})
if err != nil {
return nil, err
}
}
// content := msg.Content
//
// switch content.MsgType {
// case event.MsgText:
// break
// case event.MsgVideo, event.MsgImage:
// if content.Body == content.FileName {
// sendMessagePayload.Message.Body.Text = ""
// }
//
// file := content.GetFile()
// data, err := lc.connector.br.Bot.DownloadMedia(ctx, file.URL, file)
// if err != nil {
// return nil, err
// }
//
// attachmentType := payloadold.MediaUploadFileAttachment
// if content.MsgType == event.MsgImage {
// attachmentType = payloadold.MediaUploadTypePhotoAttachment
// }
//
// mediaMetadata, err := lc.client.UploadMedia(attachmentType, content.FileName, data, typesold.ContentTypeJSONPlaintextUTF8)
// if err != nil {
// return nil, err
// }
//
// lc.client.Logger.Debug().Any("media_metadata", mediaMetadata).Msg("Successfully uploaded media to LinkedIn's servers")
// sendMessagePayload.Message.RenderContentUnions = append(sendMessagePayload.Message.RenderContentUnions, payloadold.RenderContent{
// File: &payloadold.File{
// AssetUrn: mediaMetadata.Urn,
// Name: content.FileName,
// MediaType: typesold.ContentType(content.Info.MimeType),
// ByteSize: len(data),
// },
// })
// default:
// return nil, fmt.Errorf("%w %s", bridgev2.ErrUnsupportedMessageType, content.MsgType)
// }
resp, err := l.client.SendMessage(ctx, conversationURN, matrixfmt.Parse(ctx, l.matrixParser, msg.Content), renderContent)
if err != nil {
return nil, err
}
return &bridgev2.MatrixMessageResponse{
DB: &database.Message{
ID: resp.Data.MessageID(),
MXID: msg.Event.ID,
Room: msg.Portal.PortalKey,
SenderID: l.userID,
Timestamp: resp.Data.DeliveredAt.Time,
},
}, nil
}
func (l *LinkedInClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
return l.client.EditMessage(ctx, types.NewURN(msg.EditTarget.ID), matrixfmt.Parse(ctx, l.matrixParser, msg.Content))
}
func (l *LinkedInClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
return l.client.RecallMessage(ctx, types.NewURN(msg.TargetMessage.ID))
}
......@@ -23,12 +23,14 @@ const (
linkedInRealtimeHeartbeatURL = "https://www.linkedin.com/realtime/realtimeFrontendClientConnectivityTracking?action=sendHeartbeat"
linkedInLogoutURL = "https://www.linkedin.com/uas/logout"
linkedInVoyagerMessagingDashMessengerMessagesURL = "https://www.linkedin.com/voyager/api/voyagerMessagingDashMessengerMessages"
linkedInVoyagerMediaUploadMetadataURL = "https://www.linkedin.com/voyager/api/voyagerVideoDashMediaUploadMetadata"
)
const LinkedInCookieJSESSIONID = "JSESSIONID"
const (
contentTypeJSON = "application/json"
contentTypeJSONPlaintextUTF8 = "application/json; charset=UTF-8"
contentTypeJSONLinkedInNormalized = "application/vnd.linkedin.normalized+json+2.1"
contentTypeGraphQL = "application/graphql"
contentTypeTextEventStream = "text/event-stream"
......
......@@ -3,9 +3,12 @@ package linkedingo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"strconv"
"maunium.net/go/mautrix/event"
......@@ -54,3 +57,71 @@ func (c *Client) GetAudioFileInfo(ctx context.Context, audio *types.AudioMetadat
}
return
}
type MediaUploadType string
const (
MediaUploadTypePhotoAttachment MediaUploadType = "MESSAGING_PHOTO_ATTACHMENT"
MediaUploadTypeFileAttachment MediaUploadType = "MESSAGING_FILE_ATTACHMENT"
)
type UploadMediaMetadataPayload struct {
MediaUploadType MediaUploadType `json:"mediaUploadType,omitempty"`
FileSize int `json:"fileSize,omitempty"`
Filename string `json:"filename,omitempty"`
}
type UploadMediaMetadataResponse struct {
Data ActionResponse `json:"data,omitempty"`
}
// ActionResponse represents a com.linkedin.restli.common.ActionResponse
// object.
type ActionResponse struct {
Value MediaUploadMetadata `json:"value,omitempty"`
}
// MediaUploadMetadata represents a
// com.linkedin.mediauploader.MediaUploadMetadata object.
type MediaUploadMetadata struct {
URN types.URN `json:"urn,omitempty"`
SingleUploadURL string `json:"singleUploadUrl,omitempty"`
}
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").
WithCSRF().
WithRealtimeHeaders().
WithHeader("accept", contentTypeJSONLinkedInNormalized).
WithContentType(contentTypeJSONPlaintextUTF8).
WithJSONPayload(UploadMediaMetadataPayload{
MediaUploadType: mediaUploadType,
FileSize: size,
Filename: filename,
}).
Do(ctx)
if err != nil {
return types.URN{}, err
} else if resp.StatusCode != http.StatusOK {
return types.URN{}, fmt.Errorf("failed to get upload media metadata (statusCode=%d)", resp.StatusCode)
}
var uploadMetadata UploadMediaMetadataResponse
if err = json.NewDecoder(resp.Body).Decode(&uploadMetadata); err != nil {
return types.URN{}, err
}
resp, err = c.newAuthedRequest(http.MethodPut, uploadMetadata.Data.Value.SingleUploadURL).
WithCSRF().
WithHeader("content-length", strconv.Itoa(size)).
WithContentType(contentType).
WithBody(r).
Do(ctx)
if err != nil {
return types.URN{}, err
} else if resp.StatusCode != http.StatusCreated {
return types.URN{}, fmt.Errorf("failed to upload media: status=%d", resp.StatusCode)
}
return uploadMetadata.Data.Value.URN, nil
}
......@@ -13,7 +13,7 @@ import (
"go.mau.fi/mautrix-linkedin/pkg/linkedingo/types"
)
type SendMessagePayload struct {
type sendMessagePayload struct {
Message SendMessage `json:"message,omitempty"`
MailboxURN types.URN `json:"mailboxUrn,omitempty"`
TrackingID string `json:"trackingId,omitempty"`
......@@ -23,10 +23,10 @@ type SendMessagePayload struct {
}
type SendMessage struct {
Body SendMessageBody `json:"body,omitempty"`
RenderContentUnions []any `json:"renderContentUnions,omitempty"`
ConversationURN types.URN `json:"conversationUrn,omitempty"`
OriginToken uuid.UUID `json:"originToken,omitempty"`
Body SendMessageBody `json:"body,omitempty"`
RenderContentUnions []SendRenderContent `json:"renderContentUnions,omitempty"`
ConversationURN types.URN `json:"conversationUrn,omitempty"`
OriginToken uuid.UUID `json:"originToken,omitempty"`
}
type SendMessageBody struct {
......@@ -44,14 +44,32 @@ type AttributeType struct {
Entity *types.Entity `json:"com.linkedin.pemberly.text.Entity,omitempty"`
}
type SendRenderContent struct {
File *SendFile `json:"file,omitempty"`
}
type SendFile struct {
AssetURN types.URN `json:"assetUrn,omitempty"`
ByteSize int `json:"byteSize,omitempty"`
MediaType string `json:"mediaType,omitempty"`
Name string `json:"name,omitempty"`
}
type MessageSentResponse struct {
Data types.Message `json:"value,omitempty"`
}
func (c *Client) SendMessage(ctx context.Context, payload SendMessagePayload) (*MessageSentResponse, error) {
payload.MailboxURN = c.userEntityURN.WithPrefix("urn", "li", "fsd_profile")
payload.TrackingID = random.String(16)
payload.Message.OriginToken = uuid.New()
func (c *Client) SendMessage(ctx context.Context, conversationURN types.URN, body SendMessageBody, renderContent []SendRenderContent) (*MessageSentResponse, error) {
payload := sendMessagePayload{
Message: SendMessage{
Body: body,
RenderContentUnions: renderContent,
ConversationURN: conversationURN,
OriginToken: uuid.New(),
},
MailboxURN: c.userEntityURN.WithPrefix("urn", "li", "fsd_profile"),
TrackingID: random.String(16),
}
resp, err := c.newAuthedRequest(http.MethodPost, linkedInVoyagerMessagingDashMessengerMessagesURL).
WithJSONPayload(payload).
......
......@@ -263,8 +263,6 @@ func (c *Client) realtimeConnectLoop(ctx context.Context) {
case realtimeEvent.Heartbeat != nil:
c.handlers.onHeartbeat(ctx)
case realtimeEvent.ClientConnection != nil:
c.realtimeSessionID = realtimeEvent.ClientConnection.ID
log.Debug().Stringer("realtime_session_id", c.realtimeSessionID).Msg("Got new realtime session ID")
c.handlers.onClientConnection(ctx, realtimeEvent.ClientConnection)
case realtimeEvent.DecoratedEvent != nil:
log.Debug().
......
......@@ -17,23 +17,22 @@ func (c *Client) getCSRFToken() string {
}
type authedRequest struct {
parseErr error
method string
url string
body io.Reader
url *url.URL
header http.Header
params url.Values
body io.Reader
client *Client
}
func (c *Client) newAuthedRequest(method, url string) *authedRequest {
return &authedRequest{
method: method,
url: url,
header: http.Header{},
params: map[string][]string{},
client: c,
}
func (c *Client) newAuthedRequest(method, urlStr string) *authedRequest {
ar := authedRequest{header: http.Header{}, method: method, client: c}
ar.url, ar.parseErr = url.Parse(urlStr)
ar.params = ar.url.Query()
return &ar
}
func (a *authedRequest) WithHeader(key, value string) *authedRequest {
......@@ -55,6 +54,11 @@ func (a *authedRequest) WithJSONPayload(payload any) *authedRequest {
return a
}
func (a *authedRequest) WithBody(r io.Reader) *authedRequest {
a.body = r
return a
}
func (a *authedRequest) WithContentType(contentType string) *authedRequest {
return a.WithHeader("content-type", contentType)
}
......@@ -83,12 +87,12 @@ func (a *authedRequest) WithWebpageHeaders() *authedRequest {
}
func (a *authedRequest) Do(ctx context.Context) (*http.Response, error) {
u, err := url.Parse(a.url)
if err != nil {
return nil, err
if a.parseErr != nil {
return nil, a.parseErr
}
u.RawQuery = a.params.Encode()
req, err := http.NewRequestWithContext(ctx, a.method, u.String(), a.body)
a.url.RawQuery = a.params.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)
}
......
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