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
7 changes: 5 additions & 2 deletions pkg/connector/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pkg/connector/chatinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
83 changes: 69 additions & 14 deletions pkg/connector/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we check spam first? Is it possible to have a folder be a request and marked as spam?

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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
40 changes: 31 additions & 9 deletions pkg/connector/handlematrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
38 changes: 36 additions & 2 deletions pkg/connector/handlemeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pkg/messagix/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 31 additions & 6 deletions pkg/messagix/graphql/docs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package graphql

type GraphQLDoc struct {
DocId string
DocID string
ClientDocID string
CallerClass string
FriendlyName string
Expand All @@ -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",
Expand All @@ -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"`
Expand Down
Loading
Loading