From efcc2d2de3b4ab7dae71ba1ab8591df57c13e85b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 07:11:05 -0400 Subject: [PATCH 01/12] connector: handle failed chat syncs and invalid push data --- pkg/connector/handlewhatsapp.go | 4 +++- pkg/connector/push.go | 3 +++ pkg/messagix/pushcrypto/decrypt.go | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 8bf72e36..d0f7e4b3 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -32,7 +32,9 @@ func (m *MetaClient) e2eeEventHandler(rawEvt any) bool { Any("ig_transport", evt.IGTransport). Any("payload", evt.Message). Msg("Received WhatsApp message") - m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &EnsureWAChatStateEvent{JID: evt.Info.Chat, m: m}) + if !m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &EnsureWAChatStateEvent{JID: evt.Info.Chat, m: m}).Success { + return false + } return m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &WAMessageEvent{FBMessage: evt, m: m}).Success case *events.ChatPresence: m.handleWAChatPresence(m.Main.Bridge.BackgroundCtx, evt) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index 9f265ed3..675d0f65 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -58,6 +58,9 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri if pushType != bridgev2.PushTypeWeb { return fmt.Errorf("unsupported push type %s", pushType) } + if token == "" { + return errors.New("empty push token") + } meta := m.UserLogin.Metadata.(*metaid.UserLoginMetadata) if meta.PushKeys == nil { meta.GeneratePushKeys() diff --git a/pkg/messagix/pushcrypto/decrypt.go b/pkg/messagix/pushcrypto/decrypt.go index c40c9ea4..32706730 100644 --- a/pkg/messagix/pushcrypto/decrypt.go +++ b/pkg/messagix/pushcrypto/decrypt.go @@ -110,6 +110,9 @@ func (keys *PushKeys) Decrypt(ctx context.Context, push json.RawMessage) (*Decry if err != nil { return nil, fmt.Errorf("failed to open gcm: %w", err) } + if len(decrypted) < 2 { + return nil, errors.New("missing push padding length") + } padLen := int(binary.BigEndian.Uint16(decrypted[:2])) if 2+padLen > len(decrypted) { return nil, errors.New("invalid padding length") From 775fcf1be283b2f3b05e5f15729588e93e465245 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 09:09:26 -0400 Subject: [PATCH 02/12] connector: retry failed push catch-up --- pkg/connector/push.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index 675d0f65..21b69647 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -121,17 +121,15 @@ type connectBackgroundEvent struct { isProcessing bool } -func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcrypto.DecryptedPushData, parsed *methods.MetaMessageID) { +func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcrypto.DecryptedPushData, parsed *methods.MetaMessageID) error { if pd == nil || parsed == nil { - return + return nil } log := zerolog.Ctx(ctx) msgID := parsed.String() part, err := m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(msgID)) if err != nil { - log.Err(err).Str("message_id", msgID). - Msg("Failed to look up push message in database") - return + return fmt.Errorf("failed to look up push message %s: %w", msgID, err) } else if part != nil { log.Debug(). Str("message_id", msgID). @@ -139,7 +137,7 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp Str("f_param", pd.Params["f"]). Stringer("event_id", part.MXID). Msg("Confirmed push message was bridged") - return + return nil } threadType := table.ONE_TO_ONE chatID := parsed.ChatID @@ -163,10 +161,12 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp LatestMessageTS: parsed.Time, }) log.Debug().Any("result", res).Msg("Event handling result for push backfill") + if !res.Success { + return errors.Join(errors.New("failed to queue push backfill"), res.Error) + } part, err = m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(msgID)) if err != nil { - log.Err(err).Str("message_id", msgID). - Msg("Failed to look up push message in database after backfill") + return fmt.Errorf("failed to look up push message %s after backfill: %w", msgID, err) } else if part != nil { log.Debug(). Str("message_id", msgID). @@ -174,10 +174,9 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp Stringer("event_id", part.MXID). Msg("Confirmed push message was bridged after backfill") } else { - log.Warn(). - Str("message_id", msgID). - Msg("Push message still wasn't bridged after backfill") + return fmt.Errorf("push message %s wasn't bridged after backfill", msgID) } + return nil } func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.ConnectBackgroundParams) error { @@ -234,8 +233,7 @@ func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.Con Bool("wa_queue_empty", waDone). Int("wa_message_count", waCount). Msg("Closing background connection due to timeout") - m.ensurePushMessageReceived(ctx, data, parsedMsgID) - return nil + return m.ensurePushMessageReceived(ctx, data, parsedMsgID) case <-ctx.Done(): log.Debug(). Bool("fb_tables_received", anythingReceived). @@ -243,7 +241,7 @@ func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.Con Bool("wa_queue_empty", waDone). Int("wa_message_count", waCount). Msg("Closing background connection due to cancellation") - return nil + return ctx.Err() case <-waOfflineSyncChan: waOfflineSyncChan = nil waDone = true From 5b649e4a6153a150813c6001ee61256af07ed0f1 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 09:48:23 -0400 Subject: [PATCH 03/12] connector: retry incomplete encrypted background sync --- pkg/connector/push.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index 21b69647..ba5dec6c 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -233,6 +233,9 @@ func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.Con Bool("wa_queue_empty", waDone). Int("wa_message_count", waCount). Msg("Closing background connection due to timeout") + if parsedMsgID == nil && m.LoginMeta.Platform.IsMessenger() && !m.connectBackgroundWAOfflineSync.IsSet() { + return errors.New("encrypted offline sync did not complete") + } return m.ensurePushMessageReceived(ctx, data, parsedMsgID) case <-ctx.Done(): log.Debug(). From 26953aadb62b75329e64bb844c7c3feedd4cecc1 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 16:29:08 -0400 Subject: [PATCH 04/12] connector: add native Messenger push notifications --- pkg/connector/client.go | 40 +++++--- pkg/connector/login.go | 131 +++++++++++++++++++++++---- pkg/connector/push.go | 70 +++++++++++++- pkg/messagix/messengerlite.go | 54 ++++++++++- pkg/messagix/nativepush.go | 127 ++++++++++++++++++++++++++ pkg/messagix/pushcrypto/native.go | 30 ++++++ pkg/messagix/types/native_session.go | 10 ++ pkg/metaid/dbmeta.go | 14 +-- 8 files changed, 431 insertions(+), 45 deletions(-) create mode 100644 pkg/messagix/nativepush.go create mode 100644 pkg/messagix/pushcrypto/native.go create mode 100644 pkg/messagix/types/native_session.go diff --git a/pkg/connector/client.go b/pkg/connector/client.go index adc016a4..a0c4d30b 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -36,15 +36,16 @@ type MetaClient struct { UserLogin *bridgev2.UserLogin Ghost *bridgev2.Ghost - stopHandlingTables atomic.Pointer[context.CancelFunc] - initialTable atomic.Pointer[table.LSTable] - initialTableHandled atomic.Bool - parsedTables chan *parsedTable - backfillCollectors map[int64]*BackfillCollector - backfillLock sync.Mutex - connectLock sync.Mutex - stopConnectAttempt atomic.Pointer[context.CancelFunc] - permanentErrored atomic.Bool + stopHandlingTables atomic.Pointer[context.CancelFunc] + initialTable atomic.Pointer[table.LSTable] + initialTableHandled atomic.Bool + parsedTables chan *parsedTable + backfillCollectors map[int64]*BackfillCollector + backfillLock sync.Mutex + connectLock sync.Mutex + pushRegistrationLock sync.Mutex + stopConnectAttempt atomic.Pointer[context.CancelFunc] + permanentErrored atomic.Bool editChannels *exsync.Map[string, chan *FBEditEvent] @@ -81,6 +82,9 @@ func (m *MetaConnector) getMessagixConfig() *messagix.Config { func (m *MetaConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { loginMetadata := login.Metadata.(*metaid.UserLoginMetadata) + if err := validateNativeSession(loginMetadata.NativeSession); err != nil { + return err + } c := &MetaClient{ Main: m, LoginMeta: loginMetadata, @@ -150,6 +154,7 @@ func (m *MetaClient) ensureMessagixClient() { m.Main.getMessagixConfig(), ) m.Client.SetEventHandler(m.handleMetaEvent) + m.Client.MessengerLite.SetNativeSession(m.LoginMeta.NativeSession) } } @@ -157,7 +162,14 @@ func (m *MetaClient) ExportCredentials(ctx context.Context) any { if m.Client == nil { return nil } - return m.Client.GetCookies() + if m.LoginMeta.NativeSession == nil { + return m.Client.GetCookies() + } + return &metaCredentials{ + Platform: m.LoginMeta.Platform, + Cookies: m.Client.GetCookies(), + NativeSession: m.LoginMeta.NativeSession, + } } func (m *MetaClient) Connect(ctx context.Context) { @@ -454,9 +466,11 @@ func (m *MetaClient) connectE2EE() error { if m.WADevice == nil { isNew = true m.WADevice = m.Main.DeviceStore.NewDevice() - } - if suggested := m.Client.MessengerLite.GetSuggestedDeviceID(); suggested != uuid.Nil { - m.WADevice.FacebookUUID = suggested + if session := m.LoginMeta.NativeSession; session != nil { + m.WADevice.FacebookUUID = session.DeviceID + } else if suggested := m.Client.MessengerLite.GetSuggestedDeviceID(); suggested != uuid.Nil { + m.WADevice.FacebookUUID = suggested + } } m.Client.SetDevice(m.WADevice) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index e10add31..3aab6cb8 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -2,6 +2,7 @@ package connector import ( "context" + "encoding/json" "errors" "fmt" "maps" @@ -9,7 +10,9 @@ import ( "slices" "time" + "github.com/google/uuid" "github.com/rs/zerolog" + waTypes "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/status" @@ -73,24 +76,59 @@ func (m *MetaConnector) CreateLogin(ctx context.Context, user *bridgev2.User, fl }, nil } -// This creates a user login using credentials transferred from another instance of the meta bridge, -// via the `ExportCredentials` API. +type metaCredentials struct { + Platform types.Platform `json:"platform"` + Cookies *cookies.Cookies `json:"cookies"` + NativeSession *types.NativeSession `json:"native_session,omitempty"` +} + +func validateNativeSession(session *types.NativeSession) error { + if session != nil && (session.AccessToken == "" || session.AppID != useragent.MessengerLiteAndroidAppID || session.DeviceID == uuid.Nil || session.FamilyDeviceID == uuid.Nil) { + return fmt.Errorf("invalid native Messenger session") + } + return nil +} + func (m *MetaConnector) CreateUserLoginFromCredentials(ctx context.Context, user *bridgev2.User, credentials any) error { creds, ok := credentials.(map[string]any) if !ok { return fmt.Errorf("invalid credentials type: %T", credentials) } - cleanCreds := make(map[string]string, len(creds)) - for k, v := range creds { - cleanCreds[k] = v.(string) + var transferred metaCredentials + if _, structured := creds["cookies"]; structured { + data, err := json.Marshal(creds) + if err != nil { + return fmt.Errorf("failed to encode transferred credentials: %w", err) + } else if err = json.Unmarshal(data, &transferred); err != nil { + return fmt.Errorf("failed to decode transferred credentials: %w", err) + } else if transferred.Cookies == nil || !transferred.Platform.IsMessenger() { + return fmt.Errorf("invalid transferred Messenger credentials") + } else if err = validateNativeSession(transferred.NativeSession); err != nil { + return err + } + transferred.Cookies.Platform = transferred.Platform + } else { + transferred.Platform = types.Facebook + if m.Config.Tor { + transferred.Platform = types.FacebookTor + } + transferred.Cookies = &cookies.Cookies{Platform: transferred.Platform} + values := make(map[cookies.MetaCookieName]string, len(creds)) + for key, value := range creds { + str, ok := value.(string) + if !ok { + return fmt.Errorf("invalid cookie value type for %s", key) + } + values[cookies.MetaCookieName(key)] = str + } + transferred.Cookies.UpdateValues(values) } - - login, err := m.CreateLogin(ctx, user, FlowIDFacebookCookies) + log := zerolog.Ctx(ctx).With().Str("component", "messagix").Logger() + client, err := getMessagixClient(log, m, transferred.Cookies, m.Config.ProxyOther) if err != nil { return err } - - step, err := login.(bridgev2.LoginProcessCookies).SubmitCookies(ctx, cleanCreds) + step, err := loginWithCookies(ctx, log, client, user, m, transferred.Cookies, transferred.NativeSession, true) if err != nil { return err } else if step.Type != bridgev2.LoginStepTypeComplete { @@ -124,7 +162,7 @@ var ( ) func (m *MetaConnector) GetLoginFlows() []bridgev2.LoginFlow { - return []bridgev2.LoginFlow{loginFlowFacebook, loginFlowMessenger, loginFlowMessengerLiteIOS, loginFlowMessengerLiteAndroid} + return []bridgev2.LoginFlow{loginFlowMessengerLiteAndroid, loginFlowFacebook, loginFlowMessenger, loginFlowMessengerLiteIOS} } type MetaCookieLogin struct { @@ -198,7 +236,15 @@ func loginWithCookies( bridgeUser *bridgev2.User, conn *MetaConnector, c *cookies.Cookies, + nativeSession *types.NativeSession, + transfer bool, ) (*bridgev2.LoginStep, error) { + if missing := c.GetMissingCookieNames(); len(missing) > 0 { + return nil, loginerrors.MissingCookies.AppendMessage(": %v", missing) + } else if err := validateNativeSession(nativeSession); err != nil { + return nil, err + } + client.MessengerLite.SetNativeSession(nativeSession) log.Debug(). Strs("cookie_names", exslices.CastToString[string](slices.Collect(maps.Keys(c.GetAll())))). @@ -221,10 +267,65 @@ func loginWithCookies( id := user.GetFBID() loginID := metaid.MakeUserLoginID(id) + if id != c.GetUserID() { + return nil, fmt.Errorf("logged-in account does not match cookies") + } var loginUA string if req, ok := ctx.Value("fi.mau.provision.request").(*http.Request); ok { loginUA = req.Header.Get("User-Agent") } + metadata := &metaid.UserLoginMetadata{} + existing, err := conn.Bridge.GetExistingUserLoginByID(ctx, loginID) + if err != nil { + return nil, fmt.Errorf("failed to load existing login: %w", err) + } else if existing != nil && existing.UserMXID != bridgeUser.MXID { + return nil, fmt.Errorf("that account is already logged in by another user") + } else if existing != nil { + *metadata = *existing.Metadata.(*metaid.UserLoginMetadata) + } + if nativeSession != nil { + deviceID := nativeSession.DeviceID + if transfer { + deviceID = uuid.New() + } + if metadata.WADeviceID != 0 { + device, err := conn.DeviceStore.GetDevice(ctx, waTypes.JID{User: string(loginID), Device: metadata.WADeviceID, Server: waTypes.MessengerServer}) + if err != nil { + return nil, fmt.Errorf("failed to load existing encrypted device: %w", err) + } else if device != nil { + if device.FacebookUUID == uuid.Nil { + return nil, fmt.Errorf("existing encrypted device has no native identity") + } + deviceID = device.FacebookUUID + } else { + metadata.WADeviceID = 0 + } + } + if deviceID != nativeSession.DeviceID { + authClient, err := getMessagixClient(log, conn, &cookies.Cookies{Platform: types.MessengerLiteAndroid}, conn.Config.ProxyMessengerLite) + if err != nil { + return nil, err + } + updated := *nativeSession + updated.DeviceID = deviceID + authClient.MessengerLite.SetNativeSession(&updated) + newCookies, err := authClient.MessengerLite.ExchangeTransientToken(ctx, nativeSession.AccessToken) + if err != nil { + return nil, fmt.Errorf("failed to prepare native session for encrypted device: %w", err) + } else if newCookies.GetUserID() != id { + return nil, fmt.Errorf("exchanged native session account does not match login") + } + nativeSession = authClient.MessengerLite.GetNativeSession() + } + } + metadata.Platform = c.Platform + metadata.Cookies = c + metadata.LoginUA = loginUA + metadata.NativeSession = nativeSession + client.MessengerLite.SetNativeSession(nativeSession) + if existing != nil && existing.Client != nil { + existing.Client.Disconnect() + } ul, err := bridgeUser.NewLogin(ctx, &database.UserLogin{ ID: loginID, @@ -232,11 +333,7 @@ func loginWithCookies( RemoteProfile: status.RemoteProfile{ Name: user.GetName(), }, - Metadata: &metaid.UserLoginMetadata{ - Platform: c.Platform, - Cookies: c, - LoginUA: loginUA, - }, + Metadata: metadata, }, nil) if err != nil { return nil, fmt.Errorf("failed to save new login: %w", err) @@ -281,7 +378,7 @@ func (m *MetaCookieLogin) SubmitCookies(ctx context.Context, strCookies map[stri if err != nil { return nil, err } - return loginWithCookies(ctx, log, client, m.User, m.Main, c) + return loginWithCookies(ctx, log, client, m.User, m.Main, c, nil, false) } type MetaNativeLogin struct { @@ -366,7 +463,7 @@ func (m *MetaNativeLogin) proceed(ctx context.Context, userInput map[string]stri newClient.GetCookies().UpdateValues(newCookies.GetAll()) - step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies) + step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies, m.SavedClient.MessengerLite.GetNativeSession(), false) if err != nil { return nil, err } diff --git a/pkg/connector/push.go b/pkg/connector/push.go index ba5dec6c..b779baff 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -33,6 +33,7 @@ import ( "go.mau.fi/mautrix-meta/pkg/messagix/pushcrypto" "go.mau.fi/mautrix-meta/pkg/messagix/table" "go.mau.fi/mautrix-meta/pkg/messagix/types" + "go.mau.fi/mautrix-meta/pkg/messagix/useragent" "go.mau.fi/mautrix-meta/pkg/metaid" ) @@ -45,7 +46,14 @@ var pushCfg = &bridgev2.PushConfig{ Web: &bridgev2.WebPushConfig{VapidKey: "BIBn3E_rWTci8Xn6P9Xj3btShT85Wdtne0LtwNUyRQ5XjFNkuTq9j4MPAVLvAFhXrUU1A9UxyxBA7YIOjqDIDHI"}, } +var nativePushCfg = &bridgev2.PushConfig{ + FCM: &bridgev2.FCMPushConfig{SenderID: "622912139302"}, +} + func (m *MetaClient) GetPushConfigs() *bridgev2.PushConfig { + if session := m.LoginMeta.NativeSession; session != nil && session.AccessToken != "" && session.AppID == useragent.MessengerLiteAndroidAppID { + return nativePushCfg + } return pushCfg } @@ -55,12 +63,16 @@ type DoubleToken struct { } func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error { - if pushType != bridgev2.PushTypeWeb { - return fmt.Errorf("unsupported push type %s", pushType) - } + m.pushRegistrationLock.Lock() + defer m.pushRegistrationLock.Unlock() if token == "" { return errors.New("empty push token") } + if pushType == bridgev2.PushTypeFCM { + return m.registerNativePush(ctx, token) + } else if pushType != bridgev2.PushTypeWeb { + return fmt.Errorf("unsupported push type %s", pushType) + } meta := m.UserLogin.Metadata.(*metaid.UserLoginMetadata) if meta.PushKeys == nil { meta.GeneratePushKeys() @@ -84,6 +96,9 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri encToken = dt.Encrypted } if encToken != "" { + if m.E2EEClient == nil { + return ErrNotConnected + } err := m.E2EEClient.RegisterForPushNotifications(ctx, &whatsmeow.WebPushConfig{ Endpoint: encToken, Auth: meta.PushKeys.Auth, @@ -108,6 +123,43 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri return err } +func (m *MetaClient) registerNativePush(ctx context.Context, token string) error { + if m.Client == nil || m.LoginMeta.NativeSession == nil { + return bridgev2.ErrNotLoggedIn + } + if err := m.e2eeConnectWaiter.WaitTimeoutCtx(ctx, ConnectWaitTimeout); err != nil { + return fmt.Errorf("waiting for encrypted push connection: %w", err) + } + client := m.Client + e2eeClient := m.E2EEClient + device := m.WADevice + if client == nil || e2eeClient == nil || !e2eeClient.IsLoggedIn() || device == nil { + return ErrNotConnected + } + session := m.LoginMeta.NativeSession + if device.FacebookUUID != session.DeviceID { + return errors.New("native push session does not match encrypted device") + } + if m.LoginMeta.NativePushKeys == nil { + keys, err := pushcrypto.NewNativePushKeys(metaid.ParseUserLoginID(m.UserLogin.ID)) + if err != nil { + return fmt.Errorf("failed to generate native push keys: %w", err) + } + m.LoginMeta.NativePushKeys = keys + if err = m.UserLogin.Save(ctx); err != nil { + m.LoginMeta.NativePushKeys = nil + return fmt.Errorf("failed to save native push keys: %w", err) + } + } + if err := client.Facebook.RegisterNativePushNotifications(ctx, session, token, m.LoginMeta.NativePushKeys); err != nil { + return err + } + if err := e2eeClient.RegisterForPushNotifications(ctx, &messagix.NativePushConfig{AppID: session.AppID, DeviceID: session.DeviceID}); err != nil { + return fmt.Errorf("failed to register encrypted native push: %w", err) + } + return nil +} + func (m *MetaClient) notifyBackgroundConnAboutEvent(isProcessing bool) { if ch := m.connectBackgroundEvt; ch != nil { select { @@ -182,7 +234,17 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.ConnectBackgroundParams) error { log := zerolog.Ctx(ctx) var parsedMsgID *methods.MetaMessageID - data, err := m.UserLogin.Metadata.(*metaid.UserLoginMetadata).PushKeys.Decrypt(ctx, params.RawData) + var envelope struct { + PIM string `json:"pim"` + Data struct { + PIM string `json:"pim"` + } `json:"data"` + } + var data *pushcrypto.DecryptedPushData + var err error + if json.Unmarshal(params.RawData, &envelope) != nil || (envelope.PIM == "" && envelope.Data.PIM == "") { + data, err = m.LoginMeta.PushKeys.Decrypt(ctx, params.RawData) + } if err != nil { log.Warn().Err(err).Msg("Failed to decrypt web push") } else if data != nil { diff --git a/pkg/messagix/messengerlite.go b/pkg/messagix/messengerlite.go index 88488e2e..e75a3ccf 100644 --- a/pkg/messagix/messengerlite.go +++ b/pkg/messagix/messengerlite.go @@ -30,6 +30,23 @@ type MessengerLiteMethods struct { deviceID uuid.UUID familyDeviceID uuid.UUID machineID string + nativeSession *types.NativeSession +} + +func (m *MessengerLiteMethods) GetNativeSession() *types.NativeSession { + if m == nil { + return nil + } + return m.nativeSession +} + +func (m *MessengerLiteMethods) SetNativeSession(session *types.NativeSession) { + if m == nil || session == nil { + return + } + m.nativeSession = session + m.deviceID = session.DeviceID + m.familyDeviceID = session.FamilyDeviceID } func (fb *MessengerLiteMethods) GetSuggestedDeviceID() uuid.UUID { @@ -270,7 +287,7 @@ func (m *MessengerLiteMethods) GetSessionForApp(ctx context.Context, accessToken params.Set("generate_session_cookies", "1") params.Set("generate_analytics_claim", "1") if m.deviceID != uuid.Nil { - params.Set("device_id", strings.ToUpper(m.deviceID.String())) + params.Set("device_id", m.deviceID.String()) } if m.machineID != "" { params.Set("machine_id", m.machineID) @@ -317,7 +334,7 @@ func (m *MessengerLiteMethods) GetSessionForApp(ctx context.Context, accessToken } func (m *MessengerLiteMethods) ExchangeTransientToken(ctx context.Context, accessToken string) (*cookies.Cookies, error) { - resp, err := m.GetSessionForApp(ctx, accessToken, SessionForAppOptions{}) + resp, err := m.GetSessionForApp(ctx, accessToken, SessionForAppOptions{NewAppID: useragent.MessengerLiteAndroidAppID}) if err != nil { return nil, err } @@ -325,6 +342,18 @@ func (m *MessengerLiteMethods) ExchangeTransientToken(ctx context.Context, acces if !newCookies.IsLoggedIn() { return nil, fmt.Errorf("session-for-app response is missing cookies: %v", newCookies.GetMissingCookieNames()) } + uid, err := resp.UID.Int64() + if err != nil || uid <= 0 || uid != newCookies.GetUserID() { + return nil, fmt.Errorf("session-for-app response account does not match cookies") + } else if resp.AccessToken == "" { + return nil, fmt.Errorf("session-for-app response is missing an access token") + } + m.SetNativeSession(&types.NativeSession{ + AccessToken: resp.AccessToken, + AppID: useragent.MessengerLiteAndroidAppID, + DeviceID: m.deviceID, + FamilyDeviceID: m.familyDeviceID, + }) return newCookies, nil } @@ -375,13 +404,13 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s return nil, nil, fmt.Errorf("parsing login response data: %w", err) } - if len(loginRespPayload.SessionCookies) == 0 { + if len(loginRespPayload.SessionCookies) == 0 || (m.client.Platform == types.MessengerLiteIOS && loginRespPayload.AccessToken != "") { if loginRespPayload.AccessToken == "" { return nil, nil, loginerrors.MissingCookies.AppendMessage(". Meta returned incomplete credentials after login. If you don't have MFA enabled, please turn it on for your Facebook account. Otherwise, it may help to try again, log in from the official app/website first, or change the MFA settings for your Facebook account") } m.client.Logger.Debug(). Str("credential_type", loginRespPayload.CredentialType). - Msg("Login response didn't include session cookies, exchanging access token for a session") + Msg("Exchanging login access token for a Messenger Android session") newCookies, err := m.ExchangeTransientToken(ctx, loginRespPayload.AccessToken) if err != nil { m.client.Logger.Warn().Err(err). @@ -389,6 +418,9 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s Msg("Failed to exchange access token for session cookies") return nil, nil, loginerrors.TokenExchange } + if loginRespPayload.UID != 0 && newCookies.GetUserID() != loginRespPayload.UID { + return nil, nil, loginerrors.TokenExchange + } m.client.Logger.Debug(). Str("credential_type", loginRespPayload.CredentialType). Any("cookie_names", slices.Collect(maps.Keys(newCookies.GetAll()))). @@ -396,7 +428,19 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s return nil, newCookies, nil } - return nil, m.convertCookies(loginRespPayload.SessionCookies), nil + newCookies := m.convertCookies(loginRespPayload.SessionCookies) + if newCookies.GetUserID() <= 0 || (loginRespPayload.UID != 0 && loginRespPayload.UID != newCookies.GetUserID()) { + return nil, nil, loginerrors.TokenExchange + } + if loginRespPayload.AccessToken != "" { + m.SetNativeSession(&types.NativeSession{ + AccessToken: loginRespPayload.AccessToken, + AppID: useragent.MessengerLiteAndroidAppID, + DeviceID: m.deviceID, + FamilyDeviceID: m.familyDeviceID, + }) + } + return nil, newCookies, nil } func (m *MessengerLiteMethods) CancelLoginStep(ctx context.Context) error { diff --git a/pkg/messagix/nativepush.go b/pkg/messagix/nativepush.go new file mode 100644 index 00000000..faf91fd3 --- /dev/null +++ b/pkg/messagix/nativepush.go @@ -0,0 +1,127 @@ +package messagix + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/ecdh" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/google/uuid" + waBinary "go.mau.fi/whatsmeow/binary" + + "go.mau.fi/mautrix-meta/pkg/messagix/pushcrypto" + "go.mau.fi/mautrix-meta/pkg/messagix/types" + "go.mau.fi/mautrix-meta/pkg/messagix/useragent" +) + +type NativePushConfig struct { + AppID string + DeviceID uuid.UUID +} + +func (cfg *NativePushConfig) GetPushConfigAttrs() waBinary.Attrs { + return waBinary.Attrs{ + "platform": "fb", + "appid": cfg.AppID, + "deviceid": cfg.DeviceID.String(), + } +} + +func (fb *FacebookMethods) RegisterNativePushNotifications(ctx context.Context, session *types.NativeSession, token string, keys *pushcrypto.NativePushKeys) error { + if session == nil || session.AccessToken == "" || session.AppID != useragent.MessengerLiteAndroidAppID || session.DeviceID == uuid.Nil || session.FamilyDeviceID == uuid.Nil { + return errors.New("missing native Messenger session") + } + userID := fb.client.GetCookies().GetUserID() + if userID <= 0 || token == "" { + return errors.New("missing native push account or token") + } + if keys == nil || len(keys.AESKey) != 32 || keys.AESKeyID>>8 != userID || keys.HPKEKeyID == "" { + return errors.New("invalid native push keys") + } + private, err := ecdh.P256().NewPrivateKey(keys.HPKEPrivateKey) + if err != nil { + return fmt.Errorf("invalid native push private key: %w", err) + } + deviceID := session.DeviceID.String() + androidID := sha256.Sum256([]byte(deviceID)) + protocol, err := json.Marshal(map[string]any{ + "url": "https://fcm.googleapis.com/fcm/send", + "token": token, + "device_id": deviceID, + "family_device_id": strings.ToUpper(session.FamilyDeviceID.String()), + "sub_type": 0, + "encryption": map[string]any{ + "key": base64.StdEncoding.EncodeToString(keys.AESKey) + "\n", + "key_id": keys.AESKeyID, + "algorithm": "AES_GCM", + }, + "hpke_params": map[string]string{ + "hpke_ciphersuite": "1001000010000", + "hpke_pubkey": base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()), + "hpke_keystore_id": keys.HPKEKeyID, + }, + "typed_os_settings": map[string]int{"notificationEnabled": 1}, + "extra_data": map[string]int{ + "android_build": 345613452, + "android_setting_mask": 240, + "orca_muted_until_ms": 0, + "sys_notif": 1, + "messaging_channel_mask": 118, + }, + "android_id": hex.EncodeToString(androidID[:8]), + "request_id": uuid.NewString(), + "client_reported_user_ids": strconv.FormatInt(userID, 10), + }) + if err != nil { + return fmt.Errorf("failed to encode native push parameters: %w", err) + } + form := url.Values{ + "format": {"json"}, + "return_structure": {"1"}, + "protocol_params": {string(protocol)}, + "locale": {"en_US"}, + "client_country_code": {"US"}, + "fb_api_req_friendly_name": {"registerPush"}, + "fb_api_caller_class": {"FacebookPushServerRegisterJobImpl"}, + } + var body bytes.Buffer + compressed := gzip.NewWriter(&body) + if _, err = compressed.Write([]byte(form.Encode())); err != nil { + return fmt.Errorf("failed to compress native push request: %w", err) + } + if err = compressed.Close(); err != nil { + return fmt.Errorf("failed to finish native push request: %w", err) + } + headers := http.Header{} + headers.Set("Authorization", "OAuth "+session.AccessToken) + headers.Set("User-Agent", useragent.MessengerLiteAndroidUserAgent) + headers.Set("X-FB-Connection-Quality", "EXCELLENT") + headers.Set("X-FB-Friendly-Name", "registerPush") + headers.Set("X-ZERO-STATE", "unknown") + headers.Set("X-FB-Family-Device-Id", session.FamilyDeviceID.String()) + headers.Set("Content-Encoding", "gzip") + _, response, err := fb.client.http.MakeRequestOnceNoRedirect(ctx, "https://b-graph.facebook.com/me/register_push_tokens", http.MethodPost, headers, body.Bytes(), types.FORM) + if err != nil { + return fmt.Errorf("failed to register native push: %w", err) + } + var result struct { + Success bool `json:"success"` + DisabledSource int `json:"disabled_source"` + } + if err = json.Unmarshal(response, &result); err != nil { + return fmt.Errorf("failed to decode native push response: %w", err) + } else if !result.Success || result.DisabledSource != 0 { + return fmt.Errorf("native push registration rejected (disabled source %d)", result.DisabledSource) + } + return nil +} diff --git a/pkg/messagix/pushcrypto/native.go b/pkg/messagix/pushcrypto/native.go new file mode 100644 index 00000000..c6a35aff --- /dev/null +++ b/pkg/messagix/pushcrypto/native.go @@ -0,0 +1,30 @@ +package pushcrypto + +import ( + "crypto/ecdh" + "crypto/rand" + "strconv" + "time" +) + +type NativePushKeys struct { + AESKey []byte `json:"aes_key"` + AESKeyID int64 `json:"aes_key_id"` + HPKEPrivateKey []byte `json:"hpke_private_key"` + HPKEKeyID string `json:"hpke_key_id"` +} + +func NewNativePushKeys(userID int64) (*NativePushKeys, error) { + private, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + key := make([]byte, 32) + rand.Read(key) + return &NativePushKeys{ + AESKey: key, + AESKeyID: userID<<8 | 1, + HPKEPrivateKey: private.Bytes(), + HPKEKeyID: strconv.FormatInt(time.Now().UnixMilli(), 10), + }, nil +} diff --git a/pkg/messagix/types/native_session.go b/pkg/messagix/types/native_session.go new file mode 100644 index 00000000..cba5dd33 --- /dev/null +++ b/pkg/messagix/types/native_session.go @@ -0,0 +1,10 @@ +package types + +import "github.com/google/uuid" + +type NativeSession struct { + AccessToken string `json:"access_token"` + AppID string `json:"app_id"` + DeviceID uuid.UUID `json:"device_id"` + FamilyDeviceID uuid.UUID `json:"family_device_id"` +} diff --git a/pkg/metaid/dbmeta.go b/pkg/metaid/dbmeta.go index ace0958e..d90c35a5 100644 --- a/pkg/metaid/dbmeta.go +++ b/pkg/metaid/dbmeta.go @@ -40,12 +40,14 @@ type GhostMetadata struct { } type UserLoginMetadata struct { - Platform types.Platform `json:"platform"` - Cookies *cookies.Cookies `json:"cookies"` - WADeviceID uint16 `json:"wa_device_id,omitempty"` - PushKeys *pushcrypto.PushKeys `json:"push_keys,omitempty"` - LoginUA string `json:"login_ua,omitempty"` - IGID string `json:"igid,omitempty"` + Platform types.Platform `json:"platform"` + Cookies *cookies.Cookies `json:"cookies"` + WADeviceID uint16 `json:"wa_device_id,omitempty"` + PushKeys *pushcrypto.PushKeys `json:"push_keys,omitempty"` + LoginUA string `json:"login_ua,omitempty"` + IGID string `json:"igid,omitempty"` + NativeSession *types.NativeSession `json:"native_session,omitempty"` + NativePushKeys *pushcrypto.NativePushKeys `json:"native_push_keys,omitempty"` // Thread backfill state BackfillCompleted bool `json:"backfill_completed,omitempty"` From d603f8395c7c3dfcfbe39ddff2fc9ab532c2ca1e Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 17:06:25 -0400 Subject: [PATCH 05/12] messagix: preserve iOS login sessions --- pkg/messagix/messengerlite.go | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pkg/messagix/messengerlite.go b/pkg/messagix/messengerlite.go index e75a3ccf..a2e8d797 100644 --- a/pkg/messagix/messengerlite.go +++ b/pkg/messagix/messengerlite.go @@ -334,7 +334,7 @@ func (m *MessengerLiteMethods) GetSessionForApp(ctx context.Context, accessToken } func (m *MessengerLiteMethods) ExchangeTransientToken(ctx context.Context, accessToken string) (*cookies.Cookies, error) { - resp, err := m.GetSessionForApp(ctx, accessToken, SessionForAppOptions{NewAppID: useragent.MessengerLiteAndroidAppID}) + resp, err := m.GetSessionForApp(ctx, accessToken, SessionForAppOptions{}) if err != nil { return nil, err } @@ -342,18 +342,20 @@ func (m *MessengerLiteMethods) ExchangeTransientToken(ctx context.Context, acces if !newCookies.IsLoggedIn() { return nil, fmt.Errorf("session-for-app response is missing cookies: %v", newCookies.GetMissingCookieNames()) } - uid, err := resp.UID.Int64() - if err != nil || uid <= 0 || uid != newCookies.GetUserID() { - return nil, fmt.Errorf("session-for-app response account does not match cookies") - } else if resp.AccessToken == "" { - return nil, fmt.Errorf("session-for-app response is missing an access token") - } - m.SetNativeSession(&types.NativeSession{ - AccessToken: resp.AccessToken, - AppID: useragent.MessengerLiteAndroidAppID, - DeviceID: m.deviceID, - FamilyDeviceID: m.familyDeviceID, - }) + if m.client.Platform == types.MessengerLiteAndroid { + uid, err := resp.UID.Int64() + if err != nil || uid <= 0 || uid != newCookies.GetUserID() { + return nil, fmt.Errorf("session-for-app response account does not match cookies") + } else if resp.AccessToken == "" { + return nil, fmt.Errorf("session-for-app response is missing an access token") + } + m.SetNativeSession(&types.NativeSession{ + AccessToken: resp.AccessToken, + AppID: useragent.MessengerLiteAndroidAppID, + DeviceID: m.deviceID, + FamilyDeviceID: m.familyDeviceID, + }) + } return newCookies, nil } @@ -404,13 +406,13 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s return nil, nil, fmt.Errorf("parsing login response data: %w", err) } - if len(loginRespPayload.SessionCookies) == 0 || (m.client.Platform == types.MessengerLiteIOS && loginRespPayload.AccessToken != "") { + if len(loginRespPayload.SessionCookies) == 0 { if loginRespPayload.AccessToken == "" { return nil, nil, loginerrors.MissingCookies.AppendMessage(". Meta returned incomplete credentials after login. If you don't have MFA enabled, please turn it on for your Facebook account. Otherwise, it may help to try again, log in from the official app/website first, or change the MFA settings for your Facebook account") } m.client.Logger.Debug(). Str("credential_type", loginRespPayload.CredentialType). - Msg("Exchanging login access token for a Messenger Android session") + Msg("Exchanging login access token for session cookies") newCookies, err := m.ExchangeTransientToken(ctx, loginRespPayload.AccessToken) if err != nil { m.client.Logger.Warn().Err(err). @@ -432,7 +434,7 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s if newCookies.GetUserID() <= 0 || (loginRespPayload.UID != 0 && loginRespPayload.UID != newCookies.GetUserID()) { return nil, nil, loginerrors.TokenExchange } - if loginRespPayload.AccessToken != "" { + if m.client.Platform == types.MessengerLiteAndroid && loginRespPayload.AccessToken != "" { m.SetNativeSession(&types.NativeSession{ AccessToken: loginRespPayload.AccessToken, AppID: useragent.MessengerLiteAndroidAppID, From 05bbf173e80939bc49ec2e44c25cf1717de4060f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Sep 2026 14:39:25 +0300 Subject: [PATCH 06/12] Revert login flow order changes Choosing the right flow is the client's responsibility --- pkg/connector/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 3aab6cb8..ff2e0757 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -162,7 +162,7 @@ var ( ) func (m *MetaConnector) GetLoginFlows() []bridgev2.LoginFlow { - return []bridgev2.LoginFlow{loginFlowMessengerLiteAndroid, loginFlowFacebook, loginFlowMessenger, loginFlowMessengerLiteIOS} + return []bridgev2.LoginFlow{loginFlowFacebook, loginFlowMessenger, loginFlowMessengerLiteIOS, loginFlowMessengerLiteAndroid} } type MetaCookieLogin struct { From 66a106ceb97ec22c654a16beeaf2ffff3cbbfe7a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Sep 2026 14:40:06 +0300 Subject: [PATCH 07/12] Remove unused whatsapp push registration --- pkg/connector/push.go | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index b779baff..b1d6e6be 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -24,7 +24,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/whatsmeow" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/simplevent" @@ -57,11 +56,6 @@ func (m *MetaClient) GetPushConfigs() *bridgev2.PushConfig { return pushCfg } -type DoubleToken struct { - Unencrypted string `json:"unencrypted"` - Encrypted string `json:"encrypted"` -} - func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error { m.pushRegistrationLock.Lock() defer m.pushRegistrationLock.Unlock() @@ -85,29 +79,6 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri P256DH: meta.PushKeys.P256DH, Auth: meta.PushKeys.Auth, } - var encToken string - if token[0] == '{' && token[len(token)-1] == '}' { - var dt DoubleToken - err := json.Unmarshal([]byte(token), &dt) - if err != nil { - return fmt.Errorf("failed to unmarshal double token: %w", err) - } - token = dt.Unencrypted - encToken = dt.Encrypted - } - if encToken != "" { - if m.E2EEClient == nil { - return ErrNotConnected - } - err := m.E2EEClient.RegisterForPushNotifications(ctx, &whatsmeow.WebPushConfig{ - Endpoint: encToken, - Auth: meta.PushKeys.Auth, - P256DH: meta.PushKeys.P256DH, - }) - if err != nil { - return fmt.Errorf("failed to register e2ee notifications: %w", err) - } - } cli := m.Client if cli == nil { return messagix.ErrClientIsNil From fc8d097d1d284df8b32d6d8eed6b8b5205b3ea70 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Sep 2026 14:40:25 +0300 Subject: [PATCH 08/12] Remove manual compression --- pkg/messagix/nativepush.go | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pkg/messagix/nativepush.go b/pkg/messagix/nativepush.go index faf91fd3..19e6cd16 100644 --- a/pkg/messagix/nativepush.go +++ b/pkg/messagix/nativepush.go @@ -1,8 +1,6 @@ package messagix import ( - "bytes" - "compress/gzip" "context" "crypto/ecdh" "crypto/sha256" @@ -94,14 +92,6 @@ func (fb *FacebookMethods) RegisterNativePushNotifications(ctx context.Context, "fb_api_req_friendly_name": {"registerPush"}, "fb_api_caller_class": {"FacebookPushServerRegisterJobImpl"}, } - var body bytes.Buffer - compressed := gzip.NewWriter(&body) - if _, err = compressed.Write([]byte(form.Encode())); err != nil { - return fmt.Errorf("failed to compress native push request: %w", err) - } - if err = compressed.Close(); err != nil { - return fmt.Errorf("failed to finish native push request: %w", err) - } headers := http.Header{} headers.Set("Authorization", "OAuth "+session.AccessToken) headers.Set("User-Agent", useragent.MessengerLiteAndroidUserAgent) @@ -109,8 +99,7 @@ func (fb *FacebookMethods) RegisterNativePushNotifications(ctx context.Context, headers.Set("X-FB-Friendly-Name", "registerPush") headers.Set("X-ZERO-STATE", "unknown") headers.Set("X-FB-Family-Device-Id", session.FamilyDeviceID.String()) - headers.Set("Content-Encoding", "gzip") - _, response, err := fb.client.http.MakeRequestOnceNoRedirect(ctx, "https://b-graph.facebook.com/me/register_push_tokens", http.MethodPost, headers, body.Bytes(), types.FORM) + _, response, err := fb.client.http.MakeRequest(ctx, "https://b-graph.facebook.com/me/register_push_tokens", http.MethodPost, headers, []byte(form.Encode()), types.FORM) if err != nil { return fmt.Errorf("failed to register native push: %w", err) } From 37516535de118061d7fca1d36f35f13fe44f1e5a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Sep 2026 14:40:36 +0300 Subject: [PATCH 09/12] Minor cleanup --- pkg/connector/push.go | 2 +- pkg/messagix/pushcrypto/native.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index b1d6e6be..fb43289f 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -185,7 +185,7 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp }) log.Debug().Any("result", res).Msg("Event handling result for push backfill") if !res.Success { - return errors.Join(errors.New("failed to queue push backfill"), res.Error) + return fmt.Errorf("failed to queue push backfill: %w", res.Error) } part, err = m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(msgID)) if err != nil { diff --git a/pkg/messagix/pushcrypto/native.go b/pkg/messagix/pushcrypto/native.go index c6a35aff..33008dd6 100644 --- a/pkg/messagix/pushcrypto/native.go +++ b/pkg/messagix/pushcrypto/native.go @@ -5,6 +5,8 @@ import ( "crypto/rand" "strconv" "time" + + "go.mau.fi/util/random" ) type NativePushKeys struct { @@ -19,10 +21,8 @@ func NewNativePushKeys(userID int64) (*NativePushKeys, error) { if err != nil { return nil, err } - key := make([]byte, 32) - rand.Read(key) return &NativePushKeys{ - AESKey: key, + AESKey: random.Bytes(32), AESKeyID: userID<<8 | 1, HPKEPrivateKey: private.Bytes(), HPKEKeyID: strconv.FormatInt(time.Now().UnixMilli(), 10), From 6372794b0c809a1c4791298c4404dab5861f79c6 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 15 Sep 2026 12:16:26 -0400 Subject: [PATCH 10/12] login: replace native sessions on re-login --- pkg/connector/client.go | 3 + pkg/connector/login.go | 85 ++++++++----------------- pkg/messagix/bloks/selenium_platform.go | 3 + 3 files changed, 32 insertions(+), 59 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index a0c4d30b..333482e3 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -85,6 +85,9 @@ func (m *MetaConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserL if err := validateNativeSession(loginMetadata.NativeSession); err != nil { return err } + if login.Client != nil { + login.Client.Disconnect() + } c := &MetaClient{ Main: m, LoginMeta: loginMetadata, diff --git a/pkg/connector/login.go b/pkg/connector/login.go index ff2e0757..a76d75a6 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -12,7 +12,6 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog" - waTypes "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/status" @@ -124,11 +123,28 @@ func (m *MetaConnector) CreateUserLoginFromCredentials(ctx context.Context, user transferred.Cookies.UpdateValues(values) } log := zerolog.Ctx(ctx).With().Str("component", "messagix").Logger() + if session := transferred.NativeSession; session != nil { + authClient, err := getMessagixClient(log, m, &cookies.Cookies{Platform: types.MessengerLiteAndroid}, m.Config.ProxyMessengerLite) + if err != nil { + return err + } + session.DeviceID = uuid.New() + authClient.MessengerLite.SetNativeSession(session) + newCookies, err := authClient.MessengerLite.ExchangeTransientToken(ctx, session.AccessToken) + if err != nil { + return fmt.Errorf("failed to create transferred native session: %w", err) + } else if newCookies.GetUserID() != transferred.Cookies.GetUserID() { + return fmt.Errorf("exchanged native session account does not match login") + } + newCookies.Platform = transferred.Platform + transferred.Cookies = newCookies + transferred.NativeSession = authClient.MessengerLite.GetNativeSession() + } client, err := getMessagixClient(log, m, transferred.Cookies, m.Config.ProxyOther) if err != nil { return err } - step, err := loginWithCookies(ctx, log, client, user, m, transferred.Cookies, transferred.NativeSession, true) + step, err := loginWithCookies(ctx, log, client, user, m, transferred.Cookies, transferred.NativeSession) if err != nil { return err } else if step.Type != bridgev2.LoginStepTypeComplete { @@ -237,7 +253,6 @@ func loginWithCookies( conn *MetaConnector, c *cookies.Cookies, nativeSession *types.NativeSession, - transfer bool, ) (*bridgev2.LoginStep, error) { if missing := c.GetMissingCookieNames(); len(missing) > 0 { return nil, loginerrors.MissingCookies.AppendMessage(": %v", missing) @@ -274,66 +289,18 @@ func loginWithCookies( if req, ok := ctx.Value("fi.mau.provision.request").(*http.Request); ok { loginUA = req.Header.Get("User-Agent") } - metadata := &metaid.UserLoginMetadata{} - existing, err := conn.Bridge.GetExistingUserLoginByID(ctx, loginID) - if err != nil { - return nil, fmt.Errorf("failed to load existing login: %w", err) - } else if existing != nil && existing.UserMXID != bridgeUser.MXID { - return nil, fmt.Errorf("that account is already logged in by another user") - } else if existing != nil { - *metadata = *existing.Metadata.(*metaid.UserLoginMetadata) - } - if nativeSession != nil { - deviceID := nativeSession.DeviceID - if transfer { - deviceID = uuid.New() - } - if metadata.WADeviceID != 0 { - device, err := conn.DeviceStore.GetDevice(ctx, waTypes.JID{User: string(loginID), Device: metadata.WADeviceID, Server: waTypes.MessengerServer}) - if err != nil { - return nil, fmt.Errorf("failed to load existing encrypted device: %w", err) - } else if device != nil { - if device.FacebookUUID == uuid.Nil { - return nil, fmt.Errorf("existing encrypted device has no native identity") - } - deviceID = device.FacebookUUID - } else { - metadata.WADeviceID = 0 - } - } - if deviceID != nativeSession.DeviceID { - authClient, err := getMessagixClient(log, conn, &cookies.Cookies{Platform: types.MessengerLiteAndroid}, conn.Config.ProxyMessengerLite) - if err != nil { - return nil, err - } - updated := *nativeSession - updated.DeviceID = deviceID - authClient.MessengerLite.SetNativeSession(&updated) - newCookies, err := authClient.MessengerLite.ExchangeTransientToken(ctx, nativeSession.AccessToken) - if err != nil { - return nil, fmt.Errorf("failed to prepare native session for encrypted device: %w", err) - } else if newCookies.GetUserID() != id { - return nil, fmt.Errorf("exchanged native session account does not match login") - } - nativeSession = authClient.MessengerLite.GetNativeSession() - } - } - metadata.Platform = c.Platform - metadata.Cookies = c - metadata.LoginUA = loginUA - metadata.NativeSession = nativeSession - client.MessengerLite.SetNativeSession(nativeSession) - if existing != nil && existing.Client != nil { - existing.Client.Disconnect() - } - ul, err := bridgeUser.NewLogin(ctx, &database.UserLogin{ ID: loginID, RemoteName: user.GetName(), RemoteProfile: status.RemoteProfile{ Name: user.GetName(), }, - Metadata: metadata, + Metadata: &metaid.UserLoginMetadata{ + Platform: c.Platform, + Cookies: c, + LoginUA: loginUA, + NativeSession: nativeSession, + }, }, nil) if err != nil { return nil, fmt.Errorf("failed to save new login: %w", err) @@ -378,7 +345,7 @@ func (m *MetaCookieLogin) SubmitCookies(ctx context.Context, strCookies map[stri if err != nil { return nil, err } - return loginWithCookies(ctx, log, client, m.User, m.Main, c, nil, false) + return loginWithCookies(ctx, log, client, m.User, m.Main, c, nil) } type MetaNativeLogin struct { @@ -463,7 +430,7 @@ func (m *MetaNativeLogin) proceed(ctx context.Context, userInput map[string]stri newClient.GetCookies().UpdateValues(newCookies.GetAll()) - step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies, m.SavedClient.MessengerLite.GetNativeSession(), false) + step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies, m.SavedClient.MessengerLite.GetNativeSession()) if err != nil { return nil, err } diff --git a/pkg/messagix/bloks/selenium_platform.go b/pkg/messagix/bloks/selenium_platform.go index cf0e2c76..3f7ddfdb 100644 --- a/pkg/messagix/bloks/selenium_platform.go +++ b/pkg/messagix/bloks/selenium_platform.go @@ -191,6 +191,9 @@ func (profile browserPlatformProfile) fallbackCodeInput( if profile.isInstagram && input == nil { return page.FindDescendant(FilterByComponent("bk.components.TextInput")) } + if input == nil { + return page.FindDescendant(FilterByAttribute("bk.components.TextInput", "type", "number")) + } return input } From f66313ccfb406f23999285b98fc0d3889e5c1961 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 15 Sep 2026 19:50:13 +0300 Subject: [PATCH 11/12] Extract build number to user agent package --- pkg/messagix/nativepush.go | 2 +- pkg/messagix/useragent/useragent.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/messagix/nativepush.go b/pkg/messagix/nativepush.go index 19e6cd16..cc9f36bd 100644 --- a/pkg/messagix/nativepush.go +++ b/pkg/messagix/nativepush.go @@ -70,7 +70,7 @@ func (fb *FacebookMethods) RegisterNativePushNotifications(ctx context.Context, }, "typed_os_settings": map[string]int{"notificationEnabled": 1}, "extra_data": map[string]int{ - "android_build": 345613452, + "android_build": useragent.MessengerLiteAndroidBuild, "android_setting_mask": 240, "orca_muted_until_ms": 0, "sys_notif": 1, diff --git a/pkg/messagix/useragent/useragent.go b/pkg/messagix/useragent/useragent.go index 5a8f8fcd..d4c336c6 100644 --- a/pkg/messagix/useragent/useragent.go +++ b/pkg/messagix/useragent/useragent.go @@ -25,6 +25,7 @@ const MessengerLiteIOSUserAgent = "LightSpeed [FBAN/MessengerLiteForiOS;FBAV/571 const MessengerLiteAndroidAppID = "256002347743983" const MessengerLiteAndroidAccessToken = MessengerLiteAndroidAppID + "|374e60f8b9bb6b8cbb30f78030438895" const MessengerLiteAndroidUserAgent = "Dalvik/2.1.0 (Linux; U; Android 16; Pixel 7 Build/BP2A.250805.005) [FBAN/Orca-Android;FBAV/569.0.0.44.91;FBPN/com.facebook.orca;FBLC/en_US;FBBV/1012430703;FBCR/Tello;FBMF/Google;FBBD/google;FBDV/Pixel 7;FBSV/16;FBCA/arm64-v8a:null;FBDM/{density=2.625,width=1080,height=2400};FB_FW/1;]" +const MessengerLiteAndroidBuild = 345613452 // FIXME the user agent above and this build number don't match const IGDUserAgent = "WMI Web" From 74d9e89658ff77032c59928fef5eda30441ddc94 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 15 Sep 2026 13:15:47 -0400 Subject: [PATCH 12/12] push: log missing messages after backfill --- pkg/connector/push.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/connector/push.go b/pkg/connector/push.go index fb43289f..56bda959 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -197,7 +197,9 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp Stringer("event_id", part.MXID). Msg("Confirmed push message was bridged after backfill") } else { - return fmt.Errorf("push message %s wasn't bridged after backfill", msgID) + log.Warn(). + Str("message_id", msgID). + Msg("Push message still wasn't bridged after backfill") } return nil }