diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 94977c97..1cca87ae 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -55,7 +55,7 @@ func (m *MetaConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities { } func (m *MetaConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 12 + return 1, 13 } const MaxTextLength = 20000 @@ -71,7 +71,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.meta.capabilities.2026_02_24" + base := "fi.mau.meta.capabilities.2026_03_26" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -206,6 +206,9 @@ func init() { for _, value := range igCaps.File { value.Caption = event.CapLevelDropped } + igCaps.MessageRequest = &event.MessageRequestFeatures{ + AcceptWithButton: event.CapLevelFullySupported, + } igCaps.ID += "+instagram" igCapsGroup = igCaps.Clone() igCapsGroup.ID += "+instagram-group" diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index d24ac23e..0f2ad432 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -236,6 +236,7 @@ func (m *MetaClient) wrapChatInfo(tbl table.ThreadInfo) *bridgev2.ChatInfo { if chatInfo.UserLocal == nil { chatInfo.UserLocal = &bridgev2.UserLocalPortalInfo{} } + chatInfo.MessageRequest = ptr.Ptr(tbl.GetFolderName() == folderPending) if tbl.GetFolderName() == folderE2EECutover { chatInfo.ExtraUpdates = bridgev2.MergeExtraUpdaters(chatInfo.ExtraUpdates, markPortalAsEncrypted) } diff --git a/pkg/connector/events.go b/pkg/connector/events.go index b8d6c48a..e96762ce 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -69,7 +69,7 @@ func (evt *VerifyThreadExistsEvent) GetType() bridgev2.RemoteEventType { } func (evt *VerifyThreadExistsEvent) ShouldCreatePortal() bool { - return evt.FolderName != folderPending && evt.FolderName != folderSpam + return evt.FolderName != folderSpam } func (evt *VerifyThreadExistsEvent) GetPortalKey() networkid.PortalKey { @@ -112,7 +112,9 @@ func (evt *VerifyThreadExistsEvent) GetChatInfo(ctx context.Context, portal *bri zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("Requested full thread info") } } - return evt.m.makeMinimalChatInfo(evt.ThreadKey, evt.ThreadType, evt.ParentThreadKey), nil + chatInfo := evt.m.makeMinimalChatInfo(evt.ThreadKey, evt.ThreadType, evt.ParentThreadKey) + chatInfo.MessageRequest = ptr.Ptr(evt.FolderName == folderPending) + return chatInfo, nil } type FBMessageEvent struct { @@ -546,6 +548,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por type FBChatResync struct { Raw *table.LSDeleteThenInsertThread + Update *table.LSUpdateOrInsertThread PortalKey networkid.PortalKey Info *bridgev2.ChatInfo Members map[int64]bridgev2.ChatMember @@ -578,26 +581,50 @@ func (r *FBChatResync) PortalReceiverIsUncertain() bool { } func (r *FBChatResync) ShouldCreatePortal() bool { - if r.Raw == nil { + if r.Raw != nil && r.Raw.FolderName == folderSpam { return false + } else if r.Update != nil && r.Update.FolderName == folderSpam { + return false + } else if r.Raw == nil && r.Update == nil { + // Backfill-only resyncs don't carry folder metadata, so they can't + // reliably decide portal creation here. + return false + } + if r.Info != nil && r.Info.MessageRequest != nil && *r.Info.MessageRequest { + return true + } + if r.Raw != nil { + return r.Raw.FolderName != folderPending } - return r.Raw.FolderName != folderPending && r.Raw.FolderName != folderSpam + return r.Update.FolderName != folderPending } func (r *FBChatResync) AddLogContext(c zerolog.Context) zerolog.Context { if r.UpsertID != 0 { c = c.Int64("global_upsert_counter", r.UpsertID) } - if r.Raw == nil { - return c + var threadID int64 + var threadType table.ThreadType + var threadFolder string + if r.Raw != nil { + threadID = r.Raw.ThreadKey + threadType = r.Raw.ThreadType + threadFolder = r.Raw.FolderName + } else if r.Update != nil { + threadID = r.Update.ThreadKey + threadType = r.Update.ThreadType + threadFolder = r.Update.FolderName + } else { + threadID = metaid.ParseFBPortalID(r.PortalKey.ID) + threadFolder = "unknown" } c = c. - Int64("thread_id", r.Raw.ThreadKey). - Int("thread_type", int(r.Raw.ThreadType)). + Int64("thread_id", threadID). + Int("thread_type", int(threadType)). Dict("debug_info", zerolog.Dict(). - Str("thread_folder", r.Raw.FolderName). - Int64("ig_folder", r.Raw.IgFolder). - Int64("group_notification_settings", r.Raw.GroupNotificationSettings)) + Str("thread_folder", threadFolder). + Int64("ig_folder", r.getIGFolder()). + Int64("group_notification_settings", r.getGroupNotificationSettings())) return c } @@ -606,7 +633,7 @@ func (r *FBChatResync) GetSender() bridgev2.EventSender { } func (r *FBChatResync) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) { - if r.Raw == nil { + if r.Info == nil { return nil, nil } if len(r.Members) > 0 && !r.filled { @@ -639,10 +666,11 @@ func (r *FBChatResync) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) func (r *FBChatResync) CheckNeedsBackfill(ctx context.Context, lastMessage *database.Message) (bool, error) { // Check for forward backfill if we're handling a remote update, we need to fill any gap between // the last message we know of and the last activity timestamp specified on the thread. - if r.Backfill == nil && r.Raw != nil && lastMessage != nil && r.Raw.LastActivityTimestampMs > lastMessage.Timestamp.UnixMilli() { + lastActivity := r.getLastActivityTimestampMs() + if r.Backfill == nil && lastActivity != 0 && lastMessage != nil && lastActivity > lastMessage.Timestamp.UnixMilli() { zerolog.Ctx(ctx).Debug(). Int64("last_message_ts", lastMessage.Timestamp.UnixMilli()). - Int64("thread_last_activity_ts", r.Raw.LastActivityTimestampMs). + Int64("thread_last_activity_ts", lastActivity). Msg("Thread has newer activity than last known message, triggering forward backfill") return true, nil } @@ -669,6 +697,33 @@ func (r *FBChatResync) GetBundledBackfillData() any { return r.Backfill } +func (r *FBChatResync) getLastActivityTimestampMs() int64 { + if r.Raw != nil { + return r.Raw.LastActivityTimestampMs + } else if r.Update != nil { + return r.Update.LastActivityTimestampMs + } + return 0 +} + +func (r *FBChatResync) getIGFolder() int64 { + if r.Raw != nil { + return r.Raw.IgFolder + } else if r.Update != nil { + return r.Update.IgFolder + } + return 0 +} + +func (r *FBChatResync) getGroupNotificationSettings() int64 { + if r.Raw != nil { + return r.Raw.GroupNotificationSettings + } else if r.Update != nil { + return r.Update.GroupNotificationSettings + } + return 0 +} + type FBFolderResync struct { PortalKey networkid.PortalKey LSUpsertFolder *table.LSUpsertFolder diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 09825337..edf0bf37 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -30,15 +30,16 @@ import ( ) var ( - _ bridgev2.EditHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.ReactionHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.RedactionHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.ReadReceiptHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.ChatViewingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.TypingHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.DeleteChatHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.RoomNameHandlingNetworkAPI = (*MetaClient)(nil) - _ bridgev2.RoomAvatarHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.EditHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.ReactionHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.RedactionHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.ReadReceiptHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.ChatViewingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.TypingHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.MessageRequestAcceptingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.DeleteChatHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.RoomNameHandlingNetworkAPI = (*MetaClient)(nil) + _ bridgev2.RoomAvatarHandlingNetworkAPI = (*MetaClient)(nil) ) var _ bridgev2.TransactionIDGeneratingNetwork = (*MetaConnector)(nil) @@ -694,6 +695,27 @@ func (t *MetaClient) HandleMatrixDeleteChat(ctx context.Context, chat *bridgev2. return fmt.Errorf("unknown platform for deleting chat: %v", platform) } +func (m *MetaClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg *bridgev2.MatrixAcceptMessageRequest) error { + threadID := metaid.ParseFBPortalID(msg.Portal.ID) + platform := m.LoginMeta.Platform + + zerolog.Ctx(ctx).Info(). + Int64("thread_id", threadID). + Any("platform", platform). + Bool("implicit", msg.Content != nil && msg.Content.IsImplicit). + Msg("Accepting message request") + + if platform.IsInstagram() { + return m.Client.Instagram.AcceptMessageRequest(ctx, strconv.FormatInt(threadID, 10)) + } + + return bridgev2.WrapErrorInStatus(fmt.Errorf("accepting message requests is not implemented for %v", platform)). + WithIsCertain(true). + WithErrorAsMessage(). + WithSendNotice(false). + WithErrorReason(event.MessageStatusUnsupported) +} + func (m *MetaClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) { if msg.Portal.RoomType == database.RoomTypeDM { return false, fmt.Errorf("renaming not supported in DMs") diff --git a/pkg/connector/handlemeta.go b/pkg/connector/handlemeta.go index 147a0d4e..4cf8ba13 100644 --- a/pkg/connector/handlemeta.go +++ b/pkg/connector/handlemeta.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exmaps" + "go.mau.fi/util/ptr" "golang.org/x/exp/maps" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" @@ -403,7 +404,20 @@ func (m *MetaClient) parseTable(ctx context.Context, tbl *table.LSTable) (innerQ UncertainReceiver: thread.ThreadType == table.UNKNOWN_THREAD_TYPE, } } - // TODO resync threads with LSUpdateOrInsertThread? + for _, thread := range tbl.LSUpdateOrInsertThread { + if _, ok := threadResyncs[thread.ThreadKey]; ok { + continue + } + threadResyncs[thread.ThreadKey] = &FBChatResync{ + PortalKey: m.makeFBPortalKey(thread.ThreadKey, thread.ThreadType), + Info: m.wrapChatInfo(thread), + Update: thread, + Members: make(map[int64]bridgev2.ChatMember), + m: m, + + UncertainReceiver: thread.ThreadType == table.UNKNOWN_THREAD_TYPE, + } + } // Deleting a thread will cancel all further events, so handle those first collectPortalEvents(params, tbl.LSDeleteThread, m.handleDeleteThread, &innerQueue) @@ -443,6 +457,7 @@ func (m *MetaClient) parseTable(ctx context.Context, tbl *table.LSTable) (innerQ collectPortalEvents(params, tbl.LSUpdateTypingIndicator, m.handleTypingIndicator, &innerQueue) collectPortalEvents(params, tbl.LSDeleteMessage, m.handleDeleteMessage, &innerQueue) collectPortalEvents(params, tbl.LSDeleteThenInsertMessage, m.handleDeleteThenInsertMessage, &innerQueue) + collectPortalEvents(params, tbl.LSDeleteThenInsertMessageRequest, m.handleDeleteThenInsertMessageRequest, &innerQueue) collectPortalEvents(params, tbl.LSUpsertReaction, m.handleUpsertReaction, &innerQueue) collectPortalEvents(params, tbl.LSDeleteReaction, m.handleDeleteReaction, &innerQueue) collectPortalEvents(params, tbl.LSRemoveParticipantFromThread, m.handleRemoveParticipant, &innerQueue) @@ -552,6 +567,21 @@ func (m *MetaClient) handleDeleteThenInsertMessage(tk handlerParams, msg *table. return wrapMessageDelete(tk.Portal, tk.IsUncertainReceiver(), msg.MessageId) } +func (m *MetaClient) handleDeleteThenInsertMessageRequest(tk handlerParams, msg *table.LSDeleteThenInsertMessageRequest) bridgev2.RemoteEvent { + if tk.Sync != nil { + if tk.Sync.Raw != nil && tk.Sync.Raw.FolderName == folderSpam { + return nil + } + tk.Sync.Info.MessageRequest = ptr.Ptr(true) + return nil + } + return m.wrapChatInfoChange(msg.ThreadKey, 0, tk.Type, &bridgev2.ChatInfoChange{ + ChatInfo: &bridgev2.ChatInfo{ + MessageRequest: ptr.Ptr(true), + }, + }, "LSDeleteThenInsertMessageRequest") +} + func (m *MetaClient) handleDeleteThreadKey(tk handlerParams, threadKey int64, onlyForMe bool) bridgev2.RemoteEvent { // Only issue the delete if we're confident it's not a delete then insert combination if tk.activeThreads.Has(threadKey) { @@ -821,7 +851,11 @@ func collectPortalEvents[T ThreadKeyable]( if ok { threadType = v.ThreadType } else if syncOK { - threadType = sync.Raw.ThreadType + if sync.Raw != nil { + threadType = sync.Raw.ThreadType + } else { + threadType = sync.Update.ThreadType + } } // TODO this check isn't needed for all types parentKey, threadMsgID, err := p.m.Main.DB.GetThreadByKey(p.ctx, threadKey) diff --git a/pkg/messagix/graphql.go b/pkg/messagix/graphql.go index 60a320c6..c1b51c97 100644 --- a/pkg/messagix/graphql.go +++ b/pkg/messagix/graphql.go @@ -136,10 +136,10 @@ func (c *Client) makeGraphQLRequest(ctx context.Context, name string, variables payload.FbAPIReqFriendlyName = graphQLDoc.FriendlyName payload.Variables = string(vBytes) payload.ServerTimestamps = "true" + payload.DocID = graphQLDoc.DocID if graphQLDoc.ClientDocID != "" { payload.ClientDocID = graphQLDoc.ClientDocID - } else { - payload.DocID = graphQLDoc.DocId + payload.DocID = "" } payload.Jssesw = graphQLDoc.Jsessw diff --git a/pkg/messagix/graphql/docs.go b/pkg/messagix/graphql/docs.go index d433fc12..e6338df9 100644 --- a/pkg/messagix/graphql/docs.go +++ b/pkg/messagix/graphql/docs.go @@ -1,7 +1,7 @@ package graphql type GraphQLDoc struct { - DocId string + DocID string ClientDocID string CallerClass string FriendlyName string @@ -11,31 +11,41 @@ type GraphQLDoc struct { var GraphQLDocs = map[string]GraphQLDoc{ "LSGraphQLRequest": { - DocId: "7357432314358409", + DocID: "7357432314358409", CallerClass: "RelayModern", FriendlyName: "LSPlatformGraphQLLightspeedRequestQuery", }, "LSGraphQLRequestIG": { - DocId: "6195354443842040", + DocID: "6195354443842040", CallerClass: "RelayModern", FriendlyName: "LSPlatformGraphQLLightspeedRequestForIGDQuery", }, "MAWCatQuery": { - DocId: "23999698219677129", + DocID: "23999698219677129", CallerClass: "RelayModern", FriendlyName: "MAWCatQuery", Jsessw: "1", }, "IGDeleteThread": { - DocId: "23915602751379354", + DocID: "23915602751379354", CallerClass: "RelayModern", FriendlyName: "IGDInboxInfoDeleteThreadDialogOffMsysMutation", }, "IGEditGroupTitle": { - DocId: "29088580780787855", + DocID: "29088580780787855", CallerClass: "RelayModern", FriendlyName: "IGDEditThreadNameDialogOffMsysMutation", }, + "IGAcceptMessageRequest": { + DocID: "25093807760274522", + CallerClass: "RelayModern", + FriendlyName: "useIGDirectAcceptMessageRequestMutation", + }, + "IGListMessageRequests": { + DocID: "25843909248644743", + CallerClass: "RelayModern", + FriendlyName: "PolarisDirectMessageRequestQuery", + }, "IGUpdateGroupAvatar": { ClientDocID: "5576567352987267181917649770", CallerClass: "RelayModern", @@ -58,6 +68,21 @@ type IGEditGroupTitleGraphQLRequestPayload struct { NewTitle string `json:"new_title"` } +type IGAcceptMessageRequestGraphQLRequestPayload struct { + ThreadID string `json:"thread_fbid"` + IGInboxFolder *string `json:"ig_inbox_folder"` + OfflineThreadingID string `json:"offline_threading_id"` +} + +type IGListMessageRequestsGraphQLRequestPayload struct { + DeviceIDForIrisSubscription string `json:"device_id_for_iris_subscription"` + EnablePendingThreadsList bool `json:"enable_pending_threads_list"` + IGD30DayAgoTimestampMsRelayProvider string `json:"__relay_internal__pv__IGD30DayAgoTimestampMsrelayprovider"` + IGDPinnedThreadsRenderEnabledGKRelayProvider bool `json:"__relay_internal__pv__IGDPinnedThreadsRenderEnabledGKrelayprovider"` + IGDMaxUnreadMessagesCountRelayProvider int `json:"__relay_internal__pv__IGDMaxUnreadMessagesCountrelayprovider"` + IGDThreadListActionsEnabledGKRelayProvider bool `json:"__relay_internal__pv__IGDThreadListActionsEnabledGKrelayprovider"` +} + type IGEditGroupAvatarGraphQLRequestPayload struct { ThreadID string `json:"ig_thread_igid"` OfflineThreadingID string `json:"offline_threading_id"` diff --git a/pkg/messagix/instagram.go b/pkg/messagix/instagram.go index 32668237..d91c7ae2 100644 --- a/pkg/messagix/instagram.go +++ b/pkg/messagix/instagram.go @@ -11,6 +11,7 @@ import ( "net/http" "net/url" "strconv" + "time" "github.com/google/go-querystring/query" "github.com/google/uuid" @@ -317,6 +318,42 @@ func (ig *InstagramMethods) EditGroupTitle(ctx context.Context, threadID, newTit return nil } +func (ig *InstagramMethods) AcceptMessageRequest(ctx context.Context, threadID string) error { + threadFBID, err := ig.fetchRouteDefinition(ctx, threadID) + if err != nil { + return fmt.Errorf("failed to fetch route definition for thread %s: %w", threadID, err) + } + igVariables := &graphql.IGAcceptMessageRequestGraphQLRequestPayload{ + ThreadID: threadFBID, + IGInboxFolder: nil, + OfflineThreadingID: strconv.FormatInt(methods.GenerateEpochID(), 10), + } + resp, respBody, err := ig.client.makeGraphQLRequest(ctx, "IGAcceptMessageRequest", &igVariables) + if err != nil { + return fmt.Errorf("failed to accept message request for thread %s: %w", threadID, err) + } + if resp.StatusCode >= 300 || resp.StatusCode < 200 { + return fmt.Errorf("failed to accept message request with bad status code %d", resp.StatusCode) + } + + var graphqlResp struct { + Data struct { + AcceptMessageRequest *struct { + ID string `json:"id"` + SystemFolder string `json:"system_folder"` + Folder string `json:"folder"` + } `json:"ig_direct_accept_message_request"` + } `json:"data"` + } + if err = json.Unmarshal(respBody, &graphqlResp); err != nil { + return fmt.Errorf("failed to parse accept message request response for thread %s: %w", threadID, err) + } else if graphqlResp.Data.AcceptMessageRequest == nil { + return fmt.Errorf("accept message request response for thread %s did not contain mutation data", threadID) + } + + return nil +} + func (ig *InstagramMethods) EditGroupAvatar(ctx context.Context, threadID string, avatar []byte) error { detectedType := http.DetectContentType(avatar) if detectedType != "image/jpeg" && detectedType != "image/png" { @@ -372,6 +409,69 @@ func (ig *InstagramMethods) EditGroupAvatar(ctx context.Context, threadID string return nil } +type igListMessageRequestsResponse struct { + Data struct { + Mailbox struct { + ThreadsByFolder struct { + Edges []struct { + Node struct { + Thread struct { + ThreadKey string `json:"thread_key"` + SystemFolder string `json:"system_folder"` + IsGroup bool `json:"is_group"` + } `json:"as_ig_direct_thread"` + } `json:"node"` + } `json:"edges"` + } `json:"threads_by_folder"` + } `json:"get_slide_mailbox_for_iris_subscription"` + } `json:"data"` +} + +func (ig *InstagramMethods) FetchMessageRequests(ctx context.Context) ([]*table.LSVerifyThreadExists, error) { + variables := &graphql.IGListMessageRequestsGraphQLRequestPayload{ + DeviceIDForIrisSubscription: ig.client.configs.BrowserConfigTable.MqttWebDeviceID.ClientID, + EnablePendingThreadsList: true, + IGD30DayAgoTimestampMsRelayProvider: strconv.FormatInt(time.Now().Add(-30*24*time.Hour).UnixMilli(), 10), + IGDPinnedThreadsRenderEnabledGKRelayProvider: true, + IGDMaxUnreadMessagesCountRelayProvider: 5, + IGDThreadListActionsEnabledGKRelayProvider: true, + } + resp, respBody, err := ig.client.makeGraphQLRequest(ctx, "IGListMessageRequests", variables) + if err != nil { + return nil, fmt.Errorf("failed to fetch instagram message requests: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("failed to fetch instagram message requests with bad status code %d", resp.StatusCode) + } + + var parsed igListMessageRequestsResponse + if err = json.Unmarshal(respBody, &parsed); err != nil { + return nil, fmt.Errorf("failed to parse instagram message requests response: %w", err) + } + + threads := make([]*table.LSVerifyThreadExists, 0, len(parsed.Data.Mailbox.ThreadsByFolder.Edges)) + for _, edge := range parsed.Data.Mailbox.ThreadsByFolder.Edges { + threadKey, err := strconv.ParseInt(edge.Node.Thread.ThreadKey, 10, 64) + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Str("thread_key", edge.Node.Thread.ThreadKey).Msg("Failed to parse pending instagram thread key") + continue + } + threadType := table.ONE_TO_ONE + if edge.Node.Thread.IsGroup { + threadType = table.GROUP_THREAD + } + threads = append(threads, &table.LSVerifyThreadExists{ + ThreadKey: threadKey, + ThreadType: threadType, + FolderName: "pending", + SyncGroup: 1, + }) + } + + zerolog.Ctx(ctx).Debug().Int("thread_count", len(threads)).Msg("Fetched Instagram message requests") + return threads, nil +} + func (ig *InstagramMethods) RemoveGroupAvatar(ctx context.Context, threadID string) error { igVariables := &graphql.IGEditGroupAvatarGraphQLRequestPayload{ ThreadID: threadID, diff --git a/pkg/messagix/table/messages.go b/pkg/messagix/table/messages.go index a6908428..9f0cb2dd 100644 --- a/pkg/messagix/table/messages.go +++ b/pkg/messagix/table/messages.go @@ -603,6 +603,10 @@ type LSDeleteThenInsertMessageRequest struct { Unrecognized map[int]any `json:",omitempty"` } +func (ls *LSDeleteThenInsertMessageRequest) GetThreadKey() int64 { + return ls.ThreadKey +} + type LSMarkOptimisticMessageFailed struct { OTID string `index:"0" json:",omitempty"` Message string `index:"1" json:",omitempty"`