diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8b988db..df15747 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,6 +30,7 @@ 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 c9cc6a3..1f1fe9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,10 @@ repos: # TODO: reenable this and fix all the problems - repo: https://github.com/beeper/pre-commit-go - rev: v0.4.2 + rev: v0.5.0 hooks: - id: zerolog-ban-msgf - id: zerolog-use-stringer + - id: bridgelint + language: system + verbose: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ba2ca..4e00a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v26.11 (unreleased) + +* Updated libsignal to v0.106.1. +* Added support for sending more than 2048 characters of text to Signal by + uploading the rest of the text as an attachment, like official clients do. +* Added automatic retrying of group message sends if the server returns a 500 + or 503 error. +* Added support for encrypting direct media with FLOE ([MSC4571]). +* Fixed memory leak in libsignal store implementation. + +[MSC4571]: https://github.com/matrix-org/matrix-spec-proposals/pull/4571 + # v26.10 * Updated libsignal to v0.105.0. diff --git a/go.mod b/go.mod index 89ed63d..2847afc 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.0 + maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d ) require ( diff --git a/go.sum b/go.sum index e5a7857..0da95ea 100644 --- a/go.sum +++ b/go.sum @@ -95,5 +95,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.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I= -maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index b7b2a23..b566850 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -77,8 +77,8 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM } // GetBackupChatItems returns in reverse chronological order, so flip the list slices.Reverse(items) - var firstDirectionfulProcessed bool var isRead bool + var readUpTo time.Time convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items)) attMap := make(msgconv.AttachmentMap) recipientMap := make(map[uint64]*backuppb.Recipient) @@ -123,20 +123,17 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM case *backuppb.ChatItem_Incoming: streamOrder = int64(dt.Incoming.GetDateServerSent()) prevStreamOrder = streamOrder - if !firstDirectionfulProcessed { - firstDirectionfulProcessed = true - isRead = dt.Incoming.Read + isRead = dt.Incoming.Read + if isRead { + readUpTo = time.UnixMilli(int64(item.DateSent)) } case *backuppb.ChatItem_Outgoing: streamOrder = int64(item.GetDateSent()) // Ensure stream order is higher than previous incoming item, but lower than next incoming item streamOrder = min(streamOrder, findNextStreamOrder(i+1)-1) streamOrder = max(streamOrder, prevStreamOrder+1) - - if !firstDirectionfulProcessed { - firstDirectionfulProcessed = true - isRead = true - } + isRead = true + readUpTo = time.UnixMilli(int64(item.DateSent)) } if len(attMap) > 0 { clear(attMap) @@ -178,11 +175,15 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM Reactions: convertedReactions, }) } + if isRead { + readUpTo = time.Time{} + } return &bridgev2.FetchMessagesResponse{ Messages: convertedMessages, HasMore: len(items) >= params.Count, Forward: params.Forward, MarkRead: isRead, + ReadUpTo: readUpTo, ApproxTotalCount: chat.TotalMessages, CompleteCallback: func() { // When reaching the last backwards backfill batch, delete the chat from the backup store. diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 05c7e60..4ff4b3c 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -40,7 +40,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.signal.capabilities.2026_10_06" + base := "fi.mau.signal.capabilities.2026_10_21" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -48,7 +48,7 @@ func capID() string { } const MaxFileSize = 100 * 1024 * 1024 -const MaxTextLength = 2000 +const MaxTextLength = 64 * 1024 var signalCaps = &event.RoomFeatures{ ID: capID(), @@ -105,6 +105,8 @@ var signalCaps = &event.RoomFeatures{ "audio/mpeg": event.CapLevelFullySupported, "audio/mp3": event.CapLevelFullySupported, "audio/flac": event.CapLevelFullySupported, + "audio/mp4": event.CapLevelFullySupported, + "audio/wav": event.CapLevelFullySupported, }, MaxSize: MaxFileSize, }, @@ -158,7 +160,7 @@ var signalCaps = &event.RoomFeatures{ event.MemberActionBan: event.CapLevelFullySupported, event.MemberActionKick: event.CapLevelFullySupported, }, - MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files + MaxTextLength: MaxTextLength, LocationMessage: event.CapLevelPartialSupport, Poll: event.CapLevelFullySupported, PollEnd: event.CapLevelUnsupported, @@ -263,5 +265,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities } func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { - return 1, 13 + return 1, 15 } diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 759cdf9..669c015 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -125,9 +125,10 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec } name := s.Main.Config.FormatDisplayname(contact, username) ui.Name = &name - if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" { + contactAvatarID := networkid.AvatarID("hash:" + contact.ContactAvatar.Hash) + if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" && s.canUseContactAvatar(ctx, contact, contactAvatarID) { ui.Avatar = &bridgev2.Avatar{ - ID: networkid.AvatarID("hash:" + contact.ContactAvatar.Hash), + ID: contactAvatarID, Get: func(ctx context.Context) ([]byte, error) { if contact.ContactAvatar.Image == nil { return nil, fmt.Errorf("contact avatar not available") @@ -172,6 +173,20 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec return ui, nil } +func (s *SignalClient) canUseContactAvatar(ctx context.Context, contact *types.Recipient, avatarID networkid.AvatarID) bool { + if contact.ContactAvatar.Image != nil { + return true + } else if contact.ACI == uuid.Nil { + return false + } + ghost, err := s.Main.Bridge.GetExistingGhostByID(ctx, signalid.MakeUserID(contact.ACI)) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost to check contact avatar") + return true + } + return ghost != nil && ghost.AvatarID == avatarID && ghost.AvatarMXC != "" && ghost.AvatarSet +} + func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool { _, err := signalid.ParseUserIDAsServiceID(id) return err == nil @@ -220,7 +235,7 @@ func (s *SignalClient) resolvePhone(ctx context.Context, number string) (aci, pn aci = recipient.ACI pni = recipient.PNI } else if resp, lookupErr := s.Client.LookupPhone(ctx, e164Number); lookupErr != nil { - err = fmt.Errorf("error looking up number on server: %w", lookupErr) + err = wrapRespError(fmt.Errorf("error looking up number on server: %w", lookupErr)) } else { aci = resp[e164Number].ACI pni = resp[e164Number].PNI @@ -243,6 +258,7 @@ func (s *SignalClient) resolvePhone(ctx context.Context, number string) (aci, pn func (s *SignalClient) resolveUsername(ctx context.Context, username string) (aci, pni uuid.UUID, err error) { serviceID, err := s.Client.ResolveUsername(ctx, username) if err != nil { + err = wrapRespError(fmt.Errorf("error looking up username on server: %w", err)) return } aci, pni = serviceID.ToACIAndPNI() @@ -388,7 +404,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr } group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey) if err != nil { - return nil, fmt.Errorf("failed to upload avatar: %w", err) + return nil, wrapRespError(fmt.Errorf("failed to upload avatar: %w", err)) } } portal, err := s.Main.Bridge.GetPortalByKey(ctx, s.makePortalKey(string(group.GroupIdentifier))) @@ -418,7 +434,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr } resp, err := s.Client.CreateGroup(ctx, group) if err != nil { - return nil, fmt.Errorf("failed to create group: %w", err) + return nil, wrapRespError(fmt.Errorf("failed to create group: %w", err)) } if params.RoomID != "" { // UpdateMatrixRoomID could do this for us if we passed ChatInfoSource to it, @@ -444,11 +460,15 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI } resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients)) for i, recipient := range recipients { - recipientResp := &bridgev2.ResolveIdentifierResponse{ - Chat: s.makeCreateDMResponse(ctx, recipient, nil), + if err = ctx.Err(); err != nil { + return nil, err } + recipientResp := &bridgev2.ResolveIdentifierResponse{} var username string if recipient.ACI != uuid.Nil { + recipientResp.Chat = &bridgev2.CreateChatResponse{ + PortalKey: s.makeDMPortalKey(libsignalgo.NewACIServiceID(recipient.ACI)), + } recipientResp.UserID = signalid.MakeUserID(recipient.ACI) ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID) if err != nil { @@ -457,7 +477,11 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI recipientResp.Ghost = ghost username = ghost.Metadata.(*signalid.GhostMetadata).Username } else { - recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI)) + pniServiceID := libsignalgo.NewPNIServiceID(recipient.PNI) + recipientResp.Chat = &bridgev2.CreateChatResponse{ + PortalKey: s.makeDMPortalKey(pniServiceID), + } + recipientResp.UserID = signalid.MakeUserIDFromServiceID(pniServiceID) } recipientResp.UserInfo, err = s.contactToUserInfo(ctx, recipient, username, false) if err != nil { diff --git a/pkg/connector/chatsync.go b/pkg/connector/chatsync.go index 47963d7..ec05b36 100644 --- a/pkg/connector/chatsync.go +++ b/pkg/connector/chatsync.go @@ -19,6 +19,7 @@ package connector import ( "context" "encoding/base64" + "errors" "time" "github.com/google/uuid" @@ -28,6 +29,7 @@ import ( "go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/signalid" + "go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb" "go.mau.fi/mautrix-signal/pkg/signalmeow/types" ) @@ -48,7 +50,15 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) if s.Client.Store.EphemeralBackupKey != nil { zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats") meta, err := s.Client.WaitForTransfer(ctx) - if err != nil { + if errors.Is(err, signalmeow.ErrTransferTimedOut) { + zerolog.Ctx(ctx).Warn().Msg("Transfer archive wasn't uploaded in time, giving up") + s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced = true + err = s.UserLogin.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login metadata after transfer archive timed out") + } + return + } else if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to request transfer archive") return } else if meta.Error != "" { diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d915576..b5531b5 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -182,9 +182,9 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone { s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState") if err == nil { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "signal-disconnected"}) } else { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(connectionErrorState(err)) } } } @@ -192,11 +192,7 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec case signalmeow.SignalConnectionEventLoggedOut: s.stopChatSync() s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") - if err == nil { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) - } else { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: err.Error()}) - } + s.sendLoggedOutState(err) err = s.Client.ClearKeysAndDisconnect(context.TODO()) if err != nil { s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect") @@ -204,18 +200,18 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec case signalmeow.SignalConnectionEventError: s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(connectionErrorState(err)) case signalmeow.SignalConnectionEventFatalError: s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "signal-connection-failed", Info: map[string]any{"go_error": err.Error()}}) case signalmeow.SignalConnectionCleanShutdown: if s.Client.IsLoggedIn() { s.UserLogin.Log.Debug().Msg("Clean Shutdown - sending no BridgeState") } else { s.UserLogin.Log.Debug().Msg("Clean Shutdown, but logged out - Sending BadCredentials BridgeState") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) + s.sendLoggedOutState(nil) } } } @@ -223,7 +219,10 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec func (s *SignalClient) Connect(ctx context.Context) { if s.Client == nil { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You're not logged into Signal"}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: "signal-not-logged-in", UserAction: status.UserActionRelogin}) + return + } else if !s.Client.IsLoggedIn() { + s.sendLoggedOutState(nil) return } s.updateRemoteProfile(ctx, false) @@ -231,6 +230,9 @@ func (s *SignalClient) Connect(ctx context.Context) { } func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.ConnectBackgroundParams) error { + if !s.IsLoggedIn() { + return bridgev2.ErrNotLoggedIn + } s.queueEmptyWaiter.Clear() ch, unauthCh, err := s.Client.StartWebsockets(ctx) if err != nil { @@ -251,6 +253,11 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec log.Err(status.Err).Msg("Authed websocket disconnected") case web.SignalWebsocketConnectionEventLoggedOut: log.Err(status.Err).Msg("Authed websocket logged out") + s.sendLoggedOutState(status.Err) + err = s.Client.ClearKeysAndDisconnect(ctx) + if err != nil { + log.Err(err).Msg("Failed to clear keys and disconnect") + } return fmt.Errorf("authed websocket logged out: %w", status.Err) case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: log.Err(status.Err).Msg("Authed websocket error") @@ -315,7 +322,7 @@ func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSy ch, err := s.Client.StartReceiveLoops(ctx) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "signal-connect-failed", Info: map[string]any{"go_error": err.Error()}}) retryInSeconds := 2 << retryCount if retryInSeconds > 150 { retryInSeconds = 150 diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 6e9f2e0..9a0d65a 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -42,6 +42,7 @@ type SignalConfig struct { NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` LocationFormat string `yaml:"location_format"` ExtEvPolls bool `yaml:"extev_polls"` + IdentityChangeNotices bool `yaml:"identity_change_notices"` displaynameTemplate *template.Template `yaml:"-"` } @@ -106,6 +107,7 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Str, "note_to_self_avatar") helper.Copy(up.Str, "location_format") helper.Copy(up.Bool, "extev_polls") + helper.Copy(up.Bool, "identity_change_notices") } func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 392a100..3a3823a 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -94,8 +94,11 @@ func (s *SignalConnector) ResetHTTPTransport() { settings = hs.GetHTTPClientSettings() } oldClient := web.SignalHTTPClient + oldUploadClient := web.SignalUploadHTTPClient web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile() + web.SignalUploadHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).WithResponseHeaderTimeout(2 * time.Minute).Compile() oldClient.CloseIdleConnections() + oldUploadClient.CloseIdleConnections() } func (s *SignalConnector) ResetNetworkConnections() { diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 05e2a07..82ed16d 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -79,7 +79,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } case *signalid.DirectMediaProfileAvatar: log.Info(). @@ -107,7 +107,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } case *signalid.DirectMediaSticker: log.Info(). @@ -118,7 +118,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } default: return nil, fmt.Errorf("no downloader for direct media type: %T", info) diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go new file mode 100644 index 0000000..c06ee17 --- /dev/null +++ b/pkg/connector/errors.go @@ -0,0 +1,183 @@ +// mautrix-signal - A Matrix-Signal puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/status" + "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-signal/pkg/signalmeow" + "go.mau.fi/mautrix-signal/pkg/signalmeow/web" +) + +func init() { + status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{ + "signal-logged-out": "You have been logged out of Signal, please reconnect", + "signal-not-logged-in": "You're not logged into Signal", + "signal-disconnected": "Disconnected from Signal, reconnecting", + "signal-connection-error": "Connection to Signal failed, reconnecting", + "signal-server-error": "Signal server error, reconnecting", + "signal-rate-limited": "Rate limited by Signal, reconnecting", + "signal-connect-failed": "Failed to connect to Signal, retrying", + "signal-connection-failed": "Connection to Signal failed", + }) +} + +var ( + errEditOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only edit your own messages")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errDeleteOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only delete your own messages")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errInviteToDM = bridgev2.WrapErrorInStatus(errors.New("can't invite more users to Signal DMs")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusUnsupported). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errUnsupportedDisappearingType = bridgev2.WrapErrorInStatus(errors.New("only disappearing timers that start after reading are supported on Signal")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusUnsupported). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) +) + +func wrapConversionError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + return bridgev2.WrapErrorInStatus(err). + WithMessage("failed to convert message for Signal"). + WithIsCertain(true). + WithSendNotice(true) +} + +func wrapSendError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + switch { + case errors.Is(err, signalmeow.ErrUnregisteredUser): + return bridgev2.WrapErrorInStatus(err). + WithMessage("recipient isn't registered on Signal"). + WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.ErrRateLimited), errors.Is(err, signalmeow.RateLimitError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("rate limited by Signal, try again later"). + WithErrorReason(event.MessageStatusNetworkError). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.AuthorizationFailedError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("you don't have permission to do that in this Signal group"). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.GroupPatchNotAcceptedError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("Signal rejected the group change"). + WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + default: + return bridgev2.WrapErrorInStatus(err). + WithMessage("failed to send to Signal"). + WithSendNotice(true) + } +} + +func wrapRespError(err error) error { + var mapped bridgev2.RespError + var cdsRateLimit signalmeow.ContactDiscoveryRateLimitError + switch { + case errors.As(err, &cdsRateLimit), errors.Is(err, signalmeow.RateLimitError), errors.Is(err, signalmeow.ErrRateLimited), + grpcstatus.Code(err) == codes.ResourceExhausted: + mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithStatus(http.StatusBadRequest)) + case errors.Is(err, signalmeow.AuthorizationFailedError): + mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("Signal denied the request")) + default: + mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to Signal failed")) + } + return fmt.Errorf("%w: %w", mapped, err) +} + +func wrapDirectMediaError(err error) error { + var statusErr web.HTTPStatusError + if !errors.As(err, &statusErr) { + return err + } + switch statusErr.StatusCode { + case http.StatusNotFound, http.StatusGone: + return mautrix.MNotFound.WithMessage("Media not found on Signal") + case http.StatusUnauthorized, http.StatusForbidden: + return mautrix.MForbidden.WithMessage("Signal denied access to the media") + case http.StatusTooManyRequests: + return mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithCanRetry(true) + default: + return mautrix.MUnknown.WithMessage("Unexpected response from Signal").WithStatus(http.StatusBadGateway).WithCanRetry(true) + } +} + +func connectionErrorState(err error) status.BridgeState { + state := status.BridgeState{ + StateEvent: status.StateTransientDisconnect, + Error: "signal-connection-error", + Info: map[string]any{"go_error": err.Error()}, + } + var statusErr web.HTTPStatusError + if errors.As(err, &statusErr) { + if statusErr.StatusCode == http.StatusTooManyRequests { + state.Error = "signal-rate-limited" + } else if statusErr.StatusCode >= 500 { + state.Error = "signal-server-error" + } + } + return state +} + +func (s *SignalClient) sendLoggedOutState(err error) { + state := status.BridgeState{ + StateEvent: status.StateBadCredentials, + Error: "signal-logged-out", + UserAction: status.UserActionRelogin, + } + if err != nil { + state.Info = map[string]any{"go_error": err.Error()} + } + s.UserLogin.BridgeState.Send(state) +} diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index f67fbc8..0a818d1 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -25,3 +25,5 @@ note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s' # Should polls be sent using unstable MSC3381 event types? extev_polls: false +# Should another user's safety number changing send a message to Matrix? +identity_change_notices: false diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go index d958463..fa75743 100644 --- a/pkg/connector/groupinfo.go +++ b/pkg/connector/groupinfo.go @@ -39,6 +39,7 @@ import ( var defaultPL = 0 var moderatorPL = 50 +var terminatedPL = 100 func roleToPL(role signalmeow.GroupMemberRole) *int { switch role { @@ -70,6 +71,21 @@ func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess si plc.Events[event.StateBeeperDisappearingTimer] = attributePL } +func applyTerminated(plc *bridgev2.PowerLevelOverrides) { + plc.EventsDefault = &terminatedPL + plc.StateDefault = &terminatedPL + plc.Invite = &terminatedPL + plc.Kick = &terminatedPL + plc.Ban = &terminatedPL + plc.Redact = &terminatedPL + for _, evtType := range []event.Type{ + event.EventReaction, event.EventRedaction, event.StatePowerLevels, event.StateRoomName, + event.StateRoomAvatar, event.StateTopic, event.StateBeeperDisappearingTimer, + } { + plc.Events[evtType] = terminatedPL + } +} + func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) { if memberAccess == signalmeow.AccessControl_ADMINISTRATOR { plc.Invite = &moderatorPL @@ -112,6 +128,8 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow. PowerLevels: &bridgev2.PowerLevelOverrides{ Events: map[event.Type]int{ event.StatePowerLevels: moderatorPL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, }, }, ExcludeChangesFromTimeline: true, @@ -123,6 +141,9 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow. applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members) joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink) } + if groupInfo.Terminated { + applyTerminated(members.PowerLevels) + } for _, member := range groupInfo.RequestingMembers { members.MemberMap.Set(bridgev2.ChatMember{ EventSender: s.makeEventSender(member.ACI), @@ -182,9 +203,12 @@ func addMemberToMap(mc map[networkid.UserID]bridgev2.ChatMember, member bridgev2 mc[member.EventSender.Sender] = member } +const currentPortalInfoVersion = 1 + func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool { meta := portal.Metadata.(*signalid.PortalMetadata) meta.LastSync = jsontime.UnixNow() + meta.InfoVersion = currentPortalInfoVersion return true } @@ -260,7 +284,8 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID var pls *bridgev2.PowerLevelOverrides if groupChange.ModifyAnnouncementsOnly != nil || groupChange.ModifyAttributesAccess != nil || - groupChange.ModifyMemberAccess != nil { + groupChange.ModifyMemberAccess != nil || + groupChange.TerminateGroup { pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)} if groupChange.ModifyAnnouncementsOnly != nil { applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly) @@ -271,6 +296,9 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID if groupChange.ModifyMemberAccess != nil { applyMembersAccess(pls, *groupChange.ModifyMemberAccess) } + if groupChange.TerminateGroup { + applyTerminated(pls) + } } if groupChange.ModifyAddFromInviteLinkAccess != nil { ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{ diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 7360d67..ca7af09 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -37,6 +37,7 @@ import ( "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-signal/pkg/libsignalgo" + "go.mau.fi/mautrix-signal/pkg/msgconv" "go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/signalpb" @@ -81,7 +82,7 @@ func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.Porta log.Debug().Msg("No successes or failures - Probably sent to myself") } else if len(result.SuccessfullySentTo) == 0 { log.Error().Msg("Failed to send event to all members of Signal group") - return errors.New("failed to send to any members of Signal group") + return fmt.Errorf("failed to send to any members of Signal group: %w", result.FailureError()) } else if len(result.SuccessfullySentTo) < totalRecipients { if len(result.FailedToSendTo) > 0 { log.Warn().Msg("Failed to send event to some members of Signal group") @@ -123,10 +124,10 @@ func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Ma ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo, ) if err != nil { - return nil, err + return nil, wrapConversionError(err) } return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{ - ContainsAttachments: len(converted.Attachments) > 0, + ContainsAttachments: msgconv.ContainsAttachments(converted), ViewOnce: converted.GetIsViewOnce(), }) } @@ -146,7 +147,7 @@ func (s *SignalClient) doSendMessage( msg.AddPendingToIgnore(networkid.TransactionID(msgID)) err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted)) if err != nil { - return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true) + return nil, wrapSendError(err) } dbMsg := &database.Message{ ID: msgID, @@ -169,14 +170,14 @@ func (s *SignalClient) HandleMatrixViewLimitedMedia(ctx context.Context, msg *br if err != nil { return err } - return s.sendMessage(ctx, signalid.MakeDMPortalID(libsignalgo.NewACIServiceID(sender)), &signalpb.Content{ + return wrapSendError(s.sendMessage(ctx, signalid.MakeDMPortalID(libsignalgo.NewACIServiceID(sender)), &signalpb.Content{ Content: &signalpb.Content_ReceiptMessage{ ReceiptMessage: &signalpb.ReceiptMessage{ Type: signalpb.ReceiptMessage_VIEWED.Enum(), Timestamp: []uint64{timestamp}, }, }, - }) + })) } func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { @@ -187,7 +188,7 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) } else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) { - return fmt.Errorf("cannot edit other people's messages") + return errEditOthersMessage } var replyTo *database.Message if msg.EditTarget.ReplyTo.MessageID != "" { @@ -198,7 +199,7 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri } converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo) if err != nil { - return err + return wrapConversionError(err) } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) converted.Timestamp = &ts @@ -207,11 +208,11 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri DataMessage: converted, })) if err != nil { - return bridgev2.WrapErrorInStatus(err).WithSendNotice(true) + return wrapSendError(err) } prevID := msg.EditTarget.ID msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts) - msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0} + msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: msgconv.ContainsAttachments(converted)} msg.EditTarget.EditCount++ if prevID != msg.EditTarget.ID { err = s.Main.Bridge.DB.DoTxn(ctx, nil, func(ctx context.Context) error { @@ -276,7 +277,7 @@ func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.M }, })) if err != nil { - return nil, err + return nil, wrapSendError(err) } return &database.Reaction{}, nil } @@ -297,30 +298,52 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid TargetSentTimestamp: proto.Uint64(targetSentTimestamp), }, })) - if err != nil { - return err - } - return nil + return wrapSendError(err) } func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { - _, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID) + targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID) if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) - } else if msg.TargetMessage.SenderID != signalid.MakeUserID(s.Client.Store.ACI) { - return fmt.Errorf("cannot delete other people's messages") } - ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) - err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ - Timestamp: proto.Uint64(ts), - Delete: &signalpb.DataMessage_Delete{ + dm := &signalpb.DataMessage{ + Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)), + } + if msg.TargetMessage.SenderID == signalid.MakeUserID(s.Client.Store.ACI) { + dm.Delete = &signalpb.DataMessage_Delete{ TargetSentTimestamp: proto.Uint64(targetSentTimestamp), - }, - })) - if err != nil { - return err + } + } else if isAdmin, err := s.isGroupAdmin(ctx, msg.Portal); err != nil { + return wrapSendError(err) + } else if !isAdmin { + return errDeleteOthersMessage + } else { + dm.AdminDelete = &signalpb.DataMessage_AdminDelete{ + TargetAuthorAciBinary: targetAuthorACI[:], + TargetSentTimestamp: proto.Uint64(targetSentTimestamp), + } } - return nil + err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(dm)) + return wrapSendError(err) +} + +func (s *SignalClient) isGroupAdmin(ctx context.Context, portal *bridgev2.Portal) (bool, error) { + _, groupID, err := signalid.ParsePortalID(portal.ID) + if err != nil { + return false, err + } else if groupID == "" { + return false, nil + } + group, _, err := s.Client.RetrieveGroupByID(ctx, groupID, portal.Metadata.(*signalid.PortalMetadata).Revision) + if err != nil { + return false, fmt.Errorf("failed to get group info: %w", err) + } + for _, member := range group.Members { + if member.ACI == s.Client.Store.ACI { + return member.Role == signalmeow.GroupMember_ADMINISTRATOR, nil + } + } + return false, nil } func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error { @@ -403,7 +426,7 @@ func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev gc.Revision = portal.Metadata.(*signalid.PortalMetadata).Revision + 1 revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { - return false, err + return false, wrapSendError(err) } if gc.ModifyTitle != nil { portal.Name = *gc.ModifyTitle @@ -445,7 +468,7 @@ func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2 avatarHash = sha256.Sum256(data) avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "") if err != nil { - return false, fmt.Errorf("failed to reupload avatar: %w", err) + return false, wrapSendError(fmt.Errorf("failed to reupload avatar: %w", err)) } } return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{ @@ -472,7 +495,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2 if msg.Portal.RoomType == database.RoomTypeDM { switch msg.Type { case bridgev2.Invite: - return nil, fmt.Errorf("cannot invite additional user to dm") + return nil, errInviteToDM default: return nil, nil } @@ -601,7 +624,7 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2 gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1 revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { - return nil, err + return nil, wrapSendError(err) } if (msg.Type == bridgev2.Invite || msg.Type == bridgev2.AcceptKnock) && targetSignalID.Type != libsignalgo.ServiceIDTypePNI { err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) @@ -672,7 +695,7 @@ func (s *SignalClient) HandleMatrixPowerLevels(ctx context.Context, msg *bridgev } revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { - return false, err + return false, wrapSendError(err) } msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision return true, nil @@ -723,7 +746,7 @@ func (s *SignalClient) HandleMatrixViewingChat(ctx context.Context, msg *bridgev func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) { if msg.Content.Type != event.DisappearingTypeAfterRead && msg.Content.Timer.Duration != 0 { - return false, fmt.Errorf("unsupported disappearing timer type: %s", msg.Content.Type) + return false, errUnsupportedDisappearingType } userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil { @@ -750,7 +773,7 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), })) if !res.WasSuccessful { - return false, res.Error + return false, wrapSendError(res.Error) } msg.Portal.Disappear = newSetting return true, nil @@ -775,7 +798,7 @@ func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2 Revision: portalMeta.Revision + 1, }, groupID) if err != nil { - return fmt.Errorf("failed to leave group before deleting chat: %w", err) + return wrapSendError(fmt.Errorf("failed to leave group before deleting chat: %w", err)) } portalMeta.Revision = revision } @@ -785,7 +808,7 @@ func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2 // TODO block and delete support? err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE) if err != nil { - return fmt.Errorf("failed to send message request delete sync: %w", err) + return wrapSendError(fmt.Errorf("failed to send message request delete sync: %w", err)) } } @@ -863,7 +886,7 @@ func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2 Msg("Sent conversation deletion to Signal") if !result.WasSuccessful { - return fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID) + return wrapSendError(fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID)) } return nil @@ -967,7 +990,7 @@ func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg } err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT) if err != nil { - return fmt.Errorf("failed to sync message request acceptance: %w", err) + return wrapSendError(fmt.Errorf("failed to sync message request acceptance: %w", err)) } if userID.Type == libsignalgo.ServiceIDTypeACI { profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI) @@ -996,7 +1019,7 @@ func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg PniSignatureMessage: pniSig, }) if !res.WasSuccessful { - return fmt.Errorf("failed to share profile key to accept message request: %w", res.Error) + return wrapSendError(fmt.Errorf("failed to share profile key to accept message request: %w", res.Error)) } // TODO send read receipts too? } @@ -1033,7 +1056,7 @@ func (s *SignalClient) HandleMatrixBlockUser(ctx context.Context, msg *bridgev2. } } if err = s.syncMessageRequestResponse(ctx, msg.Portal, respType); err != nil { - return err + return wrapSendError(err) } _, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, func(recipient *types.Recipient) (bool, error) { changed := recipient.Blocked != msg.Content.Block @@ -1055,7 +1078,7 @@ func (s *SignalClient) reportSpam(ctx context.Context, portal *bridgev2.Portal, continue } if err = s.Client.ReportSpam(ctx, aci, meta.ServerGUID, meta.ReportingToken); err != nil { - return err + return wrapSendError(err) } seen[meta.ServerGUID] = true if len(seen) == 3 { diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go index a25b77a..b6e746f 100644 --- a/pkg/connector/handlesignal.go +++ b/pkg/connector/handlesignal.go @@ -34,7 +34,6 @@ import ( "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/simplevent" - "maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-signal/pkg/libsignalgo" @@ -74,9 +73,9 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool { case *events.QueueEmpty: s.queueEmptyWaiter.Set() case *events.LoggedOut: - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: evt.Error.Error()}) + s.sendLoggedOutState(evt.Error) case *events.IdentityChange: - // TODO send notice + s.handleSignalIdentityChange(evt) default: s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type") } @@ -162,6 +161,44 @@ func convertDecryptionError(_ context.Context, _ *bridgev2.Portal, _ bridgev2.Ma }, nil } +func (s *SignalClient) handleSignalIdentityChange(evt *events.IdentityChange) { + if !s.Main.Config.IdentityChangeNotices || evt.ServiceID.Type != libsignalgo.ServiceIDTypeACI || evt.ServiceID.UUID == s.Client.Store.ACI { + return + } + s.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{ + EventMeta: simplevent.EventMeta{ + Type: bridgev2.RemoteEventMessage, + LogContext: func(c zerolog.Context) zerolog.Context { + return c.Stringer("sender_id", evt.ServiceID) + }, + PortalKey: s.makeDMPortalKey(evt.ServiceID), + Sender: s.makeEventSender(evt.ServiceID.UUID), + }, + Data: evt, + ID: "idchange|" + signalid.MakeMessageID(evt.ServiceID.UUID, uint64(time.Now().UnixMilli())), + ConvertMessageFunc: convertIdentityChange, + }) +} + +func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, _ bridgev2.MatrixAPI, evt *events.IdentityChange) (*bridgev2.ConvertedMessage, error) { + name := evt.ServiceID.UUID.String() + ghost, err := portal.Bridge.GetGhostByID(ctx, signalid.MakeUserID(evt.ServiceID.UUID)) + if err != nil { + return nil, err + } else if ghost.Name != "" { + name = ghost.Name + } + return &bridgev2.ConvertedMessage{ + Parts: []*bridgev2.ConvertedMessagePart{{ + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: fmt.Sprintf("Your safety number with %s has changed.", name), + }, + }}, + }, nil +} + type Bv2ChatEvent struct { *events.ChatEvent s *SignalClient @@ -186,6 +223,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType { switch { case innerEvt.GetIsViewOnce(), innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil, innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil, + innerEvt.PinMessage != nil, innerEvt.UnpinMessage != nil, innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT), innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0: return bridgev2.RemoteEventMessage @@ -234,13 +272,16 @@ func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) if !ok || dataMsg.GroupV2 == nil { return } - portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision + portalMeta := portal.Metadata.(*signalid.PortalMetadata) + portalRev := portalMeta.Revision if evt.Info.GroupRevision > portalRev { toRevision := evt.Info.GroupRevision if dataMsg.GetGroupV2().GetGroupChange() != nil { toRevision-- } evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp()) + } else if portalMeta.InfoVersion < currentPortalInfoVersion && portal.MXID != "" { + evt.s.catchUpGroup(ctx, portal, 0, portalRev, dataMsg.GetTimestamp()) } } diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 8c6bdab..dd636b3 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -107,7 +107,7 @@ var ( ErrDeviceLinkRateLimited = bridgev2.RespError{ ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED", Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again", - StatusCode: http.StatusTooManyRequests, + StatusCode: http.StatusBadRequest, } ErrDeviceLinkRejected = bridgev2.RespError{ ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED", diff --git a/pkg/libsignalgo/backupkey.go b/pkg/libsignalgo/backupkey.go index b5f53f2..47cce90 100644 --- a/pkg/libsignalgo/backupkey.go +++ b/pkg/libsignalgo/backupkey.go @@ -49,8 +49,8 @@ func (bk *BackupKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk)) } -func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(bk.cFixedArray()) +func (bk *BackupKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(bk)) } func GenerateRandomBackupKey() *BackupKey { diff --git a/pkg/libsignalgo/fixedarray.go b/pkg/libsignalgo/fixedarray.go index 949e808..ad94514 100644 --- a/pkg/libsignalgo/fixedarray.go +++ b/pkg/libsignalgo/fixedarray.go @@ -42,118 +42,118 @@ func (a *fixedArray15) cFixedArray() *C.SignalType_FixedArray15_uint8_t { return (*C.SignalType_FixedArray15_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray15) cConstFixedArray() cFixedArray15Compat { - return cFixedArray15Compat(a.cFixedArray()) +func (a *fixedArray15) cConstFixedArray() C.SignalType_ConstPointer_FixedArray15_uint8_t { + return (C.SignalType_ConstPointer_FixedArray15_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray16) cFixedArray() *C.SignalType_FixedArray16_uint8_t { return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray16) cConstFixedArray() cFixedArray16Compat { - return cFixedArray16Compat(a.cFixedArray()) +func (a *fixedArray16) cConstFixedArray() C.SignalType_ConstPointer_FixedArray16_uint8_t { + return (C.SignalType_ConstPointer_FixedArray16_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray17) cFixedArray() *C.SignalType_FixedArray17_uint8_t { return (*C.SignalType_FixedArray17_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray17) cConstFixedArray() cFixedArray17Compat { - return cFixedArray17Compat(a.cFixedArray()) +func (a *fixedArray17) cConstFixedArray() C.SignalType_ConstPointer_FixedArray17_uint8_t { + return (C.SignalType_ConstPointer_FixedArray17_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray32) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray32) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(a.cFixedArray()) +func (a *fixedArray32) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray64) cFixedArray() *C.SignalType_FixedArray64_uint8_t { return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray64) cConstFixedArray() cFixedArray64Compat { - return cFixedArray64Compat(a.cFixedArray()) +func (a *fixedArray64) cConstFixedArray() C.SignalType_ConstPointer_FixedArray64_uint8_t { + return (C.SignalType_ConstPointer_FixedArray64_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray65) cFixedArray() *C.SignalType_FixedArray65_uint8_t { return (*C.SignalType_FixedArray65_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray65) cConstFixedArray() cFixedArray65Compat { - return cFixedArray65Compat(a.cFixedArray()) +func (a *fixedArray65) cConstFixedArray() C.SignalType_ConstPointer_FixedArray65_uint8_t { + return (C.SignalType_ConstPointer_FixedArray65_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray97) cFixedArray() *C.SignalType_FixedArray97_uint8_t { return (*C.SignalType_FixedArray97_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray97) cConstFixedArray() cFixedArray97Compat { - return cFixedArray97Compat(a.cFixedArray()) +func (a *fixedArray97) cConstFixedArray() C.SignalType_ConstPointer_FixedArray97_uint8_t { + return (C.SignalType_ConstPointer_FixedArray97_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray129) cFixedArray() *C.SignalType_FixedArray129_uint8_t { return (*C.SignalType_FixedArray129_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray129) cConstFixedArray() cFixedArray129Compat { - return cFixedArray129Compat(a.cFixedArray()) +func (a *fixedArray129) cConstFixedArray() C.SignalType_ConstPointer_FixedArray129_uint8_t { + return (C.SignalType_ConstPointer_FixedArray129_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray153) cFixedArray() *C.SignalType_FixedArray153_uint8_t { return (*C.SignalType_FixedArray153_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray153) cConstFixedArray() cFixedArray153Compat { - return cFixedArray153Compat(a.cFixedArray()) +func (a *fixedArray153) cConstFixedArray() C.SignalType_ConstPointer_FixedArray153_uint8_t { + return (C.SignalType_ConstPointer_FixedArray153_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray177) cFixedArray() *C.SignalType_FixedArray177_uint8_t { return (*C.SignalType_FixedArray177_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray177) cConstFixedArray() cFixedArray177Compat { - return cFixedArray177Compat(a.cFixedArray()) +func (a *fixedArray177) cConstFixedArray() C.SignalType_ConstPointer_FixedArray177_uint8_t { + return (C.SignalType_ConstPointer_FixedArray177_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray289) cFixedArray() *C.SignalType_FixedArray289_uint8_t { return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray289) cConstFixedArray() cFixedArray289Compat { - return cFixedArray289Compat(a.cFixedArray()) +func (a *fixedArray289) cConstFixedArray() C.SignalType_ConstPointer_FixedArray289_uint8_t { + return (C.SignalType_ConstPointer_FixedArray289_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray329) cFixedArray() *C.SignalType_FixedArray329_uint8_t { return (*C.SignalType_FixedArray329_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray329) cConstFixedArray() cFixedArray329Compat { - return cFixedArray329Compat(a.cFixedArray()) +func (a *fixedArray329) cConstFixedArray() C.SignalType_ConstPointer_FixedArray329_uint8_t { + return (C.SignalType_ConstPointer_FixedArray329_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray409) cFixedArray() *C.SignalType_FixedArray409_uint8_t { return (*C.SignalType_FixedArray409_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray409) cConstFixedArray() cFixedArray409Compat { - return cFixedArray409Compat(a.cFixedArray()) +func (a *fixedArray409) cConstFixedArray() C.SignalType_ConstPointer_FixedArray409_uint8_t { + return (C.SignalType_ConstPointer_FixedArray409_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray473) cFixedArray() *C.SignalType_FixedArray473_uint8_t { return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray473) cConstFixedArray() cFixedArray473Compat { - return cFixedArray473Compat(a.cFixedArray()) +func (a *fixedArray473) cConstFixedArray() C.SignalType_ConstPointer_FixedArray473_uint8_t { + return (C.SignalType_ConstPointer_FixedArray473_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray497) cFixedArray() *C.SignalType_FixedArray497_uint8_t { return (*C.SignalType_FixedArray497_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray497) cConstFixedArray() cFixedArray497Compat { - return cFixedArray497Compat(a.cFixedArray()) +func (a *fixedArray497) cConstFixedArray() C.SignalType_ConstPointer_FixedArray497_uint8_t { + return (C.SignalType_ConstPointer_FixedArray497_uint8_t)(unsafe.Pointer(a)) } diff --git a/pkg/libsignalgo/fixedarray_clang.go b/pkg/libsignalgo/fixedarray_clang.go deleted file mode 100644 index a364eb5..0000000 --- a/pkg/libsignalgo/fixedarray_clang.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build darwin || android || ios || (windows && arm64) || clang - -package libsignalgo - -/* -#include "./libsignal-ffi.h" -*/ -import "C" - -type cFixedArray15Compat = *C.SignalType_FixedArray15_uint8_t -type cFixedArray16Compat = *C.SignalType_FixedArray16_uint8_t -type cFixedArray17Compat = *C.SignalType_FixedArray17_uint8_t -type cFixedArray32Compat = *C.SignalType_FixedArray32_uint8_t -type cFixedArray64Compat = *C.SignalType_FixedArray64_uint8_t -type cFixedArray65Compat = *C.SignalType_FixedArray65_uint8_t -type cFixedArray97Compat = *C.SignalType_FixedArray97_uint8_t -type cFixedArray129Compat = *C.SignalType_FixedArray129_uint8_t -type cFixedArray153Compat = *C.SignalType_FixedArray153_uint8_t -type cFixedArray177Compat = *C.SignalType_FixedArray177_uint8_t -type cFixedArray289Compat = *C.SignalType_FixedArray289_uint8_t -type cFixedArray329Compat = *C.SignalType_FixedArray329_uint8_t -type cFixedArray409Compat = *C.SignalType_FixedArray409_uint8_t -type cFixedArray473Compat = *C.SignalType_FixedArray473_uint8_t -type cFixedArray497Compat = *C.SignalType_FixedArray497_uint8_t diff --git a/pkg/libsignalgo/fixedarray_gcc.go b/pkg/libsignalgo/fixedarray_gcc.go deleted file mode 100644 index 14bd230..0000000 --- a/pkg/libsignalgo/fixedarray_gcc.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !(darwin || android || ios || (windows && arm64) || clang) - -package libsignalgo - -/* -#include "./libsignal-ffi.h" -*/ -import "C" - -// Hack for https://github.com/golang/go/issues/7270 -// The clang version is more correct, but doesn't work with gcc. -type cFixedArray15Compat = *[15]C.uint8_t -type cFixedArray16Compat = *[16]C.uint8_t -type cFixedArray17Compat = *[17]C.uint8_t -type cFixedArray32Compat = *[32]C.uint8_t -type cFixedArray64Compat = *[64]C.uint8_t -type cFixedArray65Compat = *[65]C.uint8_t -type cFixedArray97Compat = *[97]C.uint8_t -type cFixedArray129Compat = *[129]C.uint8_t -type cFixedArray153Compat = *[153]C.uint8_t -type cFixedArray177Compat = *[177]C.uint8_t -type cFixedArray289Compat = *[289]C.uint8_t -type cFixedArray329Compat = *[329]C.uint8_t -type cFixedArray409Compat = *[409]C.uint8_t -type cFixedArray473Compat = *[473]C.uint8_t -type cFixedArray497Compat = *[497]C.uint8_t diff --git a/pkg/libsignalgo/groupsecretparams.go b/pkg/libsignalgo/groupsecretparams.go index 90149b4..9402d0c 100644 --- a/pkg/libsignalgo/groupsecretparams.go +++ b/pkg/libsignalgo/groupsecretparams.go @@ -57,24 +57,24 @@ func (gmk *GroupMasterKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gmk)) } -func (gmk *GroupMasterKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(gmk.cFixedArray()) +func (gmk *GroupMasterKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(gmk)) } func (gsp *GroupSecretParams) cFixedArray() *C.SignalType_FixedArray289_uint8_t { return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(gsp)) } -func (gsp *GroupSecretParams) cConstFixedArray() cFixedArray289Compat { - return cFixedArray289Compat(gsp.cFixedArray()) +func (gsp *GroupSecretParams) cConstFixedArray() C.SignalType_ConstPointer_FixedArray289_uint8_t { + return (C.SignalType_ConstPointer_FixedArray289_uint8_t)(unsafe.Pointer(gsp)) } func (gid *GroupIdentifier) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gid)) } -func (gid *GroupIdentifier) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(gid.cFixedArray()) +func (gid *GroupIdentifier) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(gid)) } func (gid *GroupIdentifier) String() string { diff --git a/pkg/libsignalgo/identitykeystore.go b/pkg/libsignalgo/identitykeystore.go index ba26f06..eb80056 100644 --- a/pkg/libsignalgo/identitykeystore.go +++ b/pkg/libsignalgo/identitykeystore.go @@ -90,12 +90,8 @@ func signal_get_local_registration_id_callback(storeCtx unsafe.Pointer, idp *C.u //export signal_save_identity_key_callback func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, address C.SignalMutPointerProtocolAddress, publicKey C.SignalMutPointerPublicKey) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - publicKeyStruct := PublicKey{ptr: publicKey.raw} - cloned, err := publicKeyStruct.Clone() - if err != nil { - return err - } - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) + pub := wrapPublicKey(publicKey.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err @@ -103,7 +99,7 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, replaced, err := store.SaveIdentityKey( ctx, theirServiceID, - &IdentityKey{cloned}, + &IdentityKey{pub}, ) if err != nil { return err @@ -120,7 +116,7 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, //export signal_get_identity_key_callback func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.SignalMutPointerPublicKey, address C.SignalMutPointerProtocolAddress) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err @@ -137,12 +133,13 @@ func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.Si //export signal_is_trusted_identity_callback func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, out *C.bool, address C.SignalMutPointerProtocolAddress, public_key C.SignalMutPointerPublicKey, direction C.uint32_t) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) + pub := wrapPublicKey(public_key.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err } - trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: public_key.raw}}, SignalDirection(direction)) + trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{pub}, SignalDirection(direction)) if err != nil { return err } diff --git a/pkg/libsignalgo/kyberprekeystore.go b/pkg/libsignalgo/kyberprekeystore.go index 9deea17..78ec925 100644 --- a/pkg/libsignalgo/kyberprekeystore.go +++ b/pkg/libsignalgo/kyberprekeystore.go @@ -52,18 +52,15 @@ func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalM //export signal_store_kyber_pre_key_callback func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerKyberPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { - record := KyberPreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreKyberPreKey(ctx, uint32(id), cloned) + record := wrapKyberPreKeyRecord(preKeyRecord.raw) + return store.StoreKyberPreKey(ctx, uint32(id), record) }) } //export signal_mark_kyber_pre_key_used_callback func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t, ecPrekeyID C.uint32_t, baseKey C.SignalMutPointerPublicKey) C.int { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { + _ = wrapPublicKey(baseKey.raw) // TODO use ecPrekeyID and baseKey? return store.MarkKyberPreKeyUsed(ctx, uint32(id)) }) diff --git a/pkg/libsignalgo/libsignal b/pkg/libsignalgo/libsignal index 4beb029..904c8dc 160000 --- a/pkg/libsignalgo/libsignal +++ b/pkg/libsignalgo/libsignal @@ -1 +1 @@ -Subproject commit 4beb029d8a941f81e7d9c6d8af1ed25a677569a8 +Subproject commit 904c8dc8216073b078a5f2f5090e3da8382e6a92 diff --git a/pkg/libsignalgo/libsignal-ffi.h b/pkg/libsignalgo/libsignal-ffi.h index b8cae20..d8e981e 100644 --- a/pkg/libsignalgo/libsignal-ffi.h +++ b/pkg/libsignalgo/libsignal-ffi.h @@ -520,6 +520,22 @@ static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfc_uchar17 typedef const SignalOwnedBuffer* SignalType_ConstPointer_SignalOwnedBuffer; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBuffer) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBuffer) == 8); +typedef SignalOwnedBuffer* SignalType_MutPointer_SignalOwnedBuffer; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalOwnedBuffer) == 8); +typedef struct { + SignalOwnedBuffer* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +typedef const SignalOwnedBufferOfMaxAlignedOwnedBuffer* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef enum { SignalAuthCheckResultFfiResultMatch, SignalAuthCheckResultFfiResultNoMatch, @@ -607,6 +623,33 @@ static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFf typedef const SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); +typedef enum { + SignalDeviceCapabilityInternalFfiResultStorage, + SignalDeviceCapabilityInternalFfiResultTransfer, + SignalDeviceCapabilityInternalFfiResultAttachmentBackfill, + SignalDeviceCapabilityInternalFfiResultSparsePostQuantumRatchet, + SignalDeviceCapabilityInternalFfiResultProfilesV2, + SignalDeviceCapabilityInternalFfiResultUsernameChangeSyncMessage, + SignalDeviceCapabilityInternalFfiResultOptionalPhoneNumber, +} SignalDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalDeviceCapabilityInternalFfiResult) == 4); +static_assert_64bit(alignof(SignalDeviceCapabilityInternalFfiResult) == 4); +typedef SignalDeviceCapabilityInternalFfiResult* SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult) == 8); +typedef struct { + SignalDeviceCapabilityInternalFfiResult* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +typedef const SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); typedef struct { uint8_t id; SignalOwnedBuffer encrypted_name; @@ -869,19 +912,6 @@ static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedi32, length) == 8); static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedi32, size_bytes) == 16); static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedi32) == 24); static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedi32) == 8); -typedef SignalOwnedBuffer* SignalType_MutPointer_SignalOwnedBuffer; -static_assert_64bit(sizeof(SignalType_MutPointer_SignalOwnedBuffer) == 8); -static_assert_64bit(alignof(SignalType_MutPointer_SignalOwnedBuffer) == 8); -typedef struct { - SignalOwnedBuffer* base; - size_t length; - size_t size_bytes; -} SignalOwnedBufferOfMaxAlignedOwnedBuffer; -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, base) == 0); -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, length) == 8); -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, size_bytes) == 16); -static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 24); -static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef struct { SignalOwnedBuffer user_handle; SignalOwnedBufferOfMaxAlignedi32 allowed_algorithms; @@ -1072,6 +1102,59 @@ static_assert_64bit(alignof(SignalType_ConstPointer_SignalServerMessageAck) == 8 typedef const SignalUnauthenticatedChatConnection* SignalType_ConstPointer_SignalUnauthenticatedChatConnection; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalUnauthenticatedChatConnection) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalUnauthenticatedChatConnection) == 8); +typedef struct { + int64_t level; + uint64_t expiration; +} SignalBackupEntitlementFfiResult; +static_assert_64bit(offsetof(SignalBackupEntitlementFfiResult, level) == 0); +static_assert_64bit(offsetof(SignalBackupEntitlementFfiResult, expiration) == 8); +static_assert_64bit(sizeof(SignalBackupEntitlementFfiResult) == 16); +static_assert_64bit(alignof(SignalBackupEntitlementFfiResult) == 8); +typedef SignalBackupEntitlementFfiResult MaybeUninitOfBackupEntitlementFfiResult; +static_assert_64bit(sizeof(MaybeUninitOfBackupEntitlementFfiResult) == 16); +static_assert_64bit(alignof(MaybeUninitOfBackupEntitlementFfiResult) == 8); +typedef struct { + bool present; + MaybeUninitOfBackupEntitlementFfiResult value; +} SignalOptionalOfBackupEntitlementFfiResult; +static_assert_64bit(offsetof(SignalOptionalOfBackupEntitlementFfiResult, present) == 0); +static_assert_64bit(offsetof(SignalOptionalOfBackupEntitlementFfiResult, value) == 8); +static_assert_64bit(sizeof(SignalOptionalOfBackupEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalOptionalOfBackupEntitlementFfiResult) == 8); +typedef struct { + const int8_t* badge_id; + uint64_t expiration; + bool visible; +} SignalBadgeEntitlementFfiResult; +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, badge_id) == 0); +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, expiration) == 8); +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, visible) == 16); +static_assert_64bit(sizeof(SignalBadgeEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalBadgeEntitlementFfiResult) == 8); +typedef SignalBadgeEntitlementFfiResult* SignalType_MutPointer_SignalBadgeEntitlementFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalBadgeEntitlementFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalBadgeEntitlementFfiResult) == 8); +typedef struct { + SignalBadgeEntitlementFfiResult* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult) == 8); +typedef struct { + SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult badges; + SignalOptionalOfBackupEntitlementFfiResult backup; +} SignalAccountEntitlementsFfiResult; +static_assert_64bit(offsetof(SignalAccountEntitlementsFfiResult, badges) == 0); +static_assert_64bit(offsetof(SignalAccountEntitlementsFfiResult, backup) == 24); +static_assert_64bit(sizeof(SignalAccountEntitlementsFfiResult) == 48); +static_assert_64bit(alignof(SignalAccountEntitlementsFfiResult) == 8); +typedef const SignalAccountEntitlementsFfiResult* SignalType_ConstPointer_SignalAccountEntitlementsFfiResult; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalAccountEntitlementsFfiResult) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalAccountEntitlementsFfiResult) == 8); typedef SignalPairOfCStringPtrCStringPtr* SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr; static_assert_64bit(sizeof(SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr) == 8); @@ -2667,6 +2750,22 @@ static_assert_64bit(alignof(SignalCPromiseOwnedBuffer) == 8); typedef SignalCPromiseOwnedBuffer* SignalType_MutPointer_SignalCPromiseOwnedBuffer; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBuffer) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBuffer) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 24); +static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +typedef SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void) == 8); @@ -2699,6 +2798,22 @@ static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmed typedef SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 24); +static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +typedef SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void) == 8); @@ -2891,6 +3006,22 @@ static_assert_64bit(alignof(SignalCPromiseDeleteBackupMediaNextChunkFfiResult) = typedef SignalCPromiseDeleteBackupMediaNextChunkFfiResult* SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalAccountEntitlementsFfiResult, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseAccountEntitlementsFfiResult; +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseAccountEntitlementsFfiResult) == 24); +static_assert_64bit(alignof(SignalCPromiseAccountEntitlementsFfiResult) == 8); +typedef SignalCPromiseAccountEntitlementsFfiResult* SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalCurrencyConversionsInternalFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void) == 8); @@ -3610,6 +3741,7 @@ typedef enum { SignalErrorCodeMfaKeyNotFound = 237, SignalErrorCodeWebAuthnRegistrationUnsuccessful = 238, SignalErrorCodeProfileNotFound = 239, + SignalErrorCodeReceiptAlreadyRedeemed = 240, } SignalErrorCode; static_assert_64bit(sizeof(SignalErrorCode) == 4); static_assert_64bit(alignof(SignalErrorCode) == 4); @@ -3633,6 +3765,13 @@ enum SignalSvr2CredentialsResult { typedef uint8_t SignalSvr2CredentialsResult; static_assert_64bit(sizeof(SignalSvr2CredentialsResult) == 1); static_assert_64bit(alignof(SignalSvr2CredentialsResult) == 1); +typedef enum { + SignalBadgeVisibilityFfiArgDoNotSet, + SignalBadgeVisibilityFfiArgSetVisible, + SignalBadgeVisibilityFfiArgSetPrimary, +} SignalBadgeVisibilityFfiArg; +static_assert_64bit(sizeof(SignalBadgeVisibilityFfiArg) == 4); +static_assert_64bit(alignof(SignalBadgeVisibilityFfiArg) == 4); typedef enum { SignalBridgeMfaVerificationCredentialFfiArgTotp, SignalBridgeMfaVerificationCredentialFfiArgWebAuthn, @@ -3915,6 +4054,13 @@ SignalFfiError* signal_authenticated_chat_connection_connect( bool receive_stories, SignalBorrowedBytestringArray languages ); +SignalFfiError* signal_authenticated_chat_connection_create_donation_permits( + SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat, + SignalBorrowedBuffer request_context, + SignalConstPointerServerPublicParams server_params +); SignalFfiError* signal_authenticated_chat_connection_delete_account( SignalCPromisebool* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -3960,6 +4106,11 @@ SignalFfiError* signal_authenticated_chat_connection_generate_totp_key( SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat ); +SignalFfiError* signal_authenticated_chat_connection_get_capabilities( + SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat +); SignalFfiError* signal_authenticated_chat_connection_get_currency_conversions( SignalCPromiseCurrencyConversionsInternalFfiResult* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -3970,6 +4121,11 @@ SignalFfiError* signal_authenticated_chat_connection_get_devices( SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat ); +SignalFfiError* signal_authenticated_chat_connection_get_entitlements( + SignalCPromiseAccountEntitlementsFfiResult* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat +); SignalFfiError* signal_authenticated_chat_connection_get_pre_key_count( SignalCPromiseBridgePreKeyCountsFfiResult* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -4012,6 +4168,13 @@ SignalFfiError* signal_authenticated_chat_connection_redeem_backup_receipt( SignalConstPointerAuthenticatedChatConnection chat, const SignalType_FixedArray329_uint8_t* presentation ); +SignalFfiError* signal_authenticated_chat_connection_redeem_donation_receipt( + SignalCPromisebool* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat, + const SignalType_FixedArray329_uint8_t* presentation, + SignalBadgeVisibilityFfiArg visibility +); SignalFfiError* signal_authenticated_chat_connection_remove_device( SignalCPromisebool* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -4859,6 +5022,7 @@ SignalFfiError* signal_expiring_profile_key_credential_get_expiration_time( SignalFfiError* signal_expiring_profile_key_credential_response_check_valid_contents( SignalBorrowedBuffer buffer ); +void signal_ffi_version_0_106_1(void); SignalFfiError* signal_fingerprint_clone( SignalMutPointerFingerprint* new_obj, SignalConstPointerFingerprint obj diff --git a/pkg/libsignalgo/prekeystore.go b/pkg/libsignalgo/prekeystore.go index 8c3c36f..00ee50f 100644 --- a/pkg/libsignalgo/prekeystore.go +++ b/pkg/libsignalgo/prekeystore.go @@ -52,12 +52,8 @@ func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPoin //export signal_store_pre_key_callback func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error { - record := PreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StorePreKey(ctx, uint32(id), cloned) + record := wrapPreKeyRecord(preKeyRecord.raw) + return store.StorePreKey(ctx, uint32(id), record) }) } diff --git a/pkg/libsignalgo/profilekey.go b/pkg/libsignalgo/profilekey.go index 0c21aff..a39eaa5 100644 --- a/pkg/libsignalgo/profilekey.go +++ b/pkg/libsignalgo/profilekey.go @@ -46,24 +46,24 @@ func (pk *ProfileKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(pk)) } -func (pk *ProfileKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(pk.cFixedArray()) +func (pk *ProfileKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(pk)) } func (pkv *ProfileKeyVersion) cFixedArray() *C.SignalType_FixedArray64_uint8_t { return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(pkv)) } -func (pkv *ProfileKeyVersion) cConstFixedArray() cFixedArray64Compat { - return cFixedArray64Compat(pkv.cFixedArray()) +func (pkv *ProfileKeyVersion) cConstFixedArray() C.SignalType_ConstPointer_FixedArray64_uint8_t { + return (C.SignalType_ConstPointer_FixedArray64_uint8_t)(unsafe.Pointer(pkv)) } func (ak *AccessKey) cFixedArray() *C.SignalType_FixedArray16_uint8_t { return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(ak)) } -func (ak *AccessKey) cConstFixedArray() cFixedArray16Compat { - return cFixedArray16Compat(ak.cFixedArray()) +func (ak *AccessKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray16_uint8_t { + return (C.SignalType_ConstPointer_FixedArray16_uint8_t)(unsafe.Pointer(ak)) } func DeserializeProfileKey(bytes []byte) (*ProfileKey, error) { @@ -177,8 +177,8 @@ func (p *ProfileKeyCredentialRequestContext) cFixedArray() *C.SignalType_FixedAr return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(p)) } -func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() cFixedArray473Compat { - return cFixedArray473Compat(p.cFixedArray()) +func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() C.SignalType_ConstPointer_FixedArray473_uint8_t { + return (C.SignalType_ConstPointer_FixedArray473_uint8_t)(unsafe.Pointer(p)) } func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) { diff --git a/pkg/libsignalgo/senderkeystore.go b/pkg/libsignalgo/senderkeystore.go index 1649216..fc51571 100644 --- a/pkg/libsignalgo/senderkeystore.go +++ b/pkg/libsignalgo/senderkeystore.go @@ -40,7 +40,8 @@ type SenderKeyStore interface { //export signal_load_sender_key_callback func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSenderKeyRecord, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid) C.int { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { - record, err := store.LoadSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID))) + addr := wrapAddress(address.raw) + record, err := store.LoadSenderKey(ctx, addr, *(*uuid.UUID)(unsafe.Pointer(&distributionID))) if err == nil && record != nil { record.CancelFinalizer() recordp.raw = record.ptr @@ -52,13 +53,9 @@ func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp *C.SignalM //export signal_store_sender_key_callback func signal_store_sender_key_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid, senderKeyRecord C.SignalMutPointerSenderKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { - record := SenderKeyRecord{ptr: senderKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - - return store.StoreSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), cloned) + addr := wrapAddress(address.raw) + record := wrapSenderKeyRecord(senderKeyRecord.raw) + return store.StoreSenderKey(ctx, addr, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), record) }) } diff --git a/pkg/libsignalgo/serviceid.go b/pkg/libsignalgo/serviceid.go index 38631ee..a320319 100644 --- a/pkg/libsignalgo/serviceid.go +++ b/pkg/libsignalgo/serviceid.go @@ -188,6 +188,6 @@ func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t { return s.FixedBytes().cFixedArray() } -func (s ServiceID) cConstFixedArray() cFixedArray17Compat { - return cFixedArray17Compat(s.cFixedArray()) +func (s ServiceID) cConstFixedArray() C.SignalType_ConstPointer_FixedArray17_uint8_t { + return s.FixedBytes().cConstFixedArray() } diff --git a/pkg/libsignalgo/sessionstore.go b/pkg/libsignalgo/sessionstore.go index 99000e5..c33a899 100644 --- a/pkg/libsignalgo/sessionstore.go +++ b/pkg/libsignalgo/sessionstore.go @@ -38,7 +38,8 @@ type SessionStore interface { //export signal_load_session_callback func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSessionRecord, address C.SignalMutPointerProtocolAddress) C.int { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { - record, err := store.LoadSession(ctx, &Address{ptr: address.raw}) + addr := wrapAddress(address.raw) + record, err := store.LoadSession(ctx, addr) if err == nil && record != nil { record.CancelFinalizer() recordp.raw = record.ptr @@ -50,12 +51,9 @@ func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutP //export signal_store_session_callback func signal_store_session_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, sessionRecord C.SignalMutPointerSessionRecord) C.int { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { - record := SessionRecord{ptr: sessionRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreSession(ctx, &Address{ptr: address.raw}, cloned) + addr := wrapAddress(address.raw) + record := wrapSessionRecord(sessionRecord.raw) + return store.StoreSession(ctx, addr, record) }) } diff --git a/pkg/libsignalgo/signalversion/version.go b/pkg/libsignalgo/signalversion/version.go index b935b64..367a3d5 100644 --- a/pkg/libsignalgo/signalversion/version.go +++ b/pkg/libsignalgo/signalversion/version.go @@ -2,4 +2,4 @@ package signalversion -const Version = "v0.105.0" +const Version = "v0.106.1" diff --git a/pkg/libsignalgo/signedprekeystore.go b/pkg/libsignalgo/signedprekeystore.go index b1306e2..a66b9f4 100644 --- a/pkg/libsignalgo/signedprekeystore.go +++ b/pkg/libsignalgo/signedprekeystore.go @@ -50,12 +50,8 @@ func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.Signal //export signal_store_signed_pre_key_callback func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerSignedPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error { - record := SignedPreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreSignedPreKey(ctx, uint32(id), cloned) + record := wrapSignedPreKeyRecord(preKeyRecord.raw) + return store.StoreSignedPreKey(ctx, uint32(id), record) }) } diff --git a/pkg/libsignalgo/update-ffi-docker.sh b/pkg/libsignalgo/update-ffi-docker.sh index 73f8947..e15e7f6 100755 --- a/pkg/libsignalgo/update-ffi-docker.sh +++ b/pkg/libsignalgo/update-ffi-docker.sh @@ -1,9 +1,3 @@ #!/bin/bash docker run --rm -itv $(pwd):/data rust:1-alpine /data/update-ffi-docker-inner.sh - -echo "// Generated by update-ffi.sh; DO NOT EDIT." > signalversion/version.go -echo >> signalversion/version.go -echo "package signalversion" >> signalversion/version.go -echo >> signalversion/version.go -cd libsignal -echo "const Version = \"$(git describe --tags --always)\"" >> ../signalversion/version.go +./update-ffi-version.sh diff --git a/pkg/libsignalgo/update-ffi-version.sh b/pkg/libsignalgo/update-ffi-version.sh new file mode 100755 index 0000000..23ae12c --- /dev/null +++ b/pkg/libsignalgo/update-ffi-version.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +pushd libsignal + +LIBSIGNAL_TAG=$(git describe --tags --always) +LIBSIGNAL_VERSION_FUNC=$(echo $LIBSIGNAL_TAG | tr -d 'v' | sed 's/\./_/g') + +popd + +cat > signalversion/version.go < version.go < signalversion/version.go -echo >> signalversion/version.go -echo "package signalversion" >> signalversion/version.go -echo >> signalversion/version.go - # Store the current working directory ORIGINAL_DIR="$(pwd)" # Navigate to libsignal directory cd "$LIBSIGNAL_DIRECTORY" -echo "const Version = \"$(git describe --tags --always)\"" >> ../signalversion/version.go - # Build libsignal cargo build -p libsignal-ffi --release @@ -35,4 +28,6 @@ cd "$ORIGINAL_DIR" cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" . cp "${LIBSIGNAL_DIRECTORY}/swift/Sources/SignalFfi/signal_ffi.h" libsignal-ffi.h +./update-ffi-version.sh + echo "Files copied successfully." diff --git a/pkg/libsignalgo/version.go b/pkg/libsignalgo/version.go new file mode 100644 index 0000000..b5732cb --- /dev/null +++ b/pkg/libsignalgo/version.go @@ -0,0 +1,13 @@ +// Generated by update-ffi.sh; DO NOT EDIT. + +package libsignalgo + +/* +#include "./libsignal-ffi.h" +*/ +import "C" + +func init() { + // Compile time check to ensure the libsignal version matches + C.signal_ffi_version_0_106_1() +} diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index c54654c..f755bbb 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "strings" + "unicode/utf8" "github.com/rs/zerolog" "go.mau.fi/util/exmime" @@ -152,9 +153,33 @@ func (mc *MessageConverter) ToSignal( dm.BodyRanges = nil dm.Preview = nil } + if err := mc.uploadLongText(ctx, dm); err != nil { + return nil, err + } return dm, nil } +const maxInlineBodySize = 2 * 1024 + +func (mc *MessageConverter) uploadLongText(ctx context.Context, dm *signalpb.DataMessage) error { + body := dm.GetBody() + if len(body) <= maxInlineBodySize { + return nil + } + att, err := getClient(ctx).UploadAttachment(ctx, []byte(body)) + if err != nil { + return fmt.Errorf("%w (long text): %w", bridgev2.ErrMediaReuploadFailed, err) + } + att.ContentType = proto.String(LongTextContentType) + dm.Attachments = append([]*signalpb.AttachmentPointer{att}, dm.Attachments...) + cut := maxInlineBodySize + for !utf8.RuneStart(body[cut]) { + cut-- + } + dm.Body = proto.String(body[:cut]) + return nil +} + func maybeInt[T constraints.Integer](v T) *T { if v == 0 { return nil @@ -179,7 +204,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. if content.MSC3245Voice != nil && mime != "audio/aac" && ffmpeg.Supported() { data, err = ffmpeg.ConvertBytes(ctx, data, ".aac", []string{}, []string{"-c:a", "aac"}, mime) if err != nil { - return nil, err + return nil, fmt.Errorf("%w (voice to aac): %w", bridgev2.ErrMediaConvertFailed, err) } mime = "audio/aac" fileName += ".aac" @@ -189,7 +214,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. // allowed case "image/gif": if !ffmpeg.Supported() { - return nil, fmt.Errorf("converting gif stickers is not supported") + return nil, fmt.Errorf("%w: converting gif stickers is not supported", bridgev2.ErrUnsupportedMediaType) } data, err = ffmpeg.ConvertBytes(ctx, data, ".apng", []string{}, []string{}, mime) if err != nil { @@ -198,7 +223,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. fileName += ".apng" mime = "image/apng" default: - return nil, fmt.Errorf("unsupported content type for sticker %s", mime) + return nil, fmt.Errorf("%w for sticker: %s", bridgev2.ErrUnsupportedMediaType, mime) } } att, err := getClient(ctx).UploadAttachment(ctx, data) diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index 68f5558..2140db9 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -99,7 +99,9 @@ func (mc *MessageConverter) ToMatrix( } if dm.GetIsViewOnce() { var unavailableReason string - if len(dm.GetAttachments()) == 0 || sender == client.Store.ACI || + if sender == client.Store.ACI { + unavailableReason = "Sent a view-once message." + } else if len(dm.GetAttachments()) == 0 || !time.UnixMilli(int64(dm.GetTimestamp())).Add(45*24*time.Hour).After(time.Now()) { unavailableReason = "View-once media is no longer available." } else if len(dm.GetAttachments()) != 1 || dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil || @@ -151,6 +153,12 @@ func (mc *MessageConverter) ToMatrix( cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate)) return cm } + if dm.PinMessage != nil || dm.UnpinMessage != nil { + var part *bridgev2.ConvertedMessagePart + part, cm.ReplyTo = mc.convertPinToMatrix(dm) + cm.Parts = append(cm.Parts, part) + return cm + } if dm.GetIsViewOnce() { if !mc.Bridge.Config.ViewLimitedMedia && (cm.Disappear.Timer == 0 || cm.Disappear.Timer > 5*time.Minute) { cm.Disappear = database.DisappearingSetting{ @@ -173,7 +181,7 @@ func (mc *MessageConverter) ToMatrix( } } for i, att := range dm.GetAttachments() { - if att.GetContentType() != "text/x-signal-plain" || att.GetSize() > matrixTextMaxLength { + if att.GetContentType() != LongTextContentType || att.GetSize() > matrixTextMaxLength { part := mc.convertAttachmentToMatrix(ctx, i, att, attMap) if dm.GetIsViewOnce() { if part.Content.MsgType == event.MsgImage || part.Content.MsgType == event.MsgVideo { @@ -217,7 +225,7 @@ func (mc *MessageConverter) ToMatrix( for i, part := range cm.Parts { part.ID = signalid.MakeMessagePartID(i) meta := &signalid.MessageMetadata{ - ContainsAttachments: len(dm.GetAttachments()) > 0, + ContainsAttachments: ContainsAttachments(dm), ViewOnce: dm.GetIsViewOnce(), } if meta.ViewOnce { @@ -500,6 +508,11 @@ func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker, attMap AttachmentMap) *bridgev2.ConvertedMessagePart { converted, err := mc.reuploadAttachment(ctx, sticker.GetData(), attMap) + hasPack := len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) + if err != nil && hasPack && (errors.Is(err, signalmeow.ErrAttachmentNotFound) || errors.Is(err, ErrAttachmentNotInBackup) || errors.Is(err, ErrBackupNotSupported)) { + zerolog.Ctx(ctx).Debug().Err(err).Msg("Sticker attachment is unavailable, downloading it from the sticker pack") + converted, err = mc.reuploadStickerFromPack(ctx, sticker) + } if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker") return &bridgev2.ConvertedMessagePart{ @@ -517,7 +530,7 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker converted.Content.Info.Height = 200 } converted.Content.Body = sticker.GetEmoji() - if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) { + if hasPack { converted.Content.Info.BridgedSticker = &event.BridgedSticker{ Network: StickerSourceID, ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10), @@ -530,6 +543,49 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker return converted } +func (mc *MessageConverter) reuploadStickerFromPack(ctx context.Context, sticker *signalpb.DataMessage_Sticker) (*bridgev2.ConvertedMessagePart, error) { + att := sticker.GetData() + content := &event.MessageEventContent{ + Body: att.GetFileName(), + Info: &event.FileInfo{ + MimeType: att.GetContentType(), + Width: int(att.GetWidth()), + Height: int(att.GetHeight()), + }, + } + if mc.DirectMedia { + mediaID, err := signalid.DirectMediaSticker{ + PackID: sticker.GetPackId(), + PackKey: sticker.GetPackKey(), + StickerID: sticker.GetStickerId(), + }.AsMediaID() + if err != nil { + return nil, err + } + content.URL, content.File, err = mc.Bridge.GenerateContentURI(ctx, getPortal(ctx).MXID, mediaID) + if err != nil { + return nil, err + } + } else { + data, err := signalmeow.DownloadStickerPackItem(ctx, sticker.GetPackId(), sticker.GetPackKey(), sticker.GetStickerId()) + if err != nil { + return nil, fmt.Errorf("failed to download sticker from pack: %w", err) + } + if content.Info.MimeType == "" { + content.Info.MimeType = http.DetectContentType(data) + } + content.Info.Size = len(data) + content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, content.Body, content.Info.MimeType) + if err != nil { + return nil, fmt.Errorf("failed to upload sticker from pack: %w", err) + } + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: content, + }, nil +} + func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) { data, err := mc.downloadAttachment(ctx, att, attMap, nil) if err != nil { @@ -607,7 +663,7 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp if err != nil { return nil, err } - content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID) + content.URL, content.File, err = mc.Bridge.GenerateContentURI(ctx, getPortal(ctx).MXID, mediaID) } else { err = mc.actuallyReuploadAttachment(ctx, content, att, attMap) if err != nil { @@ -791,6 +847,28 @@ func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, se } } +func (mc *MessageConverter) convertPinToMatrix(dm *signalpb.DataMessage) (*bridgev2.ConvertedMessagePart, *networkid.MessageOptionalPartID) { + body := "Pinned a message" + targetAuthor, targetTS := dm.GetPinMessage().GetTargetAuthorAciBinary(), dm.GetPinMessage().GetTargetSentTimestamp() + if dm.PinMessage == nil { + body = "Unpinned a message" + targetAuthor, targetTS = dm.GetUnpinMessage().GetTargetAuthorAciBinary(), dm.GetUnpinMessage().GetTargetSentTimestamp() + } + var replyTo *networkid.MessageOptionalPartID + if len(targetAuthor) == 16 { + replyTo = &networkid.MessageOptionalPartID{ + MessageID: signalid.MakeMessageID(uuid.UUID(targetAuthor), targetTS), + } + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: body, + }, + }, replyTo +} + var invalidPollVote = &bridgev2.ConvertedMessagePart{ Type: event.EventUnstablePollResponse, Content: &event.MessageEventContent{}, diff --git a/pkg/msgconv/matrixfmt/html.go b/pkg/msgconv/matrixfmt/html.go index b60c0ad..9a54274 100644 --- a/pkg/msgconv/matrixfmt/html.go +++ b/pkg/msgconv/matrixfmt/html.go @@ -134,12 +134,14 @@ func JoinEntityString(with string, strings ...*EntityString) *EntityString { continue } DebugLog(" + %q %+v\n", s.String, s.Entities) + if len(str) > 0 { + str = append(str, withUTF16...) + } for _, entity := range s.Entities { entity.Start += len(str) entities = append(entities, entity) } str = append(str, s.String...) - str = append(str, withUTF16...) } DebugLog(" -> %q %+v\n", str, entities) return &EntityString{ diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go index 03bf621..2ec6ad0 100644 --- a/pkg/msgconv/msgconv.go +++ b/pkg/msgconv/msgconv.go @@ -18,6 +18,7 @@ package msgconv import ( "context" + "slices" "github.com/google/uuid" "maunium.net/go/mautrix/bridgev2" @@ -28,6 +29,7 @@ import ( "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt" "go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalmeow" + "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/signalpb" ) type contextKey int @@ -96,6 +98,14 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter { } } +const LongTextContentType = "text/x-signal-plain" + +func ContainsAttachments(dm *signalpb.DataMessage) bool { + return slices.ContainsFunc(dm.GetAttachments(), func(att *signalpb.AttachmentPointer) bool { + return att.GetContentType() != LongTextContentType + }) +} + func getClient(ctx context.Context) *signalmeow.Client { return ctx.Value(contextKeyClient).(*signalmeow.Client) } diff --git a/pkg/signalid/dbmeta.go b/pkg/signalid/dbmeta.go index 916aec5..0c35c0d 100644 --- a/pkg/signalid/dbmeta.go +++ b/pkg/signalid/dbmeta.go @@ -24,7 +24,8 @@ type PortalMetadata struct { Revision uint32 `json:"revision,omitempty"` ExpirationTimerVersion uint32 `json:"expiration_timer_version,omitempty"` // Lazy resync tracking - LastSync jsontime.Unix `json:"last_sync,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` + InfoVersion uint32 `json:"info_version,omitempty"` } type MessageMetadata struct { diff --git a/pkg/signalmeow/attachments.go b/pkg/signalmeow/attachments.go index 0b5f7e2..c4317ad 100644 --- a/pkg/signalmeow/attachments.go +++ b/pkg/signalmeow/attachments.go @@ -277,7 +277,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb } // Get upload attributes from Signal server - attributesPath := "/v4/attachments/form/upload" + attributesPath := fmt.Sprintf("/v4/attachments/form/upload?uploadLength=%d", len(encryptedWithMAC)) resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil) if err != nil { log.Err(err).Msg("Failed to request upload attributes") @@ -344,6 +344,7 @@ func (cli *Client) uploadAttachmentLegacy( ContentType: web.ContentTypeOctetStream, Username: &username, Password: &password, + Upload: true, }) web.CloseBody(resp) if err != nil { @@ -368,6 +369,7 @@ func (cli *Client) uploadAttachmentTUS( Body: encryptedWithMAC, ContentType: web.ContentTypeOffsetOctetStream, Headers: uploadAttributes.Headers, + Upload: true, }) web.CloseBody(resp) // TODO actually support resuming on error diff --git a/pkg/signalmeow/backup.go b/pkg/signalmeow/backup.go index f003e19..911a888 100644 --- a/pkg/signalmeow/backup.go +++ b/pkg/signalmeow/backup.go @@ -42,6 +42,7 @@ const transferArchiveFetchTimeout = 1 * time.Hour var ( ErrNoEphemeralBackupKey = errors.New("no ephemeral backup key") + ErrTransferTimedOut = errors.New("timed out waiting for transfer archive") ) const ( @@ -271,8 +272,8 @@ func (cli *Client) WaitForTransfer(ctx context.Context) (*TransferArchiveMetadat for { remainingTime := time.Until(timeout) - if remainingTime < 0 { - return nil, fmt.Errorf("timed out") + if remainingTime < time.Second { + return nil, ErrTransferTimedOut } reqStart := time.Now() reqTimeout := min(remainingTime, 5*time.Minute) diff --git a/pkg/signalmeow/client.go b/pkg/signalmeow/client.go index c8898e3..fad3f8e 100644 --- a/pkg/signalmeow/client.go +++ b/pkg/signalmeow/client.go @@ -72,6 +72,8 @@ type Client struct { storageSyncLock sync.Mutex storageSyncQueued atomic.Bool + storageVersion uint64 + storageRecords []string writeCallbackCounter chan time.Time } diff --git a/pkg/signalmeow/groupcache.go b/pkg/signalmeow/groupcache.go index ee8259f..3a676fb 100644 --- a/pkg/signalmeow/groupcache.go +++ b/pkg/signalmeow/groupcache.go @@ -203,6 +203,7 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi delete(gc.data, id) return nil } + prevMembers := cached.getMemberServiceIDs() // Pending member adds, promotes and removes cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...) @@ -304,6 +305,9 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi if change.ModifyAnnouncementsOnly != nil { cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly } + if change.TerminateGroup { + cached.Terminated = true + } if change.ModifyDisappearingMessagesDuration != nil { cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration } @@ -319,6 +323,12 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi cached.UpdatedAt = time.Now() cached.Revision = change.Revision + if len(endorsementResponse) == 0 { + if !slices.Equal(prevMembers, cached.getMemberServiceIDs()) { + delete(gc.data, id) + } + return nil + } endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs( cached.getMemberServiceIDs(), gc.serviceID, diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 493ecbb..882e048 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -81,6 +81,7 @@ type Group struct { Members []*GroupMember Description string AnnouncementsOnly bool + Terminated bool Revision uint32 DisappearingMessagesDuration uint32 AccessControl *GroupAccessControl @@ -206,6 +207,7 @@ type GroupChange struct { DeleteBannedMembers []*libsignalgo.ServiceID PromotePendingPniAciMembers []*PromotePendingPniAciMember ModifyInviteLinkPassword *types.SerializedInviteLinkPassword + TerminateGroup bool } func (groupChange *GroupChange) isEmpty() bool { @@ -226,7 +228,8 @@ func (groupChange *GroupChange) isEmpty() bool { len(groupChange.PromoteRequestingMembers) == 0 && groupChange.ModifyDescription == nil && groupChange.ModifyAnnouncementsOnly == nil && - len(groupChange.AddBannedMembers) == 0 + len(groupChange.AddBannedMembers) == 0 && + !groupChange.TerminateGroup } func (groupChange *GroupChange) resolveConflict(group *Group) { @@ -497,6 +500,8 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast // These aren't encrypted decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.Revision = encryptedGroup.Version + decryptedGroup.Terminated = encryptedGroup.Terminated + decryptedGroup.AnnouncementsOnly = encryptedGroup.AnnouncementsOnly // Decrypt members for _, member := range encryptedGroup.Members { @@ -707,7 +712,7 @@ func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, g return nil, fmt.Errorf("failed to send request: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected response status: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } encryptedAvatar, err := io.ReadAll(resp.Body) if err != nil { @@ -1076,6 +1081,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange if encryptedActions.ModifyAnnouncementsOnly != nil { decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly } + decryptedGroupChange.TerminateGroup = encryptedActions.TerminateGroup != nil if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 { timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer) if err != nil { @@ -1801,8 +1807,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent "Cached-Send-Endorsements": "0", }, } - // highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false - path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState) + path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 7, includeFirstState) response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts) defer web.CloseBody(response) if err != nil { diff --git a/pkg/signalmeow/keys.go b/pkg/signalmeow/keys.go index 84a242a..1b2370a 100644 --- a/pkg/signalmeow/keys.go +++ b/pkg/signalmeow/keys.go @@ -64,6 +64,13 @@ func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore return fmt.Errorf("failed to get all kyber prekeys: %w", err) } + if len(preKeys) > PREKEY_BATCH_SIZE { + preKeys = preKeys[len(preKeys)-PREKEY_BATCH_SIZE:] + } + if len(kyberPreKeys) > PREKEY_BATCH_SIZE { + kyberPreKeys = kyberPreKeys[len(kyberPreKeys)-PREKEY_BATCH_SIZE:] + } + // We need to have some keys to upload if len(preKeys) == 0 && len(kyberPreKeys) == 0 { return fmt.Errorf("no prekeys to upload") @@ -408,6 +415,7 @@ func addBase64PaddingAndDecode(data string) ([]byte, error) { var ( ErrUnregisteredUser = errors.New("user is unregistered") ErrDevicesChanged = errors.New("device list changed while sending skdm") + ErrRateLimited = errors.New("rate limited by server") ) func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID libsignalgo.ServiceID, specificDeviceID int) error { diff --git a/pkg/signalmeow/profile.go b/pkg/signalmeow/profile.go index a515f5a..a0878b6 100644 --- a/pkg/signalmeow/profile.go +++ b/pkg/signalmeow/profile.go @@ -289,7 +289,7 @@ func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, pr } defer web.CloseBody(resp) if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected response status: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } encryptedAvatar, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index f0d69df..dcc748a 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -154,7 +154,10 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection case <-loopCtx.Done(): log.Info().Msg("Context done, exiting websocket status loop") return - case status := <-authChan: + case status, ok := <-authChan: + if !ok { + return + } lastAuthStatus = status currentStatus = status @@ -182,7 +185,11 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection default: } } - case status := <-unauthChan: + case status, ok := <-unauthChan: + if !ok { + unauthChan = nil + continue + } lastUnauthStatus = status currentStatus = status @@ -371,6 +378,19 @@ func (cli *Client) incomingAPIMessageHandler(ctx context.Context, req *signalpb. Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]). Msg("Received envelope") + if envelope.GetType() == signalpb.Envelope_SERVER_DELIVERY_RECEIPT { + if sourceServiceID.Type == libsignalgo.ServiceIDTypeACI && sourceServiceID.UUID != uuid.Nil && !cli.handleEvent(&events.Receipt{ + Sender: sourceServiceID.UUID, + Content: &signalpb.ReceiptMessage{ + Type: signalpb.ReceiptMessage_DELIVERY.Enum(), + Timestamp: []uint64{envelope.GetClientTimestamp()}, + }, + }) { + return nil, ErrHandlerFailed + } + return &web.SimpleResponse{Status: 200}, nil + } + result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID) err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID) @@ -644,7 +664,7 @@ func (cli *Client) handleDecryptedResult( }) } case *signalpb.Content_CallMessage: - if !isBlocked && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) { + if !isBlocked && (content.CallMessage.Offer != nil || (content.CallMessage.Hangup != nil && content.CallMessage.Hangup.GetType() != signalpb.CallMessage_Hangup_HANGUP_ACCEPTED)) { handlerSuccess = cli.handleEvent(&events.Call{ Info: events.MessageInfo{ Sender: theirServiceID.UUID, @@ -902,7 +922,7 @@ func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsign Stringer("aci", sender.UUID). Stringer("pni", pni). Msg("Fetching PNI identity for signature verification as it wasn't found in store") - err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 0) + err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 1) if err != nil { return fmt.Errorf("failed to fetch prekey for PNI %s after identity wasn't found in store: %w", pni, err) } else if pniIdentity, err = cli.Store.IdentityKeyStore.GetIdentityKey(ctx, pniServiceID); err != nil { diff --git a/pkg/signalmeow/receiving_decrypt.go b/pkg/signalmeow/receiving_decrypt.go index f92647c..8ad3e15 100644 --- a/pkg/signalmeow/receiving_decrypt.go +++ b/pkg/signalmeow/receiving_decrypt.go @@ -71,12 +71,15 @@ func (cli *Client) decryptEnvelope( } var result *DecryptionResult var bundleType string + var ciphertextType libsignalgo.CiphertextMessageType if *envelope.Type == signalpb.Envelope_PREKEY_MESSAGE { result, err = cli.prekeyDecrypt(ctx, destinationServiceID, sender, envelope.Content, envelope.GetServerTimestamp()) bundleType = "prekey bundle" + ciphertextType = libsignalgo.CiphertextMessageTypePreKey } else { result, err = cli.decryptCiphertextEnvelope(ctx, destinationServiceID, sender, envelope.Content, envelope.GetServerTimestamp()) bundleType = "ciphertext" + ciphertextType = libsignalgo.CiphertextMessageTypeWhisper } if err != nil { return DecryptionResult{ @@ -84,7 +87,7 @@ func (cli *Client) decryptEnvelope( Err: fmt.Errorf("failed to decrypt %s envelope: %w", bundleType, err), Retriable: true, // TODO should these ever be not retriable? Ciphertext: envelope.Content, - CiphertextType: libsignalgo.CiphertextMessageType(envelope.GetType()), + CiphertextType: ciphertextType, } } return *result @@ -104,9 +107,6 @@ func (cli *Client) decryptEnvelope( Unencrypted: true, } - case signalpb.Envelope_SERVER_DELIVERY_RECEIPT: - return DecryptionResult{Err: fmt.Errorf("server delivery receipt envelopes are not yet supported")} - case signalpb.Envelope_UNKNOWN: return DecryptionResult{Err: fmt.Errorf("unknown envelope type")} diff --git a/pkg/signalmeow/senderkey.go b/pkg/signalmeow/senderkey.go index 344bacf..322fcc8 100644 --- a/pkg/signalmeow/senderkey.go +++ b/pkg/signalmeow/senderkey.go @@ -221,8 +221,8 @@ func (cli *Client) sendToGroupWithSenderKey( header.Set("Unidentified-Access-Key", xak.String()) } path := fmt.Sprintf( - "/v1/messages/multi_recipient?ts=%d&urgent=%t&online=false", - messageTimestamp, isUrgent(content), + "/v1/messages/multi_recipient?ts=%d&urgent=%t&online=%t", + messageTimestamp, isUrgent(content), content.GetTypingMessage() != nil, ) log.Debug(). Any("recipients", ski.SharedWith). @@ -249,7 +249,7 @@ func (cli *Client) sendToGroupWithSenderKey( cli.Store.RecipientStore.MarkUnregistered(ctx, serviceID, true) result.FailedToSendTo = append(result.FailedToSendTo, FailedSendResult{ Recipient: serviceID, - Error: fmt.Errorf("multi-recipient send 404"), + Error: fmt.Errorf("%w (multi-recipient send returned 404)", ErrUnregisteredUser), }) } else { result.SuccessfullySentTo = append(result.SuccessfullySentTo, SuccessfulSendResult{ @@ -277,6 +277,13 @@ func (cli *Client) sendToGroupWithSenderKey( doUnlock() // Retry recursively after fixing device lists return cli.sendToGroupWithSenderKey(ctx, groupID, allRecipients, sec, content, messageTimestamp, retries+1) + case 413, 428, 429: + return nil, fmt.Errorf("%w (multi-recipient send returned %d)", ErrRateLimited, resp.GetStatus()) + case 500, 503: + log.Warn().Uint32("status_code", resp.GetStatus()). + Msg("Multi-recipient send failed with server error, retrying") + doUnlock() + return cli.sendToGroupWithSenderKey(ctx, groupID, allRecipients, sec, content, messageTimestamp, retries+1) default: return nil, fmt.Errorf("unexpected status code %d in multi-recipient send", resp.GetStatus()) } diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index f48a252..dce4eea 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -297,6 +297,20 @@ type GroupMessageSendResult struct { FailedToSendTo []FailedSendResult } +// FailureError returns an error from FailedToSendTo, preferring errors other than ErrUnregisteredUser, +// so that a permanent failure is only reported if every recipient failed permanently. +func (r *GroupMessageSendResult) FailureError() error { + if len(r.FailedToSendTo) == 0 { + return nil + } + for _, failure := range r.FailedToSendTo { + if !errors.Is(failure.Error, ErrUnregisteredUser) { + return failure.Error + } + } + return r.FailedToSendTo[len(r.FailedToSendTo)-1].Error +} + type SendResult interface { isSendResult() } @@ -658,8 +672,7 @@ func (cli *Client) sendToGroup( return result, nil // I only sent to myself } if len(result.SuccessfullySentTo) == 0 { - lastError := result.FailedToSendTo[len(result.FailedToSendTo)-1].Error - return nil, fmt.Errorf("failed to send to any group members: %w", lastError) + return nil, fmt.Errorf("failed to send to any group members: %w", result.FailureError()) } return result, nil @@ -951,7 +964,7 @@ func (cli *Client) sendContent( outgoingMessages := MyMessages{ Timestamp: messageTimestamp, - Online: false, + Online: content.GetTypingMessage() != nil, Urgent: isUrgent(content), Messages: messages, } @@ -1037,6 +1050,8 @@ func (cli *Client) sendContent( } cli.Store.RecipientStore.MarkUnregistered(ctx, recipient, true) return sentUnidentified, fmt.Errorf("%w (send returned 404)", ErrUnregisteredUser) + } else if *response.Status == 413 || *response.Status == 429 { + return sentUnidentified, fmt.Errorf("%w (send returned %d)", ErrRateLimited, *response.Status) } else if *response.Status != 200 { return sentUnidentified, fmt.Errorf("unexpected status code while sending: %d", *response.Status) } @@ -1189,7 +1204,7 @@ func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceI // } // } //} - return fmt.Errorf("got 428 error") + return fmt.Errorf("%w (send returned 428, retry after %ds)", ErrRateLimited, retryAfterSeconds) } func (cli *Client) ReportSpam(ctx context.Context, aci uuid.UUID, serverGUID string, token []byte) error { diff --git a/pkg/signalmeow/sticker.go b/pkg/signalmeow/sticker.go index e1364b1..f50fafc 100644 --- a/pkg/signalmeow/sticker.go +++ b/pkg/signalmeow/sticker.go @@ -72,7 +72,7 @@ func downloadStickerData(ctx context.Context, path string, packKey []byte) ([]by if err != nil { return nil, fmt.Errorf("failed to make request: %w", err) } else if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected status code: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } else if body, err = io.ReadAll(resp.Body); err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } else if decrypted, err = decryptSticker(packKey, body); err != nil { diff --git a/pkg/signalmeow/storageservice.go b/pkg/signalmeow/storageservice.go index cfc2951..f3b9aac 100644 --- a/pkg/signalmeow/storageservice.go +++ b/pkg/signalmeow/storageservice.go @@ -55,18 +55,30 @@ func (cli *Client) StorageSync(ctx context.Context) { cli.storageSyncQueued.Store(false) log := cli.Log.With().Str("action", "sync storage").Logger() ctx = log.WithContext(ctx) - // TODO only fetch changed entries - update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, 0, nil) + update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, cli.storageVersion, cli.storageRecords) if err != nil { log.Err(err).Msg("Failed to fetch storage") return + } else if update == nil { + log.Debug().Uint64("version", cli.storageVersion).Msg("Storage manifest is already up to date") + return } err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error { return cli.processStorageInTxn(ctx, update) }) if err != nil { log.Err(err).Msg("Failed to process storage update") + return } + log.Debug(). + Uint64("previous_version", cli.storageVersion). + Uint64("version", update.Version). + Int("new_records", len(update.NewRecords)). + Int("removed_records", len(update.RemovedRecords)). + Int("missing_records", len(update.MissingRecords)). + Msg("Processed storage update") + cli.storageVersion = update.Version + cli.storageRecords = update.KnownRecords } func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdate) error { @@ -101,8 +113,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat recipient.Profile.Name = strings.TrimSpace(fmt.Sprintf("%s %s", contact.GivenName, contact.FamilyName)) } if contact.SystemGivenName != "" || contact.SystemFamilyName != "" { - changed = true - recipient.ContactName = strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName)) + newContactName := strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName)) + changed = changed || recipient.ContactName != newContactName + recipient.ContactName = newContactName } newNickname := "" if contact.Nickname != nil { @@ -120,7 +133,7 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat changed = true recipient.Blocked = contact.Blocked } - if !ptr.Val(recipient.Whitelisted) { + if !ptr.Val(recipient.Whitelisted) && (recipient.Whitelisted == nil || contact.Whitelisted) { changed = true recipient.Whitelisted = &contact.Whitelisted } @@ -158,7 +171,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat return fmt.Errorf("failed to save device after receiving account record: %w", err) } log.Debug().Msg("Saved device after receiving account record") - case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList: + case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList, *signalpb.StorageRecord_StickerPack, + *signalpb.StorageRecord_CallLink, *signalpb.StorageRecord_ChatFolder, *signalpb.StorageRecord_NotificationProfile, + *signalpb.StorageRecord_FavoriteSticker: // irrelevant data default: log.Warn().Type("type", data).Str("item_id", record.StorageID).Msg("Unknown storage record type") @@ -178,6 +193,7 @@ type StorageUpdate struct { NewRecords []*DecryptedStorageRecord RemovedRecords []string MissingRecords []string + KnownRecords []string } func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVersion uint64, existingKeys []string) (*StorageUpdate, error) { @@ -189,6 +205,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe return nil, nil } removedKeys := make([]string, 0) + knownKeys := make([]string, 0, len(manifest.GetIdentifiers())) newKeys := manifestRecordToMap(manifest.GetIdentifiers()) slices.Sort(existingKeys) existingKeys = slices.Compact(existingKeys) @@ -196,6 +213,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe _, isStillThere := newKeys[key] if isStillThere { delete(newKeys, key) + knownKeys = append(knownKeys, key) } else { removedKeys = append(removedKeys, key) } @@ -205,11 +223,15 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe if err != nil { return nil, err } + for _, record := range newRecords { + knownKeys = append(knownKeys, record.StorageID) + } return &StorageUpdate{ Version: manifest.GetVersion(), NewRecords: newRecords, RemovedRecords: removedKeys, MissingRecords: missingKeys, + KnownRecords: knownKeys, }, nil } diff --git a/pkg/signalmeow/store/prekey_store.go b/pkg/signalmeow/store/prekey_store.go index 37bca7e..2cf62a5 100644 --- a/pkg/signalmeow/store/prekey_store.go +++ b/pkg/signalmeow/store/prekey_store.go @@ -51,13 +51,13 @@ type PreKeyStore interface { } const ( - getAllPreKeysQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3` + getAllPreKeysQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3 ORDER BY key_id` getPreKeyQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3 AND is_signed=$4` insertPreKeyQuery = `INSERT INTO signalmeow_pre_keys (account_id, service_id, key_id, is_signed, key_pair) VALUES ($1, $2, $3, $4, $5)` deletePreKeyQuery = `DELETE FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3 AND is_signed=$4` getLastPreKeyIDQuery = `SELECT COUNT(*), COALESCE(MAX(key_id), 0) FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3` - getAllKyberPreKeysQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_last_resort=false` + getAllKyberPreKeysQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_last_resort=false ORDER BY key_id` getKyberPreKeyQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3` insertKyberPreKeyQuery = `INSERT INTO signalmeow_kyber_pre_keys (account_id, service_id, key_id, key_pair, is_last_resort) VALUES ($1, $2, $3, $4, $5)` deleteKyberPreKeyQuery = `DELETE FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3` diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index 3231322..2ee9ac8 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/http" "net/url" "strings" @@ -116,6 +117,9 @@ func (s *SignalWebsocket) Close() (err error) { s.closeCalled.Store(true) if ws := s.ws.Swap(nil); ws != nil { err = ws.Close(websocket.StatusNormalClosure, "") + if errors.Is(err, net.ErrClosed) { + err = nil + } } if cancelLoop := s.cancel.Swap(nil); cancelLoop != nil { (*cancelLoop)() @@ -222,10 +226,12 @@ func (s *SignalWebsocket) connectLoop( return } if request == nil { - log.Fatal().Msg("Received nil request") + log.Error().Msg("Received nil request") + continue } if requestHandler == nil { - log.Fatal().Msg("Received request but no handler") + log.Error().Uint64("request_id", request.GetId()).Msg("Received request but no handler") + continue } // Handle the request with the request handler function @@ -254,6 +260,7 @@ func (s *SignalWebsocket) connectLoop( retrying := false errorCount := 0 isFirstConnect := true + var connectedAt time.Time wsURL := (&url.URL{ Scheme: "wss", Host: APIHostname, @@ -271,7 +278,7 @@ func (s *SignalWebsocket) connectLoop( case <-ctx.Done(): } backoff += backoffIncrement - } else if !isFirstConnect && s.basicAuth != nil { + } else if !isFirstConnect && (s.basicAuth != nil || time.Since(connectedAt) < time.Minute) { select { case <-time.After(initialBackoff): case <-ctx.Done(): @@ -287,20 +294,17 @@ func (s *SignalWebsocket) connectLoop( if resp != nil { if resp.StatusCode != 101 { // Server didn't want to open websocket - if resp.StatusCode >= 500 { - // We can try again if it's a 5xx - s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, fmt.Errorf("5xx opening websocket: %v", resp.Status)) - } else if resp.StatusCode == 403 { + statusErr := HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status} + if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests { + // We can try again if it's a 5xx or a rate limit + s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, fmt.Errorf("error opening websocket: %w", statusErr)) + } else if resp.StatusCode == http.StatusForbidden || (resp.StatusCode == http.StatusUnauthorized && s.basicAuth != nil) { // We are logged out, so we should stop trying to reconnect - s.pushStatus(ctx, SignalWebsocketConnectionEventLoggedOut, fmt.Errorf("403 opening websocket, we are logged out")) - return // NOT RETRYING, KILLING THE CONNECTION LOOP - } else if resp.StatusCode > 0 && resp.StatusCode < 500 { - // Unexpected status code - s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("unexpected status opening websocket: %v", resp.Status)) + s.pushStatus(ctx, SignalWebsocketConnectionEventLoggedOut, fmt.Errorf("%w opening websocket, we are logged out", statusErr)) return // NOT RETRYING, KILLING THE CONNECTION LOOP } else { - // Something is very wrong - s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected error opening websocket: %v", resp.Status)) + // Unexpected status code + s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected status opening websocket: %w", statusErr)) } // Retry the connection retrying = true @@ -319,10 +323,16 @@ func (s *SignalWebsocket) connectLoop( } // Succssfully connected + if resp != nil { + if alerts := resp.Header.Values("X-Signal-Alert"); len(alerts) > 0 { + log.Warn().Strs("alerts", alerts).Msg("Signal server sent alerts when opening websocket") + } + } s.pushStatus(ctx, SignalWebsocketConnectionEventConnected, nil) s.ws.Store(ws) retrying = false backoff = initialBackoff + connectedAt = time.Now() responseChannels := exsync.NewMap[uint64, chan *signalpb.WebSocketResponseMessage]() loopCtx, loopCancel := context.WithCancelCause(ctx) @@ -402,12 +412,16 @@ func (s *SignalWebsocket) connectLoop( if ctxCauseErr == nil || errors.Is(ctxCauseErr, context.Canceled) { s.pushStatus(ctx, SignalWebsocketConnectionEventCleanShutdown, nil) } else { - errorCount++ + if time.Since(connectedAt) > time.Minute { + errorCount = 0 + } s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, ctxCauseErr) if errors.Is(ctxCauseErr, ErrForcedReconnect) { // Skip the delay for forced reconnects // TODO should the delay be lowered globally? isFirstConnect = true + } else { + errorCount++ } } @@ -420,10 +434,12 @@ func (s *SignalWebsocket) connectLoop( wg.Wait() log.Debug().Msg("Finished websocket cleanup") if errorCount > 500 { - // Something is really wrong, we better panic. + // Something is really wrong, stop reconnecting. // This is a last defense against a runaway error loop, // like the WS continually closing and reconnecting - log.Fatal().Int("error_count", errorCount).Msg("Too many errors, panicking") + log.Error().Int("error_count", errorCount).Msg("Too many errors, stopping connection loop") + s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("too many websocket errors (%d)", errorCount)) + return } } } @@ -464,13 +480,17 @@ func readLoop( Str("request_verb", *msg.Request.Verb). Str("request_path", *msg.Request.Path). Msg("Received WS request") - incomingRequestChan <- msg.Request + select { + case incomingRequestChan <- msg.Request: + case <-ctx.Done(): + return ctx.Err() + } } else if *msg.Type == signalpb.WebSocketMessage_RESPONSE { if msg.Response == nil { - log.Fatal().Msg("Received response with no response") + return errors.New("received response message with no response") } if msg.Response.Id == nil { - log.Fatal().Msg("Received response with no id") + return errors.New("received response message with no id") } responseChannel, ok := responseChannels.Pop(*msg.Response.Id) if !ok { @@ -482,9 +502,9 @@ func readLoop( logEvt := log.Debug(). Uint64("response_id", msg.Response.GetId()). Uint32("response_status", msg.Response.GetStatus()). - Str("response_message", msg.Response.GetMessage()) - if log.GetLevel() == zerolog.TraceLevel || len(msg.Response.Body) < 256 { - logEvt.Strs("response_headers", msg.Response.Headers) + Str("response_message", msg.Response.GetMessage()). + Strs("response_headers", msg.Response.Headers) + if log.GetLevel() == zerolog.TraceLevel || msg.Response.GetStatus() >= 400 { if json.Valid(msg.Response.Body) { logEvt.RawJSON("response_body", msg.Response.Body) } else { @@ -678,6 +698,7 @@ func OpenWebsocket(ctx context.Context, url string) (*websocket.Conn, *http.Resp func CreateWSResponse(ctx context.Context, id uint64, status int) *signalpb.WebSocketMessage { if status != 200 && status != 400 { // TODO support more responses to Signal? Are there more? + //bridgelint:ignore GR004 statuses come from the bridge's own handlers, so anything else is a bridge bug zerolog.Ctx(ctx).Fatal().Int("status", status).Msg("Error creating response. Non 200/400 not supported yet.") return nil } diff --git a/pkg/signalmeow/web/web.go b/pkg/signalmeow/web/web.go index d30e367..fc86457 100644 --- a/pkg/signalmeow/web/web.go +++ b/pkg/signalmeow/web/web.go @@ -28,6 +28,7 @@ import ( "net/http" "runtime" "strings" + "sync/atomic" "time" "github.com/rs/zerolog" @@ -74,6 +75,9 @@ var signalTransport = &http.Transport{ var SignalHTTPClient = &http.Client{ Transport: signalTransport, } +var SignalUploadHTTPClient = &http.Client{ + Transport: signalTransport, +} func init() { SignalCertPool.AddCert(exerrors.Must(x509.ParseCertificate(signalRootCertBytes))) @@ -97,9 +101,10 @@ type HTTPReqOpt struct { ContentType ContentType Headers map[string]string OverrideURL string // Override the full URL, if set ignores path and Host + Upload bool } -var httpReqCounter = 0 +var httpReqCounter atomic.Int64 func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPReqOpt) (*http.Response, error) { // Set defaults @@ -143,11 +148,14 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe req.SetBasicAuth(*opt.Username, *opt.Password) } - httpReqCounter++ - log = log.With().Int("request_number", httpReqCounter).Logger() + log = log.With().Int64("request_number", httpReqCounter.Add(1)).Logger() log.Trace().Msg("Sending HTTP request") + client := SignalHTTPClient + if opt.Upload { + client = SignalUploadHTTPClient + } start := time.Now() - resp, err := SignalHTTPClient.Do(req) + resp, err := client.Do(req) dur := time.Since(start) if err != nil { log.Err(err).Dur("duration", dur).Msg("Error sending request") @@ -157,6 +165,16 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe return resp, nil } +// HTTPStatusError is returned when a Signal server responds with an unexpected HTTP status. +type HTTPStatusError struct { + StatusCode int + Status string +} + +func (e HTTPStatusError) Error() string { + return e.Status +} + func CloseBody(resp *http.Response) { if resp != nil && resp.Body != nil { _ = resp.Body.Close() @@ -242,9 +260,8 @@ func GetAttachment(ctx context.Context, path string, cdnNumber uint32) (*http.Re req.Header.Add("Content-Type", "application/octet-stream") req.Header.Set("User-Agent", UserAgent) - httpReqCounter++ log = log.With(). - Int("request_number", httpReqCounter). + Int64("request_number", httpReqCounter.Add(1)). Str("url", urlStr). Logger()