Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-11-11 16:49:17 -05:00

Compare commits

..
Author SHA1 Message Date
Gerardo Rodriguez
a96f214609
groupinfo,signalmeow: make ended groups read-only (#725) 2026-11-11 02:36:47 +00:00
Gerardo Rodriguez
561064e8f5
handlematrix: send admin deletes for other members' messages in groups (#722) 2026-11-11 02:36:43 +00:00
Gerardo Rodriguez
03c8d29ae7
signalmeow/receiving: fetch PNI identity from the primary device when verifying PNI signatures (#720) 2026-11-11 02:36:40 +00:00
Gerardo Rodriguez
59a4ff2afc
msgconv/from-signal: send notice when a message is pinned/unpinned (#724) 2026-11-10 23:18:34 +00:00
Gerardo Rodriguez
27e3fc2582
signalmeow/storageservice: fix unnecessarily marking recipients as changed (#712) 2026-11-10 23:18:23 +00:00
Gerardo Rodriguez
15674cc755
signalmeow/groupcache: keep send endorsements for group changes without a response (#714) 2026-11-10 23:18:13 +00:00
Gerardo Rodriguez
fa622888a9
client: don't connect logins that are already logged out (#713) 2026-11-10 23:18:04 +00:00
Gerardo Rodriguez
d59791e7d3
signalmeow: send typing messages as online-only (#716) 2026-11-10 23:18:01 +00:00
Gerardo Rodriguez
cd8bc557d6
signalmeow/storageservice: ignore record types the bridge doesn't use (#717) 2026-11-10 23:17:56 +00:00
Gerardo Rodriguez
5c740406db
signalmeow: handle server delivery receipts as delivery receipts (#718) 2026-11-10 23:17:50 +00:00
Gerardo Rodriguez
c6abc27db6
signalmeow/storageservice: only fetch new records after the first sync (#719) 2026-11-10 23:17:39 +00:00
Gerardo Rodriguez
0a4f1d81c8
signalmeow/web: don't return an error when closing an already closed websocket (#721) 2026-11-10 23:17:31 +00:00
Gerardo Rodriguez
74b4c70ee4
handlesignal: send a notice in DMs when a safety number changes (#723) 2026-11-10 23:16:58 +00:00
Gerardo Rodriguez
8cb696dcfc
signalmeow/groups: decode announcements-only flag in full group state (#727) 2026-11-10 23:04:39 +00:00
indent[bot]
d451962ba2
msgconv: download stickers from their pack when the attachment is gone (#708)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:42:33 +00:00
indent[bot]
c827c97adb
chatinfo: fall back to profile avatar when contact avatar isn't loaded (#710)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:41:17 +00:00
indent[bot]
c820321548
signalmeow/websocket: log alerts sent by the server when connecting (#709)
Co-authored-by: Indent <noreply@indent.com>
2026-11-09 17:34:19 +00:00
indent[bot]
34b4187e07
chatinfo: stop listing contacts after request cancellation (#705)
Co-authored-by: Indent <noreply@indent.com>
2026-11-05 15:33:19 +00:00
Nick Mills-Barrett
a630835d70
pre-commit: install bridgelint with the CI Go toolchain (#706) 2026-11-05 13:09:30 +00:00
indent[bot]
7501c40cba
handlesignal: resync group info once for portals synced before reaction overrides (#702)
Co-authored-by: Indent <noreply@indent.com>
2026-11-04 19:17:03 -05:00
Gerardo Rodriguez
e2e994f99f
signalmeow/attachments: send uploadLength when requesting upload form (#703) 2026-11-05 00:28:39 +02:00
Nick Mills-Barrett
b0346e6698
pre-commit: add bridgelint hook (#701) 2026-11-03 21:36:55 +00:00
Tulir Asokan
020738bc24 changelog: update 2026-11-02 21:59:48 +02:00
Tulir Asokan
403c050d3b libsignal: update to v0.106.1 2026-11-02 21:59:48 +02:00
indent[bot]
4b5f4f74ca
groupinfo: allow reactions in announcement-only groups (#692)
Co-authored-by: Indent <noreply@indent.com>
2026-11-02 09:28:08 -05:00
indent[bot]
a82a7f38a4
signalmeow/web: use a longer response header timeout for attachment uploads (#695)
Co-authored-by: Indent <noreply@indent.com>
2026-11-01 13:16:23 -05:00
indent[bot]
d3e8d560ba
chatsync: stop waiting for transfer archive after it times out (#698)
Co-authored-by: Indent <noreply@indent.com>
2026-11-01 13:12:33 -05:00
indent[bot]
9dbbb6e4ea
signalmeow/keys: upload at most 100 prekeys of each type (#699)
Co-authored-by: Indent <noreply@indent.com>
2026-11-01 13:11:32 -05:00
indent[bot]
ac200339c5
signalmeow/receiving: use ciphertext message type for retry requests (#697)
Co-authored-by: Indent <noreply@indent.com>
2026-11-01 13:06:19 -05:00
Gerardo Rodriguez
880ea0dceb
signalmeow/receiving: ignore hangups for calls accepted on another device (#700) 2026-11-01 07:47:23 -05:00
Tulir Asokan
1cc623ed01 signalmeow/web: only log error responses by default
Closes #696
2026-10-30 14:17:10 +02:00
Highest
572a190db2
msgconv: clarify outgoing view-once placeholders (#679) 2026-10-29 11:40:25 -04:00
Nick Mills-Barrett
e2319d6cde
signalmeow: handle closed websocket status channels (#691) 2026-10-29 13:27:39 +00:00
Nick Mills-Barrett
357eb58a44
directmedia: map upstream download failures to matrix errors (#694)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:22:11 +00:00
Nick Mills-Barrett
c2b2792f0b
client,signalmeow/web: log out on upgrade 401 and retry other upgrade errors (#693)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:59:59 +00:00
Gerardo Rodriguez
6dc9ac3e0c
signalmeow/web: make request counter atomic (#687) 2026-10-27 08:28:43 -04:00
Gerardo Rodriguez
6c5841d1b7
backfill: mark read from the newest item and pass ReadUpTo (#689) 2026-10-26 12:22:45 -04:00
Gerardo Rodriguez
45d6a02f12
matrixfmt: don't add trailing separator when joining entity strings (#688) 2026-10-26 09:54:39 -04:00
Tulir Asokan
9f44495d18 msgconv/from-signal: add support for encrypted direct media 2026-10-26 14:18:13 +02:00
Gerardo Rodriguez
ccb1792120
signalmeow/senderkey: retry multi-recipient sends on server errors (#684) 2026-10-26 06:23:57 -04:00
Gerardo Rodriguez
7e65e8ecc9
signalmeow/web: don't block read loop exit on full request queue (#685) 2026-10-24 06:38:11 -04:00
Nick Mills-Barrett
2ef3d65686
connector: map errors to message statuses, resperrors and bridge states (#683) 2026-10-22 22:10:10 +02:00
Nick Mills-Barrett
887a0d1c33
capabilities: allow m4a and wav audio files (#678) 2026-10-21 23:40:25 +02:00
Toni Spets
46aff24f80
libsignalgo: free records and addresses owned by store callbacks (#682) 2026-10-19 17:51:23 +03:00
Tulir Asokan
82299a169e libsignalgo: remove gcc const pointer hack by using typedefs from libsignal headers 2026-10-17 13:53:01 +03:00
Gerardo Rodriguez
0915c775cf
msgconv/from-matrix: send bodies over 2 KiB as long text attachments (#680) 2026-10-16 18:42:29 -04:00
60 changed files with 1046 additions and 328 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

2
go.mod
View file

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

4
go.sum
View file

@ -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=

View file

@ -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.

View file

@ -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
}

View file

@ -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 {

View file

@ -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 != "" {

View file

@ -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

View file

@ -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) {

View file

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

View file

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

183
pkg/connector/errors.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View file

@ -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

View file

@ -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{

View file

@ -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 {

View file

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

View file

@ -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",

View file

@ -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 {

Some files were not shown because too many files have changed in this diff Show more