diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 146b93a..c9fef73 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,7 +30,6 @@ jobs: run: | go install golang.org/x/tools/cmd/goimports@latest go install honnef.co/go/tools/cmd/staticcheck@latest - go install github.com/beeper/pre-commit-go/bridgelint@latest export PATH="$HOME/go/bin:$PATH" - name: Run pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c96b06..1bfb536 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,12 +21,9 @@ repos: - id: go-mod-tidy - repo: https://github.com/beeper/pre-commit-go - rev: v0.5.0 + rev: v0.4.2 hooks: - id: zerolog-ban-msgf - id: zerolog-use-stringer - id: prevent-literal-http-methods - id: zerolog-ban-global-log - - id: bridgelint - language: system - verbose: true diff --git a/go.mod b/go.mod index f940c1d..c58cd7c 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( golang.org/x/sync v0.24.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 + maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae ) require ( diff --git a/go.sum b/go.sum index fb4e878..df5df11 100644 --- a/go.sum +++ b/go.sum @@ -103,5 +103,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc= -maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae h1:4SY5+nh2B968L5NmMGmF0WPRbV9D1yFLP8U+JASuHXg= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index 8f006e0..629039d 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -142,16 +142,8 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()). Msg("Downloading history sync") blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) - if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) { - log.Err(err).Msg("History sync blob is no longer available, dropping notification") - if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil { - log.Err(err).Msg("Failed to delete queued history sync notification") - errored = true - } - return - } else if err != nil { + if err != nil { log.Err(err).Msg("Failed to download history sync") - errored = true return } if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND { @@ -179,7 +171,6 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, }) if err != nil { log.Err(err).Msg("Failed to store history sync notification data") - errored = true } else { resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP || blob.GetSyncType() == waHistorySync.HistorySync_RECENT || @@ -255,17 +246,10 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else if jid.Server == types.BroadcastServer { log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync") continue - } else if jid == types.PSAJID { - // New messages in the official updates chat aren't bridged because they use a different path, - // so drop history as well. This should be removed if the PSA chat gets properly bridged in the future. - log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync") - continue } else { totalMessageCount += len(conv.GetMessages()) } - if jid == types.MetaAIJID { - jid = types.NewMetaAIJID - } else if jid.Server == types.DefaultUserServer { + if jid.Server == types.DefaultUserServer { lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) if err != nil { log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") @@ -298,9 +282,6 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("msg_index", i). Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). - Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()). - Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())). - Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()). Msg("Dropping historical message due to parse error") log.Trace(). Any("web_message_info", rawMsg.GetMessage()). @@ -605,36 +586,16 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } } } - resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore) + resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true) if err != nil { return nil, fmt.Errorf("failed to convert messages: %w", err) } - // TODO might be better to always set true here and let GetBetween return an empty result to mark as false? resp.HasMore = hasMore resp.Forward = params.Forward resp.MarkRead = markRead - if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) { - resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount))) - } return resp, nil } -func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time { - if unreadCount <= 0 { - return time.Time{} - } - for i := len(messages) - 1; i >= 0; i-- { - if messages[i].Sender.IsFromMe { - continue - } - unreadCount-- - if unreadCount == 0 { - return messages[i].Timestamp.Add(-time.Nanosecond) - } - } - return time.Time{} -} - func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) { var err error var rows int64 @@ -667,7 +628,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( portalJID types.JID, messages []*waWeb.WebMessageInfo, explodeOnError bool, - deleteEverything bool, ) (*bridgev2.FetchMessagesResponse, error) { cli := wa.Client if cli == nil { @@ -704,9 +664,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) } - if evt.Info.IsFromMe { - wa.addPreviousOwnLID(ctx, evt.Info.Sender) - } isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension converted, mediaReq := wa.convertHistorySyncMessage( ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, @@ -726,11 +683,9 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( Messages: convertedMessages, Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), CompleteCallback: func() { - if deleteEverything { - wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS) - } else { - wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) - } + // TODO this only deletes after backfilling. If there's no need for backfill after a relogin, + // the messages will be stuck in the database + wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) if len(mediaRequests) > 0 { go func(ctx context.Context) { for _, req := range mediaRequests { @@ -811,12 +766,10 @@ type onDemandHistorySyncRequest struct { SentAt time.Time } -const onDemandHistorySyncRequestTimeout = 1 * time.Hour - func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool { wa.onDemandHistorySyncLock.Lock() defer wa.onDemandHistorySyncLock.Unlock() - if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout { + if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight { zerolog.Ctx(ctx).Debug(). Str("in_flight_request_msg_id", req.ID). Stringer("portal_jid", portalJID). @@ -850,9 +803,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") continue } - if portalJID == types.MetaAIJID { - portalJID = types.NewMetaAIJID - } else if portalJID.Server == types.DefaultUserServer { + if portalJID.Server == types.DefaultUserServer { lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID) if err != nil { zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync") @@ -893,7 +844,7 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w Int("message_count", len(messages)). Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()). Msg("Converting messages to bridge from on-demand history sync") - resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false) + resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false) if err != nil { return nil, err } diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 678c8d2..4181235 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -55,7 +55,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), pushNamesSynced: exsync.NewEvent(), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), - previousOwnLIDs: exsync.NewSetWithItems(login.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs), onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest), @@ -139,9 +138,6 @@ type WhatsAppClient struct { onDemandHistorySyncLock sync.Mutex onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest - - previousOwnLIDs *exsync.Set[types.JID] - previousOwnLIDsLock sync.Mutex } var ( @@ -210,26 +206,7 @@ func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool { return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) || - (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) || - wa.previousOwnLIDs.Has(jid.ToNonAD()) -} - -func (wa *WhatsAppClient) addPreviousOwnLID(ctx context.Context, jid types.JID) { - if jid.Server != types.HiddenUserServer || wa.IsOwnJID(jid) { - return - } - jid = jid.ToNonAD() - wa.previousOwnLIDsLock.Lock() - defer wa.previousOwnLIDsLock.Unlock() - if !wa.previousOwnLIDs.Add(jid) { - return - } - zerolog.Ctx(ctx).Info().Stringer("lid", jid).Msg("Found previous own LID in history sync") - wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs = wa.previousOwnLIDs.AsList() - err := wa.UserLogin.Save(ctx) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login after adding previous own LID") - } + (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) } func (wa *WhatsAppClient) GetLID() types.JID { diff --git a/pkg/connector/commands.go b/pkg/connector/commands.go index 120d05e..037d24e 100644 --- a/pkg/connector/commands.go +++ b/pkg/connector/commands.go @@ -148,7 +148,13 @@ func fnSync(ce *commands.Event) { case "appstate": names := appstate.AllPatchNames[:] if len(ce.Args) > 1 { - names = parseAppStateNames(ce, ce.Args[1:]) + names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) { + if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { + ce.Reply("Invalid app state name `%s`", name) + return "", false + } + return appstate.WAPatchName(name), true + }) } for _, name := range names { err := wa.Client.FetchAppState(ce.Ctx, name, true, false) @@ -168,50 +174,6 @@ func fnSync(ce *commands.Event) { } } -func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName { - return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) { - if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) { - ce.Reply("Invalid app state name `%s`", name) - return "", false - } - return appstate.WAPatchName(name), true - }) -} - -var cmdResetAppState = &commands.FullHandler{ - Func: fnResetAppState, - Name: "reset-appstate", - Help: commands.HelpMeta{ - Section: commands.HelpSectionAdmin, - Description: "Ask your phone to reset broken app state. This logs out all linked devices.", - Args: "", - }, - RequiresLogin: true, -} - -func fnResetAppState(ce *commands.Event) { - login := ce.User.GetDefaultLogin() - if login == nil { - ce.Reply("Login not found") - return - } - if len(ce.Args) == 0 { - ce.Reply("Usage: `$cmdprefix reset-appstate `\n\nThis logs out all linked devices, including the bridge. You'll have to log in again afterwards.") - return - } - names := parseAppStateNames(ce, ce.Args) - if len(names) != len(ce.Args) { - return - } - wa := login.Client.(*WhatsAppClient) - _, err := wa.Client.SendPeerMessage(ce.Ctx, whatsmeow.BuildFatalAppStateExceptionNotification(names...)) - if err != nil { - ce.Reply("Failed to send app state reset request: %v", err) - return - } - ce.Reply("Sent app state reset request. Your phone should log out all linked devices shortly, after which you can log in again.") -} - var cmdInviteLink = &commands.FullHandler{ Func: fnInviteLink, Name: "invite-link", diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 80b9d95..d7261d3 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -110,7 +110,7 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger()) wa.MsgConv.DB = wa.DB wa.Bridge.Commands.(*commands.Processor).AddHandlers( - cmdAccept, cmdSync, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin, + cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, ) wa.mediaEditCache = make(MediaEditCache) wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() @@ -164,19 +164,11 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to migrate to LID DMs: %w", err) } - err = wa.migrateMetaAIPortals(ctx) - if err != nil { - return fmt.Errorf("failed to migrate Meta AI portals: %w", err) - } go func() { err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx) if err != nil { wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts") } - err = wa.removeRejoinedPNGhosts(wa.Bridge.BackgroundCtx) - if err != nil { - wa.Bridge.Log.Err(err).Msg("Failed to remove rejoined PN ghosts") - } }() return nil } @@ -243,16 +235,7 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e } func (wa *WhatsAppConnector) ResetHTTPTransport() { - for _, login := range wa.Bridge.GetAllCachedUserLogins() { - client := login.Client.(*WhatsAppClient).Client - if client == nil { - continue - } - err := wa.updateProxy(login.Log.WithContext(wa.Bridge.BackgroundCtx), client, false) - if err != nil { - login.Log.Err(err).Msg("Failed to recreate HTTP clients") - } - } + // No-op for now, whatsmeow doesn't use the shared transport config yet } func (wa *WhatsAppConnector) ResetNetworkConnections() { diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 03ef398..b86f7dc 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -164,19 +164,7 @@ func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, par return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) } sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash) - var httpErr whatsmeow.DownloadHTTPError - if errors.As(err, &httpErr) { - switch httpErr.StatusCode { - case http.StatusNotFound, http.StatusGone: - return nil, mautrix.MNotFound.WithMessage("Sticker pack not found") - case http.StatusUnauthorized, http.StatusForbidden: - return nil, mautrix.MForbidden.WithMessage("WhatsApp denied access to the sticker pack") - case http.StatusTooManyRequests: - return nil, mautrix.MLimitExceeded.WithMessage("Rate limited by WhatsApp while fetching the sticker pack").WithCanRetry(true) - default: - return nil, mautrix.MUnknown.WithMessage("WhatsApp returned HTTP %d while fetching the sticker pack", httpErr.StatusCode).WithStatus(http.StatusBadGateway).WithCanRetry(true) - } - } else if err != nil { + if err != nil { return nil, err } else if sticker == nil { return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack") diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go index 38433d6..288b8f3 100644 --- a/pkg/connector/errors.go +++ b/pkg/connector/errors.go @@ -21,16 +21,11 @@ import ( "errors" "fmt" "net/http" - "strconv" - "strings" - "time" "go.mau.fi/whatsmeow" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/event" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" ) // wrapConversionError turns a failure to convert a Matrix event into a message status. Nothing has been @@ -78,6 +73,12 @@ func wrapWhatsAppError(err error) error { WithStatus(event.MessageStatusFail). WithIsCertain(true). WithSendNotice(true) + case errors.Is(err, whatsmeow.ErrRecipientNotOnWhatsApp): + return bridgev2.WrapErrorInStatus(err). + WithMessage("this phone number isn't on WhatsApp"). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) case errors.Is(err, whatsmeow.ErrServerReturnedError), errors.As(err, &iqErr): return bridgev2.WrapErrorInStatus(err). WithMessage("WhatsApp returned an error"). @@ -90,50 +91,6 @@ func wrapWhatsAppError(err error) error { } } -// wrapSendError turns an error from sending a message to WhatsApp into a message status. -func (wa *WhatsAppClient) wrapSendError(err error) error { - switch sendErrorCode(err) { - case 403: - return bridgev2.WrapErrorInStatus(err). - WithMessage("you don't have permission to do that on WhatsApp"). - WithErrorReason(event.MessageStatusNoPermission). - WithStatus(event.MessageStatusFail). - WithIsCertain(true). - WithSendNotice(true) - case 463: - msg := "WhatsApp has temporarily restricted your account from messaging new contacts" - if until := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil; until.After(time.Now()) { - msg = fmt.Sprintf("WhatsApp has restricted your account from messaging new contacts until %s", until.UTC().Format("Jan 2, 2006 at 3:04 PM MST")) - } - return bridgev2.WrapErrorInStatus(err). - WithMessage(msg). - WithErrorReason(event.MessageStatusNoPermission). - WithStatus(event.MessageStatusFail). - WithIsCertain(true). - WithSendNotice(true) - case 421, 479: - return bridgev2.WrapErrorInStatus(err). - WithMessage("WhatsApp rejected the message"). - WithErrorReason(event.MessageStatusNetworkError). - WithStatus(event.MessageStatusFail). - WithIsCertain(true). - WithSendNotice(true) - default: - return wrapWhatsAppError(err) - } -} - -// sendErrorCode returns the code WhatsApp rejected a message with, which whatsmeow only includes in the -// error text. -func sendErrorCode(err error) int { - if !errors.Is(err, whatsmeow.ErrServerReturnedError) { - return 0 - } - _, codeStr, _ := strings.Cut(err.Error(), whatsmeow.ErrServerReturnedError.Error()+" ") - code, _ := strconv.Atoi(codeStr) - return code -} - // wrapWhatsAppRespError turns an error from a whatsmeow request made during login or provisioning into // an HTTP error, keeping the original error in the chain. Clients retry 429 and 502-504 automatically, // which isn't wanted for provisioning requests, so those statuses aren't used. diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 7f906e8..481d0d7 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -474,9 +474,7 @@ func (evt *WAMediaRetry) getRealSender() types.JID { func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { realSender := pickLID(evt.getRealSender(), evt.senderLID) - sender := evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) - sender.ForceEditOrigSender = true - return sender + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 8419850..fec40c3 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -107,8 +107,6 @@ func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2 var ErrBroadcastSendDisabled = bridgev2.WrapErrorInStatus(errors.New("sending status messages is disabled")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrBroadcastReactionUnsupported = bridgev2.WrapErrorInStatus(errors.New("reacting to status messages is not currently supported")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) var ErrNotCommunityAdmin = bridgev2.WrapErrorInStatus(errors.New("only community admins can send announcements")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) -var ErrEmptyGroupName = bridgev2.WrapErrorInStatus(errors.New("WhatsApp group names can't be empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported).WithStatus(event.MessageStatusFail) -var ErrAddBlockedByPrivacy = bridgev2.WrapErrorInStatus(errors.New("this user's privacy settings don't allow adding them to groups")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusNoPermission).WithStatus(event.MessageStatusFail) func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage, waMsg *waE2E.Message, req *whatsmeow.SendRequestExtra) (*bridgev2.MatrixMessageResponse, error) { if req == nil { @@ -141,7 +139,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg if errors.Is(err, whatsmeow.ErrNoPermissionInGroup) { return nil, ErrNotCommunityAdmin } - return nil, wa.wrapSendError(err) + return nil, wrapWhatsAppError(err) } var pickedMessageID networkid.MessageID if resp.Sender == wa.GetLID() { @@ -220,7 +218,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev Metadata: &waid.ReactionMetadata{ SenderDeviceID: wa.JID.Device, }, - }, wa.wrapSendError(err) + }, wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { @@ -250,7 +248,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") - return wa.wrapSendError(err) + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { @@ -290,7 +288,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M ID: editID, }) log.Trace().Any("response", resp).Msg("WhatsApp edit response") - return wa.wrapSendError(err) + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { @@ -315,11 +313,11 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra) log.Trace().Any("response", resp).Msg("WhatsApp delete response") - return wa.wrapSendError(err) + return wrapWhatsAppError(err) } func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { - if receipt.Receipt.IsThread() || !receipt.ReadUpTo.After(receipt.LastRead) { + if !receipt.ReadUpTo.After(receipt.LastRead) { return nil } if receipt.LastRead.IsZero() { @@ -485,8 +483,6 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg return nil, wrapWhatsAppError(err) } else if len(resp) == 0 { return nil, fmt.Errorf("no response for participant change") - } else if resp[0].Error == 403 && action == whatsmeow.ParticipantChangeAdd { - return nil, ErrAddBlockedByPrivacy } else if resp[0].Error != 0 { return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error) } @@ -505,8 +501,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev if msg.Portal.RoomType == database.RoomTypeDM { return false, fmt.Errorf("cannot set room name for DM") - } else if msg.Content.Name == "" { - return false, ErrEmptyGroupName } defer wa.mcTrack(msg, time.Now(), &retErr) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index e6174b9..e7a9b65 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -237,11 +237,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { }) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream error: %s", message)) case *events.StreamReplaced: - wa.UserLogin.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateUnknownError, - Error: WAStreamReplaced, - BlockAutoReconnect: true, - }) + wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced}) wa.notifyOfflineSyncWaiter(fmt.Errorf("stream replaced")) case *events.KeepAliveTimeout: wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAKeepaliveTimeout}) @@ -257,11 +253,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.notifyOfflineSyncWaiter(fmt.Errorf("connection failure: %s (%s)", evt.Reason, evt.Message)) case *events.ClientOutdated: wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.") - wa.UserLogin.BridgeState.Send(status.BridgeState{ - StateEvent: status.StateUnknownError, - Error: WAClientOutdated, - BlockAutoReconnect: true, - }) + wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated}) wa.notifyOfflineSyncWaiter(fmt.Errorf("client outdated")) case *events.TemporaryBan: wa.UserLogin.BridgeState.Send(status.BridgeState{ @@ -374,14 +366,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { parentKey := messageAssoc.GetParentMessageKey() protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey) - if protocolMsg.GetEditedMessage() == nil { - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("parent_id", parentKey.GetID()). - Stringer("assoc_type", assocType). - Msg("Ignoring HD replacement message without replacement content") - return - } dontRenderEdited = shouldHideEdit wa.UserLogin.Log.Debug(). Str("message_id", evt.Info.ID). @@ -530,10 +514,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei } else if evt.Chat.Server == types.NewsletterServer { messageSender = evt.Chat } - var messageSenderAlt types.JID - if evt.MessageSender.IsEmpty() && evt.Chat.Server == types.BotServer { - messageSenderAlt = wa.JID - } var chatAlt types.JID if evt.Chat.Server == types.DefaultUserServer { chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) @@ -547,9 +527,6 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei if !chatAlt.IsEmpty() { targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id)) } - if !messageSenderAlt.IsEmpty() { - targets = append(targets, waid.MakeMessageID(evt.Chat, messageSenderAlt, id)) - } } senderLID := evt.Sender if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { @@ -978,9 +955,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt defer wa.appStateRecoveryLock.Unlock() meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata) if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists { - if !evt.Recovery { - delete(wa.appStateFullSyncAttempted, evt.Name) - } + delete(wa.appStateFullSyncAttempted, evt.Name) delete(meta.AppStateRecoveryAttempted, evt.Name) err := wa.UserLogin.Save(ctx) if err != nil { @@ -1018,7 +993,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev return } if !evt.FullSync { - if !lastFullSync.IsZero() && time.Since(lastFullSync) < 48*time.Hour { + if !lastFullSync.IsZero() { log.Debug(). Err(evt.Error). Time("last_full_sync_attempt", lastFullSync). diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go index 4295be6..ec1c700 100644 --- a/pkg/connector/lidmigrate.go +++ b/pkg/connector/lidmigrate.go @@ -18,7 +18,6 @@ package connector import ( "context" - "errors" "fmt" "time" @@ -26,11 +25,9 @@ import ( "go.mau.fi/util/dbutil" "go.mau.fi/whatsmeow/types" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -50,8 +47,6 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne if err != nil { return nil, err } - } else if parsed.Chat == types.NewMetaAIJID { - altChat = types.MetaAIJID } if !altSender.IsEmpty() { altSenderID := *parsed @@ -63,8 +58,6 @@ func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg ne altChatID.Chat = altChat if altSender.Server == types.DefaultUserServer { altChatID.Sender = altSender - } else if parsed.Sender == types.NewMetaAIJID { - altChatID.Sender = types.MetaAIJID } alts = append(alts, altChatID.String()) } @@ -218,30 +211,6 @@ func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { return nil } -func (wa *WhatsAppConnector) migrateMetaAIPortals(ctx context.Context) error { - if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_meta_ai_portals_migrated") == "true" { - return nil - } - log := zerolog.Ctx(ctx).With().Str("action", "migrate meta ai portals").Logger() - const findMetaAIPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND id=$2" - portalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findMetaAIPortals, wa.Bridge.ID, waid.MakePortalID(types.MetaAIJID))).AsList() - if err != nil { - return fmt.Errorf("failed to get Meta AI portals: %w", err) - } - for _, key := range portalKeys { - res, err := wa.reIDPhoneDMToLID(ctx, types.MetaAIJID, types.NewMetaAIJID, key.Receiver) - if err != nil { - return fmt.Errorf("failed to re-ID Meta AI portal for %s: %w", key.Receiver, err) - } - log.Info(). - Str("receiver", string(key.Receiver)). - Stringer("result", res). - Msg("Re-ID'd Meta AI phone number portal") - } - wa.Bridge.DB.KV.Set(ctx, "whatsapp_meta_ai_portals_migrated", "true") - return nil -} - func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" { return nil @@ -298,84 +267,3 @@ func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true") return nil } - -func (wa *WhatsAppConnector) removeRejoinedPNGhosts(ctx context.Context) error { - if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2") == "true" { - return nil - } - stateGetter, ok := wa.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState) - if !ok { - return nil - } - log := zerolog.Ctx(ctx).With().Str("action", "remove rejoined pn ghosts").Logger() - const findLIDPortals = "SELECT id, mxid FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@lid' AND mxid IS NOT NULL" - lidPortals, err := scanLIDPortal.NewRowIter(wa.Bridge.DB.Query(ctx, findLIDPortals, wa.Bridge.ID)).AsList() - if err != nil { - return fmt.Errorf("failed to get LID portals: %w", err) - } - var removed, failed int - for _, portal := range lidPortals { - lid, err := waid.ParsePortalID(portal.ID) - if err != nil || lid.Server != types.HiddenUserServer { - continue - } - pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid) - if err != nil { - return fmt.Errorf("failed to get PN for LID portal %s: %w", portal.ID, err) - } else if pn.IsEmpty() { - continue - } - pnUserID := waid.MakeUserID(pn) - memberEvt, err := stateGetter.GetStateEvent(ctx, portal.MXID, event.StateMember, wa.Bridge.Matrix.FormatGhostMXID(pnUserID).String()) - if errors.Is(err, mautrix.MNotFound) || errors.Is(err, mautrix.MForbidden) { - continue - } else if err != nil { - log.Warn().Err(err).Stringer("room_id", portal.MXID).Msg("Failed to get PN ghost membership") - failed++ - continue - } else if memberEvt == nil || memberEvt.Content.AsMember().Membership != event.MembershipJoin { - continue - } - pnGhost, err := wa.Bridge.GetGhostByID(ctx, pnUserID) - if err != nil { - return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err) - } - _, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{ - Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"}, - Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, - }, time.Time{}) - if err != nil { - log.Err(err).Stringer("room_id", portal.MXID).Stringer("pn", pn).Msg("Failed to remove PN ghost from LID DM") - if !errors.Is(err, mautrix.MForbidden) && !errors.Is(err, mautrix.MNotFound) { - failed++ - } - continue - } - log.Debug(). - Stringer("room_id", portal.MXID). - Stringer("pn", pn). - Stringer("join_sender", memberEvt.Sender). - Time("join_ts", time.UnixMilli(memberEvt.Timestamp)). - Msg("Removed PN ghost from LID DM") - removed++ - } - log.Info(). - Int("removed_ghosts", removed). - Int("failed", failed). - Int("total_lid_portals", len(lidPortals)). - Msg("Finished removing rejoined PN ghosts from LID DMs") - if failed == 0 && ctx.Err() == nil { - wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dm_pn_ghosts_removed_v2", "true") - } - return nil -} - -type lidPortal struct { - ID networkid.PortalID - MXID id.RoomID -} - -var scanLIDPortal = dbutil.ConvertRowFn[lidPortal](func(row dbutil.Scannable) (portal lidPortal, err error) { - err = row.Scan(&portal.ID, &portal.MXID) - return -}) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index ae57ac1..387d780 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -137,7 +137,6 @@ var ( _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) - _ bridgev2.LoginProcessWithParams = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) @@ -145,14 +144,6 @@ const LoginConnectWait = 30 * time.Second const LoginPairPhoneWait = 30 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { - return wl.StartWithParams(ctx, bridgev2.LoginStartParams{}) -} - -func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { - return wl.StartWithParams(ctx, bridgev2.LoginStartParams{Override: old}) -} - -func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginStartParams) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) device := wl.Main.DeviceStore.NewDevice() wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log)) @@ -162,20 +153,8 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta if err := wl.Main.updateProxy(ctx, wl.Client, true); err != nil { return nil, err } - if params.WebSocketHTTP != nil { - wl.Client.SetPreLoginHTTPClient(&http.Client{Transport: params.WebSocketHTTP}) - } if wl.PhoneCode { - if params.Override != nil { - phoneNumber := fmt.Sprintf("+%s", params.Override.ID) - wl.Log.Debug(). - Str("phone_number", phoneNumber). - Msg("Auto-submitting phone number for relogin") - return wl.SubmitUserInput(ctx, map[string]string{ - "phone_number": phoneNumber, - }) - } return &bridgev2.LoginStep{ Type: bridgev2.LoginStepTypeUserInput, StepID: LoginStepIDPhoneNumber, @@ -205,6 +184,20 @@ func (wl *WALogin) StartWithParams(ctx context.Context, params bridgev2.LoginSta return makeQRStep(wl.QRs[0]), nil } +func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) { + step, err := wl.Start(ctx) + if err == nil && step != nil && old != nil && step.StepID == LoginStepIDPhoneNumber { + phoneNumber := fmt.Sprintf("+%s", old.ID) + wl.Log.Debug(). + Str("phone_number", phoneNumber). + Msg("Auto-submitting phone number for relogin") + return wl.SubmitUserInput(ctx, map[string]string{ + "phone_number": phoneNumber, + }) + } + return step, err +} + func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { err := wl.Client.Connect() if err != nil { @@ -288,7 +281,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1) } wl.ADVRotate.Set() - return case *events.QRScannedWithoutMultidevice: wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.LoginError = ErrLoginMultideviceNotEnabled @@ -324,7 +316,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.LoginError = ErrUnexpectedDisconnect case *events.ManualLoginReconnect: wl.Received515.Set() - return default: wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event") return @@ -373,14 +364,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { case <-wl.PasskeyRequest.GetChan(): return wl.makePasskeyStep() case <-wl.ADVRotate.GetChan(): + wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately") wl.ADVRotate.Clear() - if currentIndex < 0 || currentIndex >= len(wl.QRs) { - wl.Log.Debug().Msg("ADV secret was rotated, but no QRs are left to return") - wl.Cancel() - return nil, ErrLoginTimeout - } - wl.Log.Debug().Msg("ADV secret was rotated, returning current QR immediately") - return makeQRStep(wl.QRs[currentIndex]), nil + return makeQRStep(wl.QRs[nextIndex]), nil case <-wl.LoginComplete.GetChan(): // continue } diff --git a/pkg/connector/muse.go b/pkg/connector/muse.go index 4a589ce..ee6b04d 100644 --- a/pkg/connector/muse.go +++ b/pkg/connector/muse.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "runtime/debug" "time" @@ -56,9 +57,9 @@ func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) { success := true if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil { log.Err(err).Msg("Failed to resync WASA root secrets") - // Mark the sync as completed on errors too, because some get permanently stuck (mismatching LTHash, - // expired patch blobs), and a failed full sync already requests app state recovery from the phone. - if ctx.Err() != nil { + // Mark the sync as completed on mismatching LTHash errors, because those can get permanently stuck + // (retrying won't help). In case it's not stuck, a single call should be enough to request a resync. + if !errors.Is(err, appstate.ErrMismatchingLTHash) || ctx.Err() != nil { return } success = false diff --git a/pkg/connector/proxy.go b/pkg/connector/proxy.go index 8bd6d65..fe1d7ff 100644 --- a/pkg/connector/proxy.go +++ b/pkg/connector/proxy.go @@ -54,38 +54,23 @@ func (wa *WhatsAppConnector) getProxy(ctx context.Context, reason string) (strin } func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow.Client, isLogin bool) error { - var proxy string - if !wa.Config.ProxyOnlyLogin || isLogin { - reason := "connect" - if isLogin { - reason = "login" - } - var err error - proxy, err = wa.getProxy(ctx, reason) - if err != nil { - return fmt.Errorf("failed to get proxy address: %w", err) - } + if wa.Config.ProxyOnlyLogin && !isLogin { + return nil } - settings := wa.Bridge.GetHTTPClientSettings().WithGlobalTimeout(0) - if settings.HTTPProxy == nil { - settings.HTTPProxy = http.ProxyFromEnvironment + reason := "connect" + if isLogin { + reason = "login" } - proxySettings := settings - if proxy != "" { - var err error - proxySettings, err = settings.WithProxy(proxy) - if err != nil { - return fmt.Errorf("failed to set proxy address: %w", err) - } - } - client.SetPreLoginHTTPClient(proxySettings.Compile()) - if wa.Config.ProxyOnlyLogin { - proxySettings = settings - } - client.SetWebsocketHTTPClient(proxySettings.Compile()) - client.SetMediaHTTPClient(proxySettings.WithResponseHeaderTimeout(0).Compile()) - if proxy != "" { - zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") + if proxy, err := wa.getProxy(ctx, reason); err != nil { + return fmt.Errorf("failed to get proxy address: %w", err) + } else if proxy == "" { + return nil + } else if err = client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{ + OnlyLogin: wa.Config.ProxyOnlyLogin, + NoMedia: wa.Config.ProxyOnlyLogin, + }); err != nil { + return fmt.Errorf("failed to set proxy address: %w", err) } + zerolog.Ctx(ctx).Debug().Msg("Enabled proxy") return nil } diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index 387971e..c78b0a8 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -29,15 +29,12 @@ var ResyncLoopInterval = 4 * time.Hour var ResyncJitterSeconds = 3600 func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { - jid := waid.ParseUserID(ghost.ID) - if jid.Server == types.BotServer { - return - } lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - if ghost.AvatarID != "" && lastSync.Add(ResyncMinInterval).After(time.Now()) { + if lastSync.Add(ResyncMinInterval).After(time.Now()) { return } wa.resyncQueueLock.Lock() + jid := waid.ParseUserID(ghost.ID) if _, exists := wa.resyncQueue[jid]; !exists { wa.resyncQueue[jid] = resyncQueueItem{ghost: ghost} nextResyncIn := time.Until(wa.nextResync).String() @@ -119,17 +116,14 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID var ghostJIDs []types.JID var ghosts []*bridgev2.Ghost var portals []*bridgev2.Portal - selectedGhosts := make(map[types.JID]*bridgev2.Ghost) for jid, item := range queue { var lastSync time.Time - unknownAvatar := false if item.ghost != nil { lastSync = item.ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - unknownAvatar = item.ghost.AvatarID == "" } else if item.portal != nil { lastSync = item.portal.Metadata.(*waid.PortalMetadata).LastSync.Time } - if !unknownAvatar && lastSync.Add(ResyncMinInterval).After(time.Now()) { + if lastSync.Add(ResyncMinInterval).After(time.Now()) { log.Debug(). Stringer("jid", jid). Time("last_sync", lastSync). @@ -137,23 +131,12 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID continue } if item.ghost != nil { - selectedGhosts[jid] = item.ghost + ghosts = append(ghosts, item.ghost) + ghostJIDs = append(ghostJIDs, jid) } else if item.portal != nil { portals = append(portals, item.portal) } } - for jid, ghost := range selectedGhosts { - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) - if err != nil { - log.Warn().Err(err).Stringer("jid", jid).Msg("Failed to get LID for background sync") - } else if _, lidSelected := selectedGhosts[lid]; lidSelected { - continue - } - } - ghosts = append(ghosts, ghost) - ghostJIDs = append(ghostJIDs, jid) - } for _, portal := range portals { wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ @@ -182,7 +165,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync") continue } - userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, ghost.AvatarID == "" || (info.PictureID != "" && string(ghost.AvatarID) != info.PictureID)) + userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) if err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") continue @@ -489,19 +472,16 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI } func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { - info := &bridgev2.UserInfo{ - Identifiers: ghost.Identifiers, - Name: &ghost.Name, + return &bridgev2.UserInfo{ + Identifiers: ghost.Identifiers, + Name: &ghost.Name, + Avatar: &bridgev2.Avatar{ + ID: ghost.AvatarID, + Remove: ghost.AvatarID == "" || ghost.AvatarMXC == "", + MXC: ghost.AvatarMXC, + Hash: ghost.AvatarHash, + }, IsBot: &ghost.IsBot, ExtraProfile: ghost.ExtraProfile, } - if ghost.AvatarID != "" && ghost.AvatarID != "unauthorized" { - info.Avatar = &bridgev2.Avatar{ - ID: ghost.AvatarID, - Remove: ghost.AvatarMXC == "", - MXC: ghost.AvatarMXC, - Hash: ghost.AvatarHash, - } - } - return info } diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index c4172d0..0f1fef9 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v11 (compatible with v3+): Latest revision +-- v0 -> v10 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql b/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql deleted file mode 100644 index 69e8b1e..0000000 --- a/pkg/connector/wadb/upgrades/11-rearm-leftover-history-sync-backfill.sql +++ /dev/null @@ -1,10 +0,0 @@ --- v11 (compatible with v3+): Re-arm backfill for chats with leftover history sync messages -UPDATE backfill_task -SET is_done=false, queue_done=false, dispatched_at=NULL, completed_at=NULL -WHERE (is_done OR queue_done) AND EXISTS ( - SELECT 1 - FROM whatsapp_history_sync_message msg - WHERE msg.bridge_id=backfill_task.bridge_id - AND msg.user_login_id=backfill_task.user_login_id - AND msg.chat_jid=backfill_task.portal_id -); diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index ba2adc2..192b8a6 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -27,15 +27,12 @@ import ( "image/color" "image/jpeg" "net/http" - "os" - "path/filepath" "slices" "strconv" "strings" "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmime" "go.mau.fi/util/ffmpeg" "go.mau.fi/util/ptr" "go.mau.fi/util/random" @@ -518,56 +515,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) { return webpBuffer.Bytes(), size, nil } -func convertUnsupportedVideoCodec(ctx context.Context, data []byte, mime string) ([]byte, string, error) { - if !ffmpeg.Supported() || !ffmpeg.ProbeSupported() { - return data, mime, nil - } - log := zerolog.Ctx(ctx) - tempdir, err := os.MkdirTemp("", "mautrix_whatsapp_video_*") - if err != nil { - log.Warn().Err(err).Msg("Failed to create temp dir for video, sending it as-is") - return data, mime, nil - } - defer os.RemoveAll(tempdir) - inputPath := filepath.Join(tempdir, "input"+exmime.ExtensionFromMimetype(mime)) - err = os.WriteFile(inputPath, data, 0600) - if err != nil { - log.Warn().Err(err).Msg("Failed to write video to temp file, sending it as-is") - return data, mime, nil - } - probe, err := ffmpeg.Probe(ctx, inputPath) - if err != nil { - log.Warn().Err(err).Msg("Failed to probe video, sending it as-is") - return data, mime, nil - } - videoSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { - return stream.CodecType == "video" && stream.Disposition.AttachedPic == 0 && stream.CodecName != "h264" - }) - audioSupported := !slices.ContainsFunc(probe.Streams, func(stream *ffmpeg.Stream) bool { - return stream.CodecType == "audio" && stream.CodecName != "aac" - }) - if videoSupported && audioSupported { - return data, mime, nil - } - videoArgs := []string{"-c:v", "copy"} - if !videoSupported { - videoArgs = []string{ - "-pix_fmt", "yuv420p", "-c:v", "h264", "-b:v", "2M", - "-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'", - } - } - outputPath := filepath.Join(tempdir, "output.mp4") - err = ffmpeg.ConvertPathWithDestination(ctx, inputPath, outputPath, nil, append(videoArgs, "-c:a", "aac", "-movflags", "+faststart"), false) - if err != nil { - return nil, mime, err - } - converted, err := os.ReadFile(outputPath) - if err != nil { - return nil, mime, err - } - return converted, "video/mp4", nil -} - func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) { if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" { return nil, nil @@ -662,10 +609,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( case event.MsgVideo: switch mime { case "video/mp4", "video/3gpp": - data, mime, err = convertUnsupportedVideoCodec(ctx, data, mime) - if err != nil { - return nil, nil, mime, fmt.Errorf("%w (%s to h264): %w", bridgev2.ErrMediaConvertFailed, mime, err) - } + // allowed case "video/webm", "video/quicktime": sourceFormat := "webm" if mime == "video/quicktime" { diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index db18f35..1ff2e02 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -17,7 +17,6 @@ package msgconv import ( - "cmp" "context" "fmt" "html" @@ -93,7 +92,7 @@ func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user types.JID portal := getPortal(ctx) login := mc.Bridge.GetCachedUserLoginByID(waid.MakeUserLoginID(pnJID)) if login != nil && (portal.Receiver == "" || portal.Receiver == login.ID) { - return login.UserMXID, cmp.Or(login.RemoteProfile.Name, ghost.Name), nil + return login.UserMXID, ghost.Name, nil } } return ghost.Intent.GetMXID(), ghost.Name, nil diff --git a/pkg/msgconv/wa-location.go b/pkg/msgconv/wa-location.go index 52f3f13..4a1e043 100644 --- a/pkg/msgconv/wa-location.go +++ b/pkg/msgconv/wa-location.go @@ -30,40 +30,6 @@ import ( "maunium.net/go/mautrix/event" ) -func formatCoordinates(lat, long float64) string { - latChar := 'N' - if lat < 0 { - latChar = 'S' - } - longChar := 'E' - if long < 0 { - longChar = 'W' - } - return fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(lat), latChar, math.Abs(long), longChar) -} - -func addLocationThumbnail(ctx context.Context, content *event.MessageEventContent, thumbnail []byte) { - if len(thumbnail) == 0 { - return - } - thumbnailMime := http.DetectContentType(thumbnail) - thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, thumbnail, "thumb.jpeg", thumbnailMime) - if err != nil { - return - } - cfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail)) - content.Info = &event.FileInfo{ - ThumbnailInfo: &event.FileInfo{ - Size: len(thumbnail), - Width: cfg.Width, - Height: cfg.Height, - MimeType: thumbnailMime, - }, - ThumbnailURL: thumbnailURL, - ThumbnailFile: thumbnailFile, - } -} - func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE2E.LocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { url := msg.GetURL() if len(url) == 0 { @@ -71,7 +37,15 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } name := msg.GetName() if len(name) == 0 { - name = formatCoordinates(msg.GetDegreesLatitude(), msg.GetDegreesLongitude()) + latChar := 'N' + if msg.GetDegreesLatitude() < 0 { + latChar = 'S' + } + longChar := 'E' + if msg.GetDegreesLongitude() < 0 { + longChar = 'W' + } + name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(msg.GetDegreesLatitude()), latChar, math.Abs(msg.GetDegreesLongitude()), longChar) } content := &event.MessageEventContent{ @@ -81,7 +55,24 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE FormattedBody: fmt.Sprintf(`Location: %s
%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())), GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()), } - addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) + + if len(msg.GetJPEGThumbnail()) > 0 { + thumbnailMime := http.DetectContentType(msg.GetJPEGThumbnail()) + thumbnailURL, thumbnailFile, err := getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, msg.GetJPEGThumbnail(), "thumb.jpeg", thumbnailMime) + if err == nil { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.GetJPEGThumbnail())) + content.Info = &event.FileInfo{ + ThumbnailInfo: &event.FileInfo{ + Size: len(msg.GetJPEGThumbnail()), + Width: cfg.Width, + Height: cfg.Height, + MimeType: thumbnailMime, + }, + ThumbnailURL: thumbnailURL, + ThumbnailFile: thumbnailFile, + } + } + } return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, @@ -90,40 +81,14 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE } func (mc *MessageConverter) convertLiveLocationMessage(ctx context.Context, msg *waE2E.LiveLocationMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - if msg.DegreesLatitude == nil || msg.DegreesLongitude == nil { - content := &event.MessageEventContent{ - Body: "Started sharing live location", - MsgType: event.MsgNotice, - } - if len(msg.GetCaption()) > 0 { - content.Body += ": " + msg.GetCaption() - } - content.Body += "\n\nUse the WhatsApp app to see the location." - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: content, - }, msg.GetContextInfo() - } - - lat, long := msg.GetDegreesLatitude(), msg.GetDegreesLongitude() - url := fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", lat, long) - name := formatCoordinates(lat, long) - const note = "Live updates are only visible in the WhatsApp app." - body := fmt.Sprintf("Live location: %s\n%s", name, url) - formattedBody := fmt.Sprintf(`Live location: %s`, html.EscapeString(url), html.EscapeString(name)) - if caption := msg.GetCaption(); len(caption) > 0 { - body += "\n" + caption - formattedBody += "
" + html.EscapeString(caption) - } content := &event.MessageEventContent{ - MsgType: event.MsgLocation, - Body: body + "\n\n" + note, - Format: event.FormatHTML, - FormattedBody: formattedBody + "

" + note, - GeoURI: fmt.Sprintf("geo:%.5f,%.5f", lat, long), + Body: "Started sharing live location", + MsgType: event.MsgNotice, } - addLocationThumbnail(ctx, content, msg.GetJPEGThumbnail()) - + if len(msg.GetCaption()) > 0 { + content.Body += ": " + msg.GetCaption() + } + content.Body += "\n\nUse the WhatsApp app to see the location." return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: content, diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 839f4c2..ff7008b 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -49,9 +49,6 @@ func (mc *MessageConverter) convertMediaMessage( isViewOnce bool, cachedPart *bridgev2.ConvertedMessagePart, ) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) { - if viewOnceMsg, ok := msg.(MediaMessageWithViewOnce); ok && viewOnceMsg.GetViewOnce() { - isViewOnce = true - } if mc.DisableViewOnce && isViewOnce { body := "You received a view once message. For added privacy, you can only open it on the WhatsApp app." if messageInfo.IsFromMe { @@ -235,11 +232,6 @@ type MediaMessageWithCaption interface { GetCaption() string } -type MediaMessageWithViewOnce interface { - MediaMessage - GetViewOnce() bool -} - type MediaMessageWithDimensions interface { MediaMessage GetHeight() uint32 @@ -263,10 +255,10 @@ func fixStickerDimensions(info *event.FileInfo) { info.Width = WhatsAppStickerSize info.Height = WhatsAppStickerSize } else if info.Width > info.Height { - info.Height = info.Height * WhatsAppStickerSize / info.Width + info.Height /= info.Width / WhatsAppStickerSize info.Width = WhatsAppStickerSize } else { - info.Width = info.Width * WhatsAppStickerSize / info.Height + info.Width /= info.Height / WhatsAppStickerSize info.Height = WhatsAppStickerSize } } diff --git a/pkg/msgconv/wa-rich-response.go b/pkg/msgconv/wa-rich-response.go index e506a64..28a648c 100644 --- a/pkg/msgconv/wa-rich-response.go +++ b/pkg/msgconv/wa-rich-response.go @@ -29,7 +29,6 @@ import ( "github.com/yuin/goldmark/extension" "go.mau.fi/whatsmeow/types/richresponse" "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format/mdext" @@ -37,8 +36,7 @@ import ( func (mc *MessageConverter) convertUnifiedRichResponseMessage(ctx context.Context, data []byte) *bridgev2.ConvertedMessagePart { extra := map[string]any{} - _, canonicalErr := canonicaljson.Marshal(json.RawMessage(data)) - if canonicalErr == nil && len(data) < 24*1024 { + if json.Valid(data) && len(data) < 24*1024 { extra["fi.mau.whatsapp.rich_message"] = json.RawMessage(data) } else if len(data) < 16*1024 { extra["fi.mau.whatsapp.non_json_rich_message"] = base64.StdEncoding.EncodeToString(data) diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index b1a47e0..15c0a42 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -45,8 +45,6 @@ type UserLoginMetadata struct { HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` - PreviousOwnLIDs []types.JID `json:"previous_own_lids,omitempty"` - Blocklist *types.Blocklist `json:"blocklist,omitempty"` MData json.RawMessage `json:"mdata,omitempty"`