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()