Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions pkg/connector/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ func (m *MetaClient) connectWithTable(ctx context.Context, initialTable *table.L
m.UserLogin.RemoteProfile.Avatar = m.Ghost.AvatarMXC

m.initialTable.Store(initialTable)
m.startMessageRequestSync(ctx)
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.

Is the extra call required? If message requests are already pushed while online and included in the 20 recent chats that are in the initial table after full reconnects, then I don't think we really need the extra request.

Could maybe be made configurable to sync everything?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated, you're right, removed the extra call


err = m.Client.Connect(ctx)
if err != nil {
Expand All @@ -376,6 +377,7 @@ func (m *MetaClient) connectWithCache(ctx context.Context) {
go m.handleTableLoop(ctx)

m.initialTableHandled.Store(true)
m.startMessageRequestSync(ctx)
err := m.Client.Connect(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to connect")
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 @@ -29,15 +29,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 @@ -682,6 +683,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 @@ -392,7 +393,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 @@ -432,6 +446,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 @@ -539,6 +554,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 @@ -808,7 +838,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
43 changes: 43 additions & 0 deletions pkg/connector/messagerequests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package connector

import (
"context"
"fmt"

"github.com/rs/zerolog"
)

func (m *MetaClient) syncInstagramMessageRequests(ctx context.Context) error {
if !m.LoginMeta.Platform.IsInstagram() {
return nil
} else if m.Client == nil || m.Client.Instagram == nil {
return fmt.Errorf("instagram client is not available")
}

threads, err := m.Client.Instagram.FetchMessageRequests(ctx)
if err != nil {
return err
}

log := zerolog.Ctx(ctx)
log.Debug().Int("thread_count", len(threads)).Msg("Queueing Instagram message requests")
for _, thread := range threads {
m.UserLogin.QueueRemoteEvent(&VerifyThreadExistsEvent{
LSVerifyThreadExists: thread,
m: m,
})
}

return nil
}

func (m *MetaClient) startMessageRequestSync(ctx context.Context) {
if !m.LoginMeta.Platform.IsInstagram() {
return
}
go func() {
if err := m.syncInstagramMessageRequests(ctx); err != nil {
m.UserLogin.Log.Err(err).Msg("Instagram message request sync failed")
}
}()
}
2 changes: 1 addition & 1 deletion pkg/messagix/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ 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
payload.DocID = graphQLDoc.DocID
payload.Jssesw = graphQLDoc.Jsessw

form, err := query.Values(payload)
Expand Down
Loading
Loading