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

Compare commits

...
Author SHA1 Message Date
indent[bot]
01626c1428
connector: warn when Signal reports the primary device as idle (#715)
Co-authored-by: Indent <noreply@indent.com>
2026-11-12 07:34:57 -07:00
indent[bot]
b91e565505
groupinfo: keep poll vote level when groups become announcement-only (#730)
Co-authored-by: Indent <noreply@indent.com>
2026-11-12 06:54:17 -07:00
indent[bot]
8761e85a5b
groupinfo: keep reaction and redaction levels when groups become announcement-only (#728)
Co-authored-by: Indent <noreply@indent.com>
2026-11-12 06:05:53 -07:00
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 1073 additions and 328 deletions

View file

@ -30,6 +30,7 @@ jobs:
run: | run: |
go install golang.org/x/tools/cmd/goimports@latest go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@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" export PATH="$HOME/go/bin:$PATH"
- name: Run pre-commit - name: Run pre-commit

View file

@ -22,7 +22,10 @@ repos:
# TODO: reenable this and fix all the problems # TODO: reenable this and fix all the problems
- repo: https://github.com/beeper/pre-commit-go - repo: https://github.com/beeper/pre-commit-go
rev: v0.4.2 rev: v0.5.0
hooks: hooks:
- id: zerolog-ban-msgf - id: zerolog-ban-msgf
- id: zerolog-use-stringer - 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 # v26.10
* Updated libsignal to v0.105.0. * 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/grpc v1.84.0
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1 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 ( 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= 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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= 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.1-0.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY=
maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= 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 // GetBackupChatItems returns in reverse chronological order, so flip the list
slices.Reverse(items) slices.Reverse(items)
var firstDirectionfulProcessed bool
var isRead bool var isRead bool
var readUpTo time.Time
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items)) convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items))
attMap := make(msgconv.AttachmentMap) attMap := make(msgconv.AttachmentMap)
recipientMap := make(map[uint64]*backuppb.Recipient) recipientMap := make(map[uint64]*backuppb.Recipient)
@ -123,20 +123,17 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
case *backuppb.ChatItem_Incoming: case *backuppb.ChatItem_Incoming:
streamOrder = int64(dt.Incoming.GetDateServerSent()) streamOrder = int64(dt.Incoming.GetDateServerSent())
prevStreamOrder = streamOrder 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: case *backuppb.ChatItem_Outgoing:
streamOrder = int64(item.GetDateSent()) streamOrder = int64(item.GetDateSent())
// Ensure stream order is higher than previous incoming item, but lower than next incoming item // Ensure stream order is higher than previous incoming item, but lower than next incoming item
streamOrder = min(streamOrder, findNextStreamOrder(i+1)-1) streamOrder = min(streamOrder, findNextStreamOrder(i+1)-1)
streamOrder = max(streamOrder, prevStreamOrder+1) streamOrder = max(streamOrder, prevStreamOrder+1)
if !firstDirectionfulProcessed {
firstDirectionfulProcessed = true
isRead = true isRead = true
} readUpTo = time.UnixMilli(int64(item.DateSent))
} }
if len(attMap) > 0 { if len(attMap) > 0 {
clear(attMap) clear(attMap)
@ -178,11 +175,15 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
Reactions: convertedReactions, Reactions: convertedReactions,
}) })
} }
if isRead {
readUpTo = time.Time{}
}
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
Messages: convertedMessages, Messages: convertedMessages,
HasMore: len(items) >= params.Count, HasMore: len(items) >= params.Count,
Forward: params.Forward, Forward: params.Forward,
MarkRead: isRead, MarkRead: isRead,
ReadUpTo: readUpTo,
ApproxTotalCount: chat.TotalMessages, ApproxTotalCount: chat.TotalMessages,
CompleteCallback: func() { CompleteCallback: func() {
// When reaching the last backwards backfill batch, delete the chat from the backup store. // 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 { func capID() string {
base := "fi.mau.signal.capabilities.2026_10_06" base := "fi.mau.signal.capabilities.2026_10_21"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -48,7 +48,7 @@ func capID() string {
} }
const MaxFileSize = 100 * 1024 * 1024 const MaxFileSize = 100 * 1024 * 1024
const MaxTextLength = 2000 const MaxTextLength = 64 * 1024
var signalCaps = &event.RoomFeatures{ var signalCaps = &event.RoomFeatures{
ID: capID(), ID: capID(),
@ -105,6 +105,8 @@ var signalCaps = &event.RoomFeatures{
"audio/mpeg": event.CapLevelFullySupported, "audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported, "audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported, "audio/flac": event.CapLevelFullySupported,
"audio/mp4": event.CapLevelFullySupported,
"audio/wav": event.CapLevelFullySupported,
}, },
MaxSize: MaxFileSize, MaxSize: MaxFileSize,
}, },
@ -158,7 +160,7 @@ var signalCaps = &event.RoomFeatures{
event.MemberActionBan: event.CapLevelFullySupported, event.MemberActionBan: event.CapLevelFullySupported,
event.MemberActionKick: event.CapLevelFullySupported, event.MemberActionKick: event.CapLevelFullySupported,
}, },
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelPartialSupport, LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelFullySupported, Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported, PollEnd: event.CapLevelUnsupported,
@ -263,5 +265,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
} }
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { 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) name := s.Main.Config.FormatDisplayname(contact, username)
ui.Name = &name 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{ ui.Avatar = &bridgev2.Avatar{
ID: networkid.AvatarID("hash:" + contact.ContactAvatar.Hash), ID: contactAvatarID,
Get: func(ctx context.Context) ([]byte, error) { Get: func(ctx context.Context) ([]byte, error) {
if contact.ContactAvatar.Image == nil { if contact.ContactAvatar.Image == nil {
return nil, fmt.Errorf("contact avatar not available") 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 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 { func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
_, err := signalid.ParseUserIDAsServiceID(id) _, err := signalid.ParseUserIDAsServiceID(id)
return err == nil return err == nil
@ -220,7 +235,7 @@ func (s *SignalClient) resolvePhone(ctx context.Context, number string) (aci, pn
aci = recipient.ACI aci = recipient.ACI
pni = recipient.PNI pni = recipient.PNI
} else if resp, lookupErr := s.Client.LookupPhone(ctx, e164Number); lookupErr != nil { } 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 { } else {
aci = resp[e164Number].ACI aci = resp[e164Number].ACI
pni = resp[e164Number].PNI 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) { func (s *SignalClient) resolveUsername(ctx context.Context, username string) (aci, pni uuid.UUID, err error) {
serviceID, err := s.Client.ResolveUsername(ctx, username) serviceID, err := s.Client.ResolveUsername(ctx, username)
if err != nil { if err != nil {
err = wrapRespError(fmt.Errorf("error looking up username on server: %w", err))
return return
} }
aci, pni = serviceID.ToACIAndPNI() 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) group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
if err != nil { 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))) 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) resp, err := s.Client.CreateGroup(ctx, group)
if err != nil { 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 != "" { if params.RoomID != "" {
// UpdateMatrixRoomID could do this for us if we passed ChatInfoSource to it, // 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)) resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients))
for i, recipient := range recipients { for i, recipient := range recipients {
recipientResp := &bridgev2.ResolveIdentifierResponse{ if err = ctx.Err(); err != nil {
Chat: s.makeCreateDMResponse(ctx, recipient, nil), return nil, err
} }
recipientResp := &bridgev2.ResolveIdentifierResponse{}
var username string var username string
if recipient.ACI != uuid.Nil { if recipient.ACI != uuid.Nil {
recipientResp.Chat = &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(libsignalgo.NewACIServiceID(recipient.ACI)),
}
recipientResp.UserID = signalid.MakeUserID(recipient.ACI) recipientResp.UserID = signalid.MakeUserID(recipient.ACI)
ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID) ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID)
if err != nil { if err != nil {
@ -457,7 +477,11 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
recipientResp.Ghost = ghost recipientResp.Ghost = ghost
username = ghost.Metadata.(*signalid.GhostMetadata).Username username = ghost.Metadata.(*signalid.GhostMetadata).Username
} else { } 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) recipientResp.UserInfo, err = s.contactToUserInfo(ctx, recipient, username, false)
if err != nil { if err != nil {

View file

@ -19,6 +19,7 @@ package connector
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"errors"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@ -28,6 +29,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalid" "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/protobuf/backuppb"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types" "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 { if s.Client.Store.EphemeralBackupKey != nil {
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats") zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
meta, err := s.Client.WaitForTransfer(ctx) meta, err := s.Client.WaitForTransfer(ctx)
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 { 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") zerolog.Ctx(ctx).Err(err).Msg("Failed to request transfer archive")
return return
} else if meta.Error != "" { } else if meta.Error != "" {

View file

@ -182,9 +182,9 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone { peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone {
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState") s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState")
if err == nil { 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 { } 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: case signalmeow.SignalConnectionEventLoggedOut:
s.stopChatSync() s.stopChatSync()
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
if err == nil { s.sendLoggedOutState(err)
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()})
}
err = s.Client.ClearKeysAndDisconnect(context.TODO()) err = s.Client.ClearKeysAndDisconnect(context.TODO())
if err != nil { if err != nil {
s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect") 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: case signalmeow.SignalConnectionEventError:
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState") 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: case signalmeow.SignalConnectionEventFatalError:
s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") 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: case signalmeow.SignalConnectionCleanShutdown:
if s.Client.IsLoggedIn() { if s.Client.IsLoggedIn() {
s.UserLogin.Log.Debug().Msg("Clean Shutdown - sending no BridgeState") s.UserLogin.Log.Debug().Msg("Clean Shutdown - sending no BridgeState")
} else { } else {
s.UserLogin.Log.Debug().Msg("Clean Shutdown, but logged out - Sending BadCredentials BridgeState") 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) { func (s *SignalClient) Connect(ctx context.Context) {
if s.Client == nil { 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 return
} }
s.updateRemoteProfile(ctx, false) 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 { func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.ConnectBackgroundParams) error {
if !s.IsLoggedIn() {
return bridgev2.ErrNotLoggedIn
}
s.queueEmptyWaiter.Clear() s.queueEmptyWaiter.Clear()
ch, unauthCh, err := s.Client.StartWebsockets(ctx) ch, unauthCh, err := s.Client.StartWebsockets(ctx)
if err != nil { if err != nil {
@ -251,6 +253,11 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
log.Err(status.Err).Msg("Authed websocket disconnected") log.Err(status.Err).Msg("Authed websocket disconnected")
case web.SignalWebsocketConnectionEventLoggedOut: case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Authed websocket logged out") 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) return fmt.Errorf("authed websocket logged out: %w", status.Err)
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError:
log.Err(status.Err).Msg("Authed websocket error") 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) ch, err := s.Client.StartReceiveLoops(ctx)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops") 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 retryInSeconds := 2 << retryCount
if retryInSeconds > 150 { if retryInSeconds > 150 {
retryInSeconds = 150 retryInSeconds = 150

View file

@ -42,6 +42,7 @@ type SignalConfig struct {
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"` LocationFormat string `yaml:"location_format"`
ExtEvPolls bool `yaml:"extev_polls"` ExtEvPolls bool `yaml:"extev_polls"`
IdentityChangeNotices bool `yaml:"identity_change_notices"`
displaynameTemplate *template.Template `yaml:"-"` 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, "note_to_self_avatar")
helper.Copy(up.Str, "location_format") helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "extev_polls") helper.Copy(up.Bool, "extev_polls")
helper.Copy(up.Bool, "identity_change_notices")
} }
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {

View file

@ -94,8 +94,11 @@ func (s *SignalConnector) ResetHTTPTransport() {
settings = hs.GetHTTPClientSettings() settings = hs.GetHTTPClientSettings()
} }
oldClient := web.SignalHTTPClient oldClient := web.SignalHTTPClient
oldUploadClient := web.SignalUploadHTTPClient
web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile() web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile()
web.SignalUploadHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).WithResponseHeaderTimeout(2 * time.Minute).Compile()
oldClient.CloseIdleConnections() oldClient.CloseIdleConnections()
oldUploadClient.CloseIdleConnections()
} }
func (s *SignalConnector) ResetNetworkConnections() { 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) rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
if err != nil { if err != nil {
log.Err(err).Msg("Direct download failed") log.Err(err).Msg("Direct download failed")
return nil, err return nil, wrapDirectMediaError(err)
} }
case *signalid.DirectMediaProfileAvatar: case *signalid.DirectMediaProfileAvatar:
log.Info(). 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) rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
if err != nil { if err != nil {
log.Err(err).Msg("Direct download failed") log.Err(err).Msg("Direct download failed")
return nil, err return nil, wrapDirectMediaError(err)
} }
case *signalid.DirectMediaSticker: case *signalid.DirectMediaSticker:
log.Info(). 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) rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
if err != nil { if err != nil {
log.Err(err).Msg("Direct download failed") log.Err(err).Msg("Direct download failed")
return nil, err return nil, wrapDirectMediaError(err)
} }
default: default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info) return nil, fmt.Errorf("no downloader for direct media type: %T", info)

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

@ -0,0 +1,196 @@
// 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",
"signal-idle-primary-device": "Signal hasn't seen your phone in a long time. The bridge is still connected, " +
"but Signal will unlink it if you don't open Signal on your phone soon.",
})
}
var _ status.BridgeStateFiller = (*SignalClient)(nil)
func (s *SignalClient) FillBridgeState(state status.BridgeState) status.BridgeState {
if state.StateEvent == status.StateConnected && s.Client != nil && s.Client.AuthedWS.PrimaryDeviceIdle() {
state.StateEvent = status.StateTransientDisconnect
state.Error = "signal-idle-primary-device"
state.UserAction = status.UserActionOpenNative
}
return state
}
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' location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'
# Should polls be sent using unstable MSC3381 event types? # Should polls be sent using unstable MSC3381 event types?
extev_polls: false 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 defaultPL = 0
var moderatorPL = 50 var moderatorPL = 50
var terminatedPL = 100
func roleToPL(role signalmeow.GroupMemberRole) *int { func roleToPL(role signalmeow.GroupMemberRole) *int {
switch role { switch role {
@ -57,6 +58,9 @@ func applyAnnouncementsOnly(plc *bridgev2.PowerLevelOverrides, announcementsOnly
} else { } else {
plc.EventsDefault = &defaultPL plc.EventsDefault = &defaultPL
} }
plc.Events[event.EventReaction] = defaultPL
plc.Events[event.EventRedaction] = defaultPL
plc.Events[event.EventUnstablePollResponse] = defaultPL
} }
func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess signalmeow.AccessControl) { func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess signalmeow.AccessControl) {
@ -70,6 +74,21 @@ func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess si
plc.Events[event.StateBeeperDisappearingTimer] = attributePL 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.EventUnstablePollResponse, event.StatePowerLevels, event.StateRoomName,
event.StateRoomAvatar, event.StateTopic, event.StateBeeperDisappearingTimer,
} {
plc.Events[evtType] = terminatedPL
}
}
func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) { func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) {
if memberAccess == signalmeow.AccessControl_ADMINISTRATOR { if memberAccess == signalmeow.AccessControl_ADMINISTRATOR {
plc.Invite = &moderatorPL plc.Invite = &moderatorPL
@ -123,6 +142,9 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members) applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink) joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink)
} }
if groupInfo.Terminated {
applyTerminated(members.PowerLevels)
}
for _, member := range groupInfo.RequestingMembers { for _, member := range groupInfo.RequestingMembers {
members.MemberMap.Set(bridgev2.ChatMember{ members.MemberMap.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
@ -182,9 +204,12 @@ func addMemberToMap(mc map[networkid.UserID]bridgev2.ChatMember, member bridgev2
mc[member.EventSender.Sender] = member mc[member.EventSender.Sender] = member
} }
const currentPortalInfoVersion = 3
func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool { func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool {
meta := portal.Metadata.(*signalid.PortalMetadata) meta := portal.Metadata.(*signalid.PortalMetadata)
meta.LastSync = jsontime.UnixNow() meta.LastSync = jsontime.UnixNow()
meta.InfoVersion = currentPortalInfoVersion
return true return true
} }
@ -260,7 +285,8 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
var pls *bridgev2.PowerLevelOverrides var pls *bridgev2.PowerLevelOverrides
if groupChange.ModifyAnnouncementsOnly != nil || if groupChange.ModifyAnnouncementsOnly != nil ||
groupChange.ModifyAttributesAccess != nil || groupChange.ModifyAttributesAccess != nil ||
groupChange.ModifyMemberAccess != nil { groupChange.ModifyMemberAccess != nil ||
groupChange.TerminateGroup {
pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)} pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)}
if groupChange.ModifyAnnouncementsOnly != nil { if groupChange.ModifyAnnouncementsOnly != nil {
applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly) applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly)
@ -271,6 +297,9 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
if groupChange.ModifyMemberAccess != nil { if groupChange.ModifyMemberAccess != nil {
applyMembersAccess(pls, *groupChange.ModifyMemberAccess) applyMembersAccess(pls, *groupChange.ModifyMemberAccess)
} }
if groupChange.TerminateGroup {
applyTerminated(pls)
}
} }
if groupChange.ModifyAddFromInviteLinkAccess != nil { if groupChange.ModifyAddFromInviteLinkAccess != nil {
ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{ ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{

View file

@ -37,6 +37,7 @@ import (
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "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/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/signalpb" "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") log.Debug().Msg("No successes or failures - Probably sent to myself")
} else if len(result.SuccessfullySentTo) == 0 { } else if len(result.SuccessfullySentTo) == 0 {
log.Error().Msg("Failed to send event to all members of Signal group") 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 { } else if len(result.SuccessfullySentTo) < totalRecipients {
if len(result.FailedToSendTo) > 0 { if len(result.FailedToSendTo) > 0 {
log.Warn().Msg("Failed to send event to some members of Signal group") 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, ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo,
) )
if err != nil { if err != nil {
return nil, err return nil, wrapConversionError(err)
} }
return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{ return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0, ContainsAttachments: msgconv.ContainsAttachments(converted),
ViewOnce: converted.GetIsViewOnce(), ViewOnce: converted.GetIsViewOnce(),
}) })
} }
@ -146,7 +147,7 @@ func (s *SignalClient) doSendMessage(
msg.AddPendingToIgnore(networkid.TransactionID(msgID)) msg.AddPendingToIgnore(networkid.TransactionID(msgID))
err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted)) err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted))
if err != nil { if err != nil {
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return nil, wrapSendError(err)
} }
dbMsg := &database.Message{ dbMsg := &database.Message{
ID: msgID, ID: msgID,
@ -169,14 +170,14 @@ func (s *SignalClient) HandleMatrixViewLimitedMedia(ctx context.Context, msg *br
if err != nil { if err != nil {
return err 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{ Content: &signalpb.Content_ReceiptMessage{
ReceiptMessage: &signalpb.ReceiptMessage{ ReceiptMessage: &signalpb.ReceiptMessage{
Type: signalpb.ReceiptMessage_VIEWED.Enum(), Type: signalpb.ReceiptMessage_VIEWED.Enum(),
Timestamp: []uint64{timestamp}, Timestamp: []uint64{timestamp},
}, },
}, },
}) }))
} }
func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { 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 { if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err) return fmt.Errorf("failed to parse target message ID: %w", err)
} else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) { } 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 var replyTo *database.Message
if msg.EditTarget.ReplyTo.MessageID != "" { 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) converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
if err != nil { if err != nil {
return err return wrapConversionError(err)
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts converted.Timestamp = &ts
@ -207,11 +208,11 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
DataMessage: converted, DataMessage: converted,
})) }))
if err != nil { if err != nil {
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return wrapSendError(err)
} }
prevID := msg.EditTarget.ID prevID := msg.EditTarget.ID
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts) 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++ msg.EditTarget.EditCount++
if prevID != msg.EditTarget.ID { if prevID != msg.EditTarget.ID {
err = s.Main.Bridge.DB.DoTxn(ctx, nil, func(ctx context.Context) error { 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 { if err != nil {
return nil, err return nil, wrapSendError(err)
} }
return &database.Reaction{}, nil return &database.Reaction{}, nil
} }
@ -297,30 +298,52 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}, },
})) }))
if err != nil { return wrapSendError(err)
return err
}
return nil
} }
func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { 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 { if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err) 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) dm := &signalpb.DataMessage{
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)),
Timestamp: proto.Uint64(ts), }
Delete: &signalpb.DataMessage_Delete{ if msg.TargetMessage.SenderID == signalid.MakeUserID(s.Client.Store.ACI) {
dm.Delete = &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
},
}))
if err != nil {
return err
} }
return nil } 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),
}
}
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 { 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 gc.Revision = portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID) revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil { if err != nil {
return false, err return false, wrapSendError(err)
} }
if gc.ModifyTitle != nil { if gc.ModifyTitle != nil {
portal.Name = *gc.ModifyTitle portal.Name = *gc.ModifyTitle
@ -445,7 +468,7 @@ func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2
avatarHash = sha256.Sum256(data) avatarHash = sha256.Sum256(data)
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "") avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "")
if err != nil { 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{ 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 { if msg.Portal.RoomType == database.RoomTypeDM {
switch msg.Type { switch msg.Type {
case bridgev2.Invite: case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm") return nil, errInviteToDM
default: default:
return nil, nil 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 gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID) revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil { if err != nil {
return nil, err return nil, wrapSendError(err)
} }
if (msg.Type == bridgev2.Invite || msg.Type == bridgev2.AcceptKnock) && targetSignalID.Type != libsignalgo.ServiceIDTypePNI { if (msg.Type == bridgev2.Invite || msg.Type == bridgev2.AcceptKnock) && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) 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) revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil { if err != nil {
return false, err return false, wrapSendError(err)
} }
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return true, nil 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) { func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
if msg.Content.Type != event.DisappearingTypeAfterRead && msg.Content.Timer.Duration != 0 { 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) userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
@ -750,7 +773,7 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
})) }))
if !res.WasSuccessful { if !res.WasSuccessful {
return false, res.Error return false, wrapSendError(res.Error)
} }
msg.Portal.Disappear = newSetting msg.Portal.Disappear = newSetting
return true, nil return true, nil
@ -775,7 +798,7 @@ func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2
Revision: portalMeta.Revision + 1, Revision: portalMeta.Revision + 1,
}, groupID) }, groupID)
if err != nil { 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 portalMeta.Revision = revision
} }
@ -785,7 +808,7 @@ func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2
// TODO block and delete support? // TODO block and delete support?
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE) err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE)
if err != nil { 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") Msg("Sent conversation deletion to Signal")
if !result.WasSuccessful { 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 return nil
@ -967,7 +990,7 @@ func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg
} }
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT) err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT)
if err != nil { 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 { if userID.Type == libsignalgo.ServiceIDTypeACI {
profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI) profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI)
@ -996,7 +1019,7 @@ func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg
PniSignatureMessage: pniSig, PniSignatureMessage: pniSig,
}) })
if !res.WasSuccessful { 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? // 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 { 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) { _, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, func(recipient *types.Recipient) (bool, error) {
changed := recipient.Blocked != msg.Content.Block changed := recipient.Blocked != msg.Content.Block
@ -1055,7 +1078,7 @@ func (s *SignalClient) reportSpam(ctx context.Context, portal *bridgev2.Portal,
continue continue
} }
if err = s.Client.ReportSpam(ctx, aci, meta.ServerGUID, meta.ReportingToken); err != nil { if err = s.Client.ReportSpam(ctx, aci, meta.ServerGUID, meta.ReportingToken); err != nil {
return err return wrapSendError(err)
} }
seen[meta.ServerGUID] = true seen[meta.ServerGUID] = true
if len(seen) == 3 { if len(seen) == 3 {

View file

@ -34,7 +34,6 @@ import (
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent" "maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
@ -74,9 +73,9 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
case *events.QueueEmpty: case *events.QueueEmpty:
s.queueEmptyWaiter.Set() s.queueEmptyWaiter.Set()
case *events.LoggedOut: case *events.LoggedOut:
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: evt.Error.Error()}) s.sendLoggedOutState(evt.Error)
case *events.IdentityChange: case *events.IdentityChange:
// TODO send notice s.handleSignalIdentityChange(evt)
default: default:
s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type") 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 }, 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 { type Bv2ChatEvent struct {
*events.ChatEvent *events.ChatEvent
s *SignalClient s *SignalClient
@ -186,6 +223,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
switch { switch {
case innerEvt.GetIsViewOnce(), innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil, 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.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil,
innerEvt.PinMessage != nil, innerEvt.UnpinMessage != nil,
innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT), innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0: innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
return bridgev2.RemoteEventMessage return bridgev2.RemoteEventMessage
@ -234,13 +272,16 @@ func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal)
if !ok || dataMsg.GroupV2 == nil { if !ok || dataMsg.GroupV2 == nil {
return return
} }
portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision portalMeta := portal.Metadata.(*signalid.PortalMetadata)
portalRev := portalMeta.Revision
if evt.Info.GroupRevision > portalRev { if evt.Info.GroupRevision > portalRev {
toRevision := evt.Info.GroupRevision toRevision := evt.Info.GroupRevision
if dataMsg.GetGroupV2().GetGroupChange() != nil { if dataMsg.GetGroupV2().GetGroupChange() != nil {
toRevision-- toRevision--
} }
evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp()) 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{ ErrDeviceLinkRateLimited = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED", ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED",
Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again", Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again",
StatusCode: http.StatusTooManyRequests, StatusCode: http.StatusBadRequest,
} }
ErrDeviceLinkRejected = bridgev2.RespError{ ErrDeviceLinkRejected = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED", 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)) return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk))
} }
func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat { func (bk *BackupKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t {
return cFixedArray32Compat(bk.cFixedArray()) return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(bk))
} }
func GenerateRandomBackupKey() *BackupKey { func GenerateRandomBackupKey() *BackupKey {

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