From 0915c775cf8244df19fb68d64fc8895639a0dce1 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 16 Oct 2026 18:42:29 -0400 Subject: [PATCH 01/46] msgconv/from-matrix: send bodies over 2 KiB as long text attachments (#680) --- pkg/connector/capabilities.go | 8 ++++---- pkg/connector/handlematrix.go | 5 +++-- pkg/msgconv/from-matrix.go | 25 +++++++++++++++++++++++++ pkg/msgconv/from-signal.go | 4 ++-- pkg/msgconv/msgconv.go | 10 ++++++++++ 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 05c7e60..b62d34f 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -40,7 +40,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.signal.capabilities.2026_10_06" + base := "fi.mau.signal.capabilities.2026_10_16" 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(), @@ -158,7 +158,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 +263,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities } func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { - return 1, 13 + return 1, 14 } diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 7360d67..8812447 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -37,6 +37,7 @@ import ( "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-signal/pkg/libsignalgo" + "go.mau.fi/mautrix-signal/pkg/msgconv" "go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/signalpb" @@ -126,7 +127,7 @@ func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Ma return nil, err } return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{ - ContainsAttachments: len(converted.Attachments) > 0, + ContainsAttachments: msgconv.ContainsAttachments(converted), ViewOnce: converted.GetIsViewOnce(), }) } @@ -211,7 +212,7 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri } 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 { diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index c54654c..0aab1f1 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "strings" + "unicode/utf8" "github.com/rs/zerolog" "go.mau.fi/util/exmime" @@ -152,9 +153,33 @@ func (mc *MessageConverter) ToSignal( dm.BodyRanges = nil dm.Preview = nil } + if err := mc.uploadLongText(ctx, dm); err != nil { + return nil, err + } return dm, nil } +const maxInlineBodySize = 2 * 1024 + +func (mc *MessageConverter) uploadLongText(ctx context.Context, dm *signalpb.DataMessage) error { + body := dm.GetBody() + if len(body) <= maxInlineBodySize { + return nil + } + att, err := getClient(ctx).UploadAttachment(ctx, []byte(body)) + if err != nil { + return fmt.Errorf("%w (long text): %w", bridgev2.ErrMediaReuploadFailed, err) + } + att.ContentType = proto.String(LongTextContentType) + dm.Attachments = append([]*signalpb.AttachmentPointer{att}, dm.Attachments...) + cut := maxInlineBodySize + for !utf8.RuneStart(body[cut]) { + cut-- + } + dm.Body = proto.String(body[:cut]) + return nil +} + func maybeInt[T constraints.Integer](v T) *T { if v == 0 { return nil diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index 68f5558..7c8a7a6 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -173,7 +173,7 @@ func (mc *MessageConverter) ToMatrix( } } for i, att := range dm.GetAttachments() { - if att.GetContentType() != "text/x-signal-plain" || att.GetSize() > matrixTextMaxLength { + if att.GetContentType() != LongTextContentType || att.GetSize() > matrixTextMaxLength { part := mc.convertAttachmentToMatrix(ctx, i, att, attMap) if dm.GetIsViewOnce() { if part.Content.MsgType == event.MsgImage || part.Content.MsgType == event.MsgVideo { @@ -217,7 +217,7 @@ func (mc *MessageConverter) ToMatrix( for i, part := range cm.Parts { part.ID = signalid.MakeMessagePartID(i) meta := &signalid.MessageMetadata{ - ContainsAttachments: len(dm.GetAttachments()) > 0, + ContainsAttachments: ContainsAttachments(dm), ViewOnce: dm.GetIsViewOnce(), } if meta.ViewOnce { diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go index 03bf621..2ec6ad0 100644 --- a/pkg/msgconv/msgconv.go +++ b/pkg/msgconv/msgconv.go @@ -18,6 +18,7 @@ package msgconv import ( "context" + "slices" "github.com/google/uuid" "maunium.net/go/mautrix/bridgev2" @@ -28,6 +29,7 @@ import ( "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt" "go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalmeow" + "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/signalpb" ) type contextKey int @@ -96,6 +98,14 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter { } } +const LongTextContentType = "text/x-signal-plain" + +func ContainsAttachments(dm *signalpb.DataMessage) bool { + return slices.ContainsFunc(dm.GetAttachments(), func(att *signalpb.AttachmentPointer) bool { + return att.GetContentType() != LongTextContentType + }) +} + func getClient(ctx context.Context) *signalmeow.Client { return ctx.Value(contextKeyClient).(*signalmeow.Client) } From 82299a169e3da9ab84caac0835b86d0e0089f655 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 17 Oct 2026 13:49:45 +0300 Subject: [PATCH 02/46] libsignalgo: remove gcc const pointer hack by using typedefs from libsignal headers --- pkg/libsignalgo/backupkey.go | 4 +- pkg/libsignalgo/fixedarray.go | 60 ++++++++++++++-------------- pkg/libsignalgo/fixedarray_clang.go | 24 ----------- pkg/libsignalgo/fixedarray_gcc.go | 26 ------------ pkg/libsignalgo/groupsecretparams.go | 12 +++--- pkg/libsignalgo/profilekey.go | 16 ++++---- pkg/libsignalgo/serviceid.go | 4 +- 7 files changed, 48 insertions(+), 98 deletions(-) delete mode 100644 pkg/libsignalgo/fixedarray_clang.go delete mode 100644 pkg/libsignalgo/fixedarray_gcc.go diff --git a/pkg/libsignalgo/backupkey.go b/pkg/libsignalgo/backupkey.go index b5f53f2..47cce90 100644 --- a/pkg/libsignalgo/backupkey.go +++ b/pkg/libsignalgo/backupkey.go @@ -49,8 +49,8 @@ func (bk *BackupKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk)) } -func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(bk.cFixedArray()) +func (bk *BackupKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(bk)) } func GenerateRandomBackupKey() *BackupKey { diff --git a/pkg/libsignalgo/fixedarray.go b/pkg/libsignalgo/fixedarray.go index 949e808..ad94514 100644 --- a/pkg/libsignalgo/fixedarray.go +++ b/pkg/libsignalgo/fixedarray.go @@ -42,118 +42,118 @@ func (a *fixedArray15) cFixedArray() *C.SignalType_FixedArray15_uint8_t { return (*C.SignalType_FixedArray15_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray15) cConstFixedArray() cFixedArray15Compat { - return cFixedArray15Compat(a.cFixedArray()) +func (a *fixedArray15) cConstFixedArray() C.SignalType_ConstPointer_FixedArray15_uint8_t { + return (C.SignalType_ConstPointer_FixedArray15_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray16) cFixedArray() *C.SignalType_FixedArray16_uint8_t { return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray16) cConstFixedArray() cFixedArray16Compat { - return cFixedArray16Compat(a.cFixedArray()) +func (a *fixedArray16) cConstFixedArray() C.SignalType_ConstPointer_FixedArray16_uint8_t { + return (C.SignalType_ConstPointer_FixedArray16_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray17) cFixedArray() *C.SignalType_FixedArray17_uint8_t { return (*C.SignalType_FixedArray17_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray17) cConstFixedArray() cFixedArray17Compat { - return cFixedArray17Compat(a.cFixedArray()) +func (a *fixedArray17) cConstFixedArray() C.SignalType_ConstPointer_FixedArray17_uint8_t { + return (C.SignalType_ConstPointer_FixedArray17_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray32) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray32) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(a.cFixedArray()) +func (a *fixedArray32) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray64) cFixedArray() *C.SignalType_FixedArray64_uint8_t { return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray64) cConstFixedArray() cFixedArray64Compat { - return cFixedArray64Compat(a.cFixedArray()) +func (a *fixedArray64) cConstFixedArray() C.SignalType_ConstPointer_FixedArray64_uint8_t { + return (C.SignalType_ConstPointer_FixedArray64_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray65) cFixedArray() *C.SignalType_FixedArray65_uint8_t { return (*C.SignalType_FixedArray65_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray65) cConstFixedArray() cFixedArray65Compat { - return cFixedArray65Compat(a.cFixedArray()) +func (a *fixedArray65) cConstFixedArray() C.SignalType_ConstPointer_FixedArray65_uint8_t { + return (C.SignalType_ConstPointer_FixedArray65_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray97) cFixedArray() *C.SignalType_FixedArray97_uint8_t { return (*C.SignalType_FixedArray97_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray97) cConstFixedArray() cFixedArray97Compat { - return cFixedArray97Compat(a.cFixedArray()) +func (a *fixedArray97) cConstFixedArray() C.SignalType_ConstPointer_FixedArray97_uint8_t { + return (C.SignalType_ConstPointer_FixedArray97_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray129) cFixedArray() *C.SignalType_FixedArray129_uint8_t { return (*C.SignalType_FixedArray129_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray129) cConstFixedArray() cFixedArray129Compat { - return cFixedArray129Compat(a.cFixedArray()) +func (a *fixedArray129) cConstFixedArray() C.SignalType_ConstPointer_FixedArray129_uint8_t { + return (C.SignalType_ConstPointer_FixedArray129_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray153) cFixedArray() *C.SignalType_FixedArray153_uint8_t { return (*C.SignalType_FixedArray153_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray153) cConstFixedArray() cFixedArray153Compat { - return cFixedArray153Compat(a.cFixedArray()) +func (a *fixedArray153) cConstFixedArray() C.SignalType_ConstPointer_FixedArray153_uint8_t { + return (C.SignalType_ConstPointer_FixedArray153_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray177) cFixedArray() *C.SignalType_FixedArray177_uint8_t { return (*C.SignalType_FixedArray177_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray177) cConstFixedArray() cFixedArray177Compat { - return cFixedArray177Compat(a.cFixedArray()) +func (a *fixedArray177) cConstFixedArray() C.SignalType_ConstPointer_FixedArray177_uint8_t { + return (C.SignalType_ConstPointer_FixedArray177_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray289) cFixedArray() *C.SignalType_FixedArray289_uint8_t { return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray289) cConstFixedArray() cFixedArray289Compat { - return cFixedArray289Compat(a.cFixedArray()) +func (a *fixedArray289) cConstFixedArray() C.SignalType_ConstPointer_FixedArray289_uint8_t { + return (C.SignalType_ConstPointer_FixedArray289_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray329) cFixedArray() *C.SignalType_FixedArray329_uint8_t { return (*C.SignalType_FixedArray329_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray329) cConstFixedArray() cFixedArray329Compat { - return cFixedArray329Compat(a.cFixedArray()) +func (a *fixedArray329) cConstFixedArray() C.SignalType_ConstPointer_FixedArray329_uint8_t { + return (C.SignalType_ConstPointer_FixedArray329_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray409) cFixedArray() *C.SignalType_FixedArray409_uint8_t { return (*C.SignalType_FixedArray409_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray409) cConstFixedArray() cFixedArray409Compat { - return cFixedArray409Compat(a.cFixedArray()) +func (a *fixedArray409) cConstFixedArray() C.SignalType_ConstPointer_FixedArray409_uint8_t { + return (C.SignalType_ConstPointer_FixedArray409_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray473) cFixedArray() *C.SignalType_FixedArray473_uint8_t { return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray473) cConstFixedArray() cFixedArray473Compat { - return cFixedArray473Compat(a.cFixedArray()) +func (a *fixedArray473) cConstFixedArray() C.SignalType_ConstPointer_FixedArray473_uint8_t { + return (C.SignalType_ConstPointer_FixedArray473_uint8_t)(unsafe.Pointer(a)) } func (a *fixedArray497) cFixedArray() *C.SignalType_FixedArray497_uint8_t { return (*C.SignalType_FixedArray497_uint8_t)(unsafe.Pointer(a)) } -func (a *fixedArray497) cConstFixedArray() cFixedArray497Compat { - return cFixedArray497Compat(a.cFixedArray()) +func (a *fixedArray497) cConstFixedArray() C.SignalType_ConstPointer_FixedArray497_uint8_t { + return (C.SignalType_ConstPointer_FixedArray497_uint8_t)(unsafe.Pointer(a)) } diff --git a/pkg/libsignalgo/fixedarray_clang.go b/pkg/libsignalgo/fixedarray_clang.go deleted file mode 100644 index a364eb5..0000000 --- a/pkg/libsignalgo/fixedarray_clang.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build darwin || android || ios || (windows && arm64) || clang - -package libsignalgo - -/* -#include "./libsignal-ffi.h" -*/ -import "C" - -type cFixedArray15Compat = *C.SignalType_FixedArray15_uint8_t -type cFixedArray16Compat = *C.SignalType_FixedArray16_uint8_t -type cFixedArray17Compat = *C.SignalType_FixedArray17_uint8_t -type cFixedArray32Compat = *C.SignalType_FixedArray32_uint8_t -type cFixedArray64Compat = *C.SignalType_FixedArray64_uint8_t -type cFixedArray65Compat = *C.SignalType_FixedArray65_uint8_t -type cFixedArray97Compat = *C.SignalType_FixedArray97_uint8_t -type cFixedArray129Compat = *C.SignalType_FixedArray129_uint8_t -type cFixedArray153Compat = *C.SignalType_FixedArray153_uint8_t -type cFixedArray177Compat = *C.SignalType_FixedArray177_uint8_t -type cFixedArray289Compat = *C.SignalType_FixedArray289_uint8_t -type cFixedArray329Compat = *C.SignalType_FixedArray329_uint8_t -type cFixedArray409Compat = *C.SignalType_FixedArray409_uint8_t -type cFixedArray473Compat = *C.SignalType_FixedArray473_uint8_t -type cFixedArray497Compat = *C.SignalType_FixedArray497_uint8_t diff --git a/pkg/libsignalgo/fixedarray_gcc.go b/pkg/libsignalgo/fixedarray_gcc.go deleted file mode 100644 index 14bd230..0000000 --- a/pkg/libsignalgo/fixedarray_gcc.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !(darwin || android || ios || (windows && arm64) || clang) - -package libsignalgo - -/* -#include "./libsignal-ffi.h" -*/ -import "C" - -// Hack for https://github.com/golang/go/issues/7270 -// The clang version is more correct, but doesn't work with gcc. -type cFixedArray15Compat = *[15]C.uint8_t -type cFixedArray16Compat = *[16]C.uint8_t -type cFixedArray17Compat = *[17]C.uint8_t -type cFixedArray32Compat = *[32]C.uint8_t -type cFixedArray64Compat = *[64]C.uint8_t -type cFixedArray65Compat = *[65]C.uint8_t -type cFixedArray97Compat = *[97]C.uint8_t -type cFixedArray129Compat = *[129]C.uint8_t -type cFixedArray153Compat = *[153]C.uint8_t -type cFixedArray177Compat = *[177]C.uint8_t -type cFixedArray289Compat = *[289]C.uint8_t -type cFixedArray329Compat = *[329]C.uint8_t -type cFixedArray409Compat = *[409]C.uint8_t -type cFixedArray473Compat = *[473]C.uint8_t -type cFixedArray497Compat = *[497]C.uint8_t diff --git a/pkg/libsignalgo/groupsecretparams.go b/pkg/libsignalgo/groupsecretparams.go index 90149b4..9402d0c 100644 --- a/pkg/libsignalgo/groupsecretparams.go +++ b/pkg/libsignalgo/groupsecretparams.go @@ -57,24 +57,24 @@ func (gmk *GroupMasterKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gmk)) } -func (gmk *GroupMasterKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(gmk.cFixedArray()) +func (gmk *GroupMasterKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(gmk)) } func (gsp *GroupSecretParams) cFixedArray() *C.SignalType_FixedArray289_uint8_t { return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(gsp)) } -func (gsp *GroupSecretParams) cConstFixedArray() cFixedArray289Compat { - return cFixedArray289Compat(gsp.cFixedArray()) +func (gsp *GroupSecretParams) cConstFixedArray() C.SignalType_ConstPointer_FixedArray289_uint8_t { + return (C.SignalType_ConstPointer_FixedArray289_uint8_t)(unsafe.Pointer(gsp)) } func (gid *GroupIdentifier) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gid)) } -func (gid *GroupIdentifier) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(gid.cFixedArray()) +func (gid *GroupIdentifier) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(gid)) } func (gid *GroupIdentifier) String() string { diff --git a/pkg/libsignalgo/profilekey.go b/pkg/libsignalgo/profilekey.go index 0c21aff..a39eaa5 100644 --- a/pkg/libsignalgo/profilekey.go +++ b/pkg/libsignalgo/profilekey.go @@ -46,24 +46,24 @@ func (pk *ProfileKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t { return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(pk)) } -func (pk *ProfileKey) cConstFixedArray() cFixedArray32Compat { - return cFixedArray32Compat(pk.cFixedArray()) +func (pk *ProfileKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray32_uint8_t { + return (C.SignalType_ConstPointer_FixedArray32_uint8_t)(unsafe.Pointer(pk)) } func (pkv *ProfileKeyVersion) cFixedArray() *C.SignalType_FixedArray64_uint8_t { return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(pkv)) } -func (pkv *ProfileKeyVersion) cConstFixedArray() cFixedArray64Compat { - return cFixedArray64Compat(pkv.cFixedArray()) +func (pkv *ProfileKeyVersion) cConstFixedArray() C.SignalType_ConstPointer_FixedArray64_uint8_t { + return (C.SignalType_ConstPointer_FixedArray64_uint8_t)(unsafe.Pointer(pkv)) } func (ak *AccessKey) cFixedArray() *C.SignalType_FixedArray16_uint8_t { return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(ak)) } -func (ak *AccessKey) cConstFixedArray() cFixedArray16Compat { - return cFixedArray16Compat(ak.cFixedArray()) +func (ak *AccessKey) cConstFixedArray() C.SignalType_ConstPointer_FixedArray16_uint8_t { + return (C.SignalType_ConstPointer_FixedArray16_uint8_t)(unsafe.Pointer(ak)) } func DeserializeProfileKey(bytes []byte) (*ProfileKey, error) { @@ -177,8 +177,8 @@ func (p *ProfileKeyCredentialRequestContext) cFixedArray() *C.SignalType_FixedAr return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(p)) } -func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() cFixedArray473Compat { - return cFixedArray473Compat(p.cFixedArray()) +func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() C.SignalType_ConstPointer_FixedArray473_uint8_t { + return (C.SignalType_ConstPointer_FixedArray473_uint8_t)(unsafe.Pointer(p)) } func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) { diff --git a/pkg/libsignalgo/serviceid.go b/pkg/libsignalgo/serviceid.go index 38631ee..a320319 100644 --- a/pkg/libsignalgo/serviceid.go +++ b/pkg/libsignalgo/serviceid.go @@ -188,6 +188,6 @@ func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t { return s.FixedBytes().cFixedArray() } -func (s ServiceID) cConstFixedArray() cFixedArray17Compat { - return cFixedArray17Compat(s.cFixedArray()) +func (s ServiceID) cConstFixedArray() C.SignalType_ConstPointer_FixedArray17_uint8_t { + return s.FixedBytes().cConstFixedArray() } From 46aff24f80816f10dae5ceff210b3dacb588a094 Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Mon, 19 Oct 2026 17:51:23 +0300 Subject: [PATCH 03/46] libsignalgo: free records and addresses owned by store callbacks (#682) --- pkg/libsignalgo/identitykeystore.go | 17 +++++++---------- pkg/libsignalgo/kyberprekeystore.go | 9 +++------ pkg/libsignalgo/prekeystore.go | 8 ++------ pkg/libsignalgo/senderkeystore.go | 13 +++++-------- pkg/libsignalgo/sessionstore.go | 12 +++++------- pkg/libsignalgo/signedprekeystore.go | 8 ++------ 6 files changed, 24 insertions(+), 43 deletions(-) diff --git a/pkg/libsignalgo/identitykeystore.go b/pkg/libsignalgo/identitykeystore.go index ba26f06..eb80056 100644 --- a/pkg/libsignalgo/identitykeystore.go +++ b/pkg/libsignalgo/identitykeystore.go @@ -90,12 +90,8 @@ func signal_get_local_registration_id_callback(storeCtx unsafe.Pointer, idp *C.u //export signal_save_identity_key_callback func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, address C.SignalMutPointerProtocolAddress, publicKey C.SignalMutPointerPublicKey) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - publicKeyStruct := PublicKey{ptr: publicKey.raw} - cloned, err := publicKeyStruct.Clone() - if err != nil { - return err - } - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) + pub := wrapPublicKey(publicKey.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err @@ -103,7 +99,7 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, replaced, err := store.SaveIdentityKey( ctx, theirServiceID, - &IdentityKey{cloned}, + &IdentityKey{pub}, ) if err != nil { return err @@ -120,7 +116,7 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, //export signal_get_identity_key_callback func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.SignalMutPointerPublicKey, address C.SignalMutPointerProtocolAddress) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err @@ -137,12 +133,13 @@ func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.Si //export signal_is_trusted_identity_callback func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, out *C.bool, address C.SignalMutPointerProtocolAddress, public_key C.SignalMutPointerPublicKey, direction C.uint32_t) C.int { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { - addr := &Address{ptr: address.raw} + addr := wrapAddress(address.raw) + pub := wrapPublicKey(public_key.raw) theirServiceID, err := addr.NameServiceID() if err != nil { return err } - trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: public_key.raw}}, SignalDirection(direction)) + trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{pub}, SignalDirection(direction)) if err != nil { return err } diff --git a/pkg/libsignalgo/kyberprekeystore.go b/pkg/libsignalgo/kyberprekeystore.go index 9deea17..78ec925 100644 --- a/pkg/libsignalgo/kyberprekeystore.go +++ b/pkg/libsignalgo/kyberprekeystore.go @@ -52,18 +52,15 @@ func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalM //export signal_store_kyber_pre_key_callback func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerKyberPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { - record := KyberPreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreKyberPreKey(ctx, uint32(id), cloned) + record := wrapKyberPreKeyRecord(preKeyRecord.raw) + return store.StoreKyberPreKey(ctx, uint32(id), record) }) } //export signal_mark_kyber_pre_key_used_callback func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t, ecPrekeyID C.uint32_t, baseKey C.SignalMutPointerPublicKey) C.int { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { + _ = wrapPublicKey(baseKey.raw) // TODO use ecPrekeyID and baseKey? return store.MarkKyberPreKeyUsed(ctx, uint32(id)) }) diff --git a/pkg/libsignalgo/prekeystore.go b/pkg/libsignalgo/prekeystore.go index 8c3c36f..00ee50f 100644 --- a/pkg/libsignalgo/prekeystore.go +++ b/pkg/libsignalgo/prekeystore.go @@ -52,12 +52,8 @@ func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPoin //export signal_store_pre_key_callback func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error { - record := PreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StorePreKey(ctx, uint32(id), cloned) + record := wrapPreKeyRecord(preKeyRecord.raw) + return store.StorePreKey(ctx, uint32(id), record) }) } diff --git a/pkg/libsignalgo/senderkeystore.go b/pkg/libsignalgo/senderkeystore.go index 1649216..fc51571 100644 --- a/pkg/libsignalgo/senderkeystore.go +++ b/pkg/libsignalgo/senderkeystore.go @@ -40,7 +40,8 @@ type SenderKeyStore interface { //export signal_load_sender_key_callback func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSenderKeyRecord, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid) C.int { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { - record, err := store.LoadSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID))) + addr := wrapAddress(address.raw) + record, err := store.LoadSenderKey(ctx, addr, *(*uuid.UUID)(unsafe.Pointer(&distributionID))) if err == nil && record != nil { record.CancelFinalizer() recordp.raw = record.ptr @@ -52,13 +53,9 @@ func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp *C.SignalM //export signal_store_sender_key_callback func signal_store_sender_key_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, distributionID C.SignalUuid, senderKeyRecord C.SignalMutPointerSenderKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { - record := SenderKeyRecord{ptr: senderKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - - return store.StoreSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), cloned) + addr := wrapAddress(address.raw) + record := wrapSenderKeyRecord(senderKeyRecord.raw) + return store.StoreSenderKey(ctx, addr, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), record) }) } diff --git a/pkg/libsignalgo/sessionstore.go b/pkg/libsignalgo/sessionstore.go index 99000e5..c33a899 100644 --- a/pkg/libsignalgo/sessionstore.go +++ b/pkg/libsignalgo/sessionstore.go @@ -38,7 +38,8 @@ type SessionStore interface { //export signal_load_session_callback func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSessionRecord, address C.SignalMutPointerProtocolAddress) C.int { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { - record, err := store.LoadSession(ctx, &Address{ptr: address.raw}) + addr := wrapAddress(address.raw) + record, err := store.LoadSession(ctx, addr) if err == nil && record != nil { record.CancelFinalizer() recordp.raw = record.ptr @@ -50,12 +51,9 @@ func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutP //export signal_store_session_callback func signal_store_session_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, sessionRecord C.SignalMutPointerSessionRecord) C.int { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { - record := SessionRecord{ptr: sessionRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreSession(ctx, &Address{ptr: address.raw}, cloned) + addr := wrapAddress(address.raw) + record := wrapSessionRecord(sessionRecord.raw) + return store.StoreSession(ctx, addr, record) }) } diff --git a/pkg/libsignalgo/signedprekeystore.go b/pkg/libsignalgo/signedprekeystore.go index b1306e2..a66b9f4 100644 --- a/pkg/libsignalgo/signedprekeystore.go +++ b/pkg/libsignalgo/signedprekeystore.go @@ -50,12 +50,8 @@ func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.Signal //export signal_store_signed_pre_key_callback func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerSignedPreKeyRecord) C.int { return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error { - record := SignedPreKeyRecord{ptr: preKeyRecord.raw} - cloned, err := record.Clone() - if err != nil { - return err - } - return store.StoreSignedPreKey(ctx, uint32(id), cloned) + record := wrapSignedPreKeyRecord(preKeyRecord.raw) + return store.StoreSignedPreKey(ctx, uint32(id), record) }) } From 887a0d1c338a4ef4bbefb81c53f0f99e19ef2c00 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Wed, 21 Oct 2026 23:40:25 +0200 Subject: [PATCH 04/46] capabilities: allow m4a and wav audio files (#678) --- pkg/connector/capabilities.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index b62d34f..4ff4b3c 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -40,7 +40,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.signal.capabilities.2026_10_16" + base := "fi.mau.signal.capabilities.2026_10_21" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -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, }, @@ -263,5 +265,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities } func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { - return 1, 14 + return 1, 15 } From 2ef3d656867b13950339967496d24c6fbf587c5f Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 22 Oct 2026 22:10:10 +0200 Subject: [PATCH 05/46] connector: map errors to message statuses, resperrors and bridge states (#683) --- pkg/connector/chatinfo.go | 7 +- pkg/connector/client.go | 32 +++-- pkg/connector/errors.go | 171 ++++++++++++++++++++++++++ pkg/connector/handlematrix.go | 58 ++++----- pkg/connector/handlesignal.go | 3 +- pkg/connector/login.go | 2 +- pkg/msgconv/from-matrix.go | 6 +- pkg/signalmeow/keys.go | 1 + pkg/signalmeow/senderkey.go | 4 +- pkg/signalmeow/sending.go | 21 +++- pkg/signalmeow/web/signalwebsocket.go | 35 ++++-- pkg/signalmeow/web/web.go | 10 ++ 12 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 pkg/connector/errors.go diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 759cdf9..4668cd7 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -220,7 +220,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 +243,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 +389,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 +419,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, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index d915576..61cc282 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -182,9 +182,9 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone { s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState") if err == nil { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "signal-disconnected"}) } else { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(connectionErrorState(err)) } } } @@ -192,11 +192,7 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec case signalmeow.SignalConnectionEventLoggedOut: s.stopChatSync() s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") - if err == nil { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) - } else { - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: err.Error()}) - } + s.sendLoggedOutState(err) err = s.Client.ClearKeysAndDisconnect(context.TODO()) if err != nil { s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect") @@ -204,18 +200,24 @@ 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()}) + if isUnauthorized(err) { + s.stopChatSync() + s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") + s.sendLoggedOutState(err) + } else { + s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") + 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 +225,7 @@ 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 } s.updateRemoteProfile(ctx, false) @@ -251,9 +253,13 @@ 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) return fmt.Errorf("authed websocket logged out: %w", status.Err) case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: log.Err(status.Err).Msg("Authed websocket error") + if isUnauthorized(status.Err) { + s.sendLoggedOutState(status.Err) + } return fmt.Errorf("authed websocket errored: %w", status.Err) case web.SignalWebsocketConnectionEventCleanShutdown: log.Info().Msg("Authed websocket clean shutdown") @@ -315,7 +321,7 @@ func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSy ch, err := s.Client.StartReceiveLoops(ctx) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()}) + s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "signal-connect-failed", Info: map[string]any{"go_error": err.Error()}}) retryInSeconds := 2 << retryCount if retryInSeconds > 150 { retryInSeconds = 150 diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go new file mode 100644 index 0000000..25092ff --- /dev/null +++ b/pkg/connector/errors.go @@ -0,0 +1,171 @@ +// mautrix-signal - A Matrix-Signal puppeting bridge. +// Copyright (C) 2026 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package connector + +import ( + "errors" + "fmt" + "net/http" + + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/status" + "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-signal/pkg/signalmeow" + "go.mau.fi/mautrix-signal/pkg/signalmeow/web" +) + +func init() { + status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{ + "signal-logged-out": "You have been logged out of Signal, please reconnect", + "signal-not-logged-in": "You're not logged into Signal", + "signal-disconnected": "Disconnected from Signal, reconnecting", + "signal-connection-error": "Connection to Signal failed, reconnecting", + "signal-server-error": "Signal server error, reconnecting", + "signal-rate-limited": "Rate limited by Signal, reconnecting", + "signal-connect-failed": "Failed to connect to Signal, retrying", + "signal-connection-failed": "Connection to Signal failed", + }) +} + +var ( + errEditOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only edit your own messages")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errDeleteOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only delete your own messages")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errInviteToDM = bridgev2.WrapErrorInStatus(errors.New("can't invite more users to Signal DMs")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusUnsupported). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + errUnsupportedDisappearingType = bridgev2.WrapErrorInStatus(errors.New("only disappearing timers that start after reading are supported on Signal")). + WithErrorAsMessage(). + WithErrorReason(event.MessageStatusUnsupported). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) +) + +func wrapConversionError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + return bridgev2.WrapErrorInStatus(err). + WithMessage("failed to convert message for Signal"). + WithIsCertain(true). + WithSendNotice(true) +} + +func wrapSendError(err error) error { + if err == nil || errors.As(err, &bridgev2.MessageStatus{}) { + return err + } + switch { + case errors.Is(err, signalmeow.ErrUnregisteredUser): + return bridgev2.WrapErrorInStatus(err). + WithMessage("recipient isn't registered on Signal"). + WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.ErrRateLimited), errors.Is(err, signalmeow.RateLimitError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("rate limited by Signal, try again later"). + WithErrorReason(event.MessageStatusNetworkError). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.AuthorizationFailedError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("you don't have permission to do that in this Signal group"). + WithErrorReason(event.MessageStatusNoPermission). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + case errors.Is(err, signalmeow.GroupPatchNotAcceptedError): + return bridgev2.WrapErrorInStatus(err). + WithMessage("Signal rejected the group change"). + WithErrorReason(event.MessageStatusNetworkError). + WithStatus(event.MessageStatusFail). + WithIsCertain(true). + WithSendNotice(true) + default: + return bridgev2.WrapErrorInStatus(err). + WithMessage("failed to send to Signal"). + WithSendNotice(true) + } +} + +func wrapRespError(err error) error { + var mapped bridgev2.RespError + var cdsRateLimit signalmeow.ContactDiscoveryRateLimitError + switch { + case errors.As(err, &cdsRateLimit), errors.Is(err, signalmeow.RateLimitError), errors.Is(err, signalmeow.ErrRateLimited), + grpcstatus.Code(err) == codes.ResourceExhausted: + mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithStatus(http.StatusBadRequest)) + case errors.Is(err, signalmeow.AuthorizationFailedError): + mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("Signal denied the request")) + default: + mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to Signal failed")) + } + return fmt.Errorf("%w: %w", mapped, err) +} + +func 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 isUnauthorized(err error) bool { + var statusErr web.HTTPStatusError + return errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusUnauthorized +} + +func (s *SignalClient) sendLoggedOutState(err error) { + state := status.BridgeState{ + StateEvent: status.StateBadCredentials, + Error: "signal-logged-out", + UserAction: status.UserActionRelogin, + } + if err != nil { + state.Info = map[string]any{"go_error": err.Error()} + } + s.UserLogin.BridgeState.Send(state) +} diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 8812447..b384d58 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -82,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") @@ -124,7 +124,7 @@ 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: msgconv.ContainsAttachments(converted), @@ -147,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, @@ -170,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 { @@ -188,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 != "" { @@ -199,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 @@ -208,7 +208,7 @@ 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) @@ -277,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 } @@ -298,10 +298,7 @@ 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 { @@ -309,7 +306,7 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg 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") + return errDeleteOthersMessage } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ @@ -318,10 +315,7 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg TargetSentTimestamp: proto.Uint64(targetSentTimestamp), }, })) - if err != nil { - return err - } - return nil + return wrapSendError(err) } func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error { @@ -404,7 +398,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 @@ -446,7 +440,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{ @@ -473,7 +467,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 } @@ -602,7 +596,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) @@ -673,7 +667,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 @@ -724,7 +718,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 { @@ -751,7 +745,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 @@ -776,7 +770,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 } @@ -786,7 +780,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)) } } @@ -864,7 +858,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 @@ -968,7 +962,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) @@ -997,7 +991,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? } @@ -1034,7 +1028,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 @@ -1056,7 +1050,7 @@ func (s *SignalClient) reportSpam(ctx context.Context, portal *bridgev2.Portal, continue } if err = s.Client.ReportSpam(ctx, aci, meta.ServerGUID, meta.ReportingToken); err != nil { - return err + return wrapSendError(err) } seen[meta.ServerGUID] = true if len(seen) == 3 { diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go index a25b77a..3704800 100644 --- a/pkg/connector/handlesignal.go +++ b/pkg/connector/handlesignal.go @@ -34,7 +34,6 @@ import ( "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/simplevent" - "maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-signal/pkg/libsignalgo" @@ -74,7 +73,7 @@ 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 default: diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 8c6bdab..dd636b3 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -107,7 +107,7 @@ var ( ErrDeviceLinkRateLimited = bridgev2.RespError{ ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED", Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again", - StatusCode: http.StatusTooManyRequests, + StatusCode: http.StatusBadRequest, } ErrDeviceLinkRejected = bridgev2.RespError{ ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED", diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 0aab1f1..f755bbb 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -204,7 +204,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. if content.MSC3245Voice != nil && mime != "audio/aac" && ffmpeg.Supported() { data, err = ffmpeg.ConvertBytes(ctx, data, ".aac", []string{}, []string{"-c:a", "aac"}, mime) if err != nil { - return nil, err + return nil, fmt.Errorf("%w (voice to aac): %w", bridgev2.ErrMediaConvertFailed, err) } mime = "audio/aac" fileName += ".aac" @@ -214,7 +214,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. // allowed case "image/gif": if !ffmpeg.Supported() { - return nil, fmt.Errorf("converting gif stickers is not supported") + return nil, fmt.Errorf("%w: converting gif stickers is not supported", bridgev2.ErrUnsupportedMediaType) } data, err = ffmpeg.ConvertBytes(ctx, data, ".apng", []string{}, []string{}, mime) if err != nil { @@ -223,7 +223,7 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event. fileName += ".apng" mime = "image/apng" default: - return nil, fmt.Errorf("unsupported content type for sticker %s", mime) + return nil, fmt.Errorf("%w for sticker: %s", bridgev2.ErrUnsupportedMediaType, mime) } } att, err := getClient(ctx).UploadAttachment(ctx, data) diff --git a/pkg/signalmeow/keys.go b/pkg/signalmeow/keys.go index 84a242a..2a04c4e 100644 --- a/pkg/signalmeow/keys.go +++ b/pkg/signalmeow/keys.go @@ -408,6 +408,7 @@ func addBase64PaddingAndDecode(data string) ([]byte, error) { var ( ErrUnregisteredUser = errors.New("user is unregistered") ErrDevicesChanged = errors.New("device list changed while sending skdm") + ErrRateLimited = errors.New("rate limited by server") ) func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID libsignalgo.ServiceID, specificDeviceID int) error { diff --git a/pkg/signalmeow/senderkey.go b/pkg/signalmeow/senderkey.go index 344bacf..9d46005 100644 --- a/pkg/signalmeow/senderkey.go +++ b/pkg/signalmeow/senderkey.go @@ -249,7 +249,7 @@ func (cli *Client) sendToGroupWithSenderKey( cli.Store.RecipientStore.MarkUnregistered(ctx, serviceID, true) result.FailedToSendTo = append(result.FailedToSendTo, FailedSendResult{ Recipient: serviceID, - Error: fmt.Errorf("multi-recipient send 404"), + Error: fmt.Errorf("%w (multi-recipient send returned 404)", ErrUnregisteredUser), }) } else { result.SuccessfullySentTo = append(result.SuccessfullySentTo, SuccessfulSendResult{ @@ -277,6 +277,8 @@ func (cli *Client) sendToGroupWithSenderKey( doUnlock() // Retry recursively after fixing device lists return cli.sendToGroupWithSenderKey(ctx, groupID, allRecipients, sec, content, messageTimestamp, retries+1) + case 413, 428, 429: + return nil, fmt.Errorf("%w (multi-recipient send returned %d)", ErrRateLimited, resp.GetStatus()) default: return nil, fmt.Errorf("unexpected status code %d in multi-recipient send", resp.GetStatus()) } diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index f48a252..115b7e5 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -297,6 +297,20 @@ type GroupMessageSendResult struct { FailedToSendTo []FailedSendResult } +// FailureError returns an error from FailedToSendTo, preferring errors other than ErrUnregisteredUser, +// so that a permanent failure is only reported if every recipient failed permanently. +func (r *GroupMessageSendResult) FailureError() error { + if len(r.FailedToSendTo) == 0 { + return nil + } + for _, failure := range r.FailedToSendTo { + if !errors.Is(failure.Error, ErrUnregisteredUser) { + return failure.Error + } + } + return r.FailedToSendTo[len(r.FailedToSendTo)-1].Error +} + type SendResult interface { isSendResult() } @@ -658,8 +672,7 @@ func (cli *Client) sendToGroup( return result, nil // I only sent to myself } if len(result.SuccessfullySentTo) == 0 { - lastError := result.FailedToSendTo[len(result.FailedToSendTo)-1].Error - return nil, fmt.Errorf("failed to send to any group members: %w", lastError) + return nil, fmt.Errorf("failed to send to any group members: %w", result.FailureError()) } return result, nil @@ -1037,6 +1050,8 @@ func (cli *Client) sendContent( } cli.Store.RecipientStore.MarkUnregistered(ctx, recipient, true) return sentUnidentified, fmt.Errorf("%w (send returned 404)", ErrUnregisteredUser) + } else if *response.Status == 413 || *response.Status == 429 { + return sentUnidentified, fmt.Errorf("%w (send returned %d)", ErrRateLimited, *response.Status) } else if *response.Status != 200 { return sentUnidentified, fmt.Errorf("unexpected status code while sending: %d", *response.Status) } @@ -1189,7 +1204,7 @@ func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceI // } // } //} - return fmt.Errorf("got 428 error") + return fmt.Errorf("%w (send returned 428, retry after %ds)", ErrRateLimited, retryAfterSeconds) } func (cli *Client) ReportSpam(ctx context.Context, aci uuid.UUID, serverGUID string, token []byte) error { diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index 3231322..819fca7 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -222,10 +222,12 @@ func (s *SignalWebsocket) connectLoop( return } if request == nil { - log.Fatal().Msg("Received nil request") + log.Error().Msg("Received nil request") + continue } if requestHandler == nil { - log.Fatal().Msg("Received request but no handler") + log.Error().Uint64("request_id", request.GetId()).Msg("Received request but no handler") + continue } // Handle the request with the request handler function @@ -287,20 +289,21 @@ func (s *SignalWebsocket) connectLoop( if resp != nil { if resp.StatusCode != 101 { // Server didn't want to open websocket - if resp.StatusCode >= 500 { - // We can try again if it's a 5xx - s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, fmt.Errorf("5xx opening websocket: %v", resp.Status)) + statusErr := HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status} + if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests { + // We can try again if it's a 5xx or a rate limit + s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, fmt.Errorf("error opening websocket: %w", statusErr)) } else if resp.StatusCode == 403 { // We are logged out, so we should stop trying to reconnect s.pushStatus(ctx, SignalWebsocketConnectionEventLoggedOut, fmt.Errorf("403 opening websocket, we are logged out")) return // NOT RETRYING, KILLING THE CONNECTION LOOP } else if resp.StatusCode > 0 && resp.StatusCode < 500 { // Unexpected status code - s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("unexpected status opening websocket: %v", resp.Status)) + s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("unexpected status opening websocket: %w", statusErr)) return // NOT RETRYING, KILLING THE CONNECTION LOOP } else { // Something is very wrong - s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected error opening websocket: %v", resp.Status)) + s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected error opening websocket: %w", statusErr)) } // Retry the connection retrying = true @@ -323,6 +326,7 @@ func (s *SignalWebsocket) connectLoop( s.ws.Store(ws) retrying = false backoff = initialBackoff + connectedAt := time.Now() responseChannels := exsync.NewMap[uint64, chan *signalpb.WebSocketResponseMessage]() loopCtx, loopCancel := context.WithCancelCause(ctx) @@ -402,12 +406,16 @@ func (s *SignalWebsocket) connectLoop( if ctxCauseErr == nil || errors.Is(ctxCauseErr, context.Canceled) { s.pushStatus(ctx, SignalWebsocketConnectionEventCleanShutdown, nil) } else { - errorCount++ + if time.Since(connectedAt) > time.Minute { + errorCount = 0 + } s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, ctxCauseErr) if errors.Is(ctxCauseErr, ErrForcedReconnect) { // Skip the delay for forced reconnects // TODO should the delay be lowered globally? isFirstConnect = true + } else { + errorCount++ } } @@ -420,10 +428,12 @@ func (s *SignalWebsocket) connectLoop( wg.Wait() log.Debug().Msg("Finished websocket cleanup") if errorCount > 500 { - // Something is really wrong, we better panic. + // Something is really wrong, stop reconnecting. // This is a last defense against a runaway error loop, // like the WS continually closing and reconnecting - log.Fatal().Int("error_count", errorCount).Msg("Too many errors, panicking") + log.Error().Int("error_count", errorCount).Msg("Too many errors, stopping connection loop") + s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("too many websocket errors (%d)", errorCount)) + return } } } @@ -467,10 +477,10 @@ func readLoop( incomingRequestChan <- msg.Request } else if *msg.Type == signalpb.WebSocketMessage_RESPONSE { if msg.Response == nil { - log.Fatal().Msg("Received response with no response") + return errors.New("received response message with no response") } if msg.Response.Id == nil { - log.Fatal().Msg("Received response with no id") + return errors.New("received response message with no id") } responseChannel, ok := responseChannels.Pop(*msg.Response.Id) if !ok { @@ -678,6 +688,7 @@ func OpenWebsocket(ctx context.Context, url string) (*websocket.Conn, *http.Resp func CreateWSResponse(ctx context.Context, id uint64, status int) *signalpb.WebSocketMessage { if status != 200 && status != 400 { // TODO support more responses to Signal? Are there more? + //bridgelint:ignore GR004 statuses come from the bridge's own handlers, so anything else is a bridge bug zerolog.Ctx(ctx).Fatal().Int("status", status).Msg("Error creating response. Non 200/400 not supported yet.") return nil } diff --git a/pkg/signalmeow/web/web.go b/pkg/signalmeow/web/web.go index d30e367..684cd66 100644 --- a/pkg/signalmeow/web/web.go +++ b/pkg/signalmeow/web/web.go @@ -157,6 +157,16 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe return resp, nil } +// HTTPStatusError is returned when a Signal server responds with an unexpected HTTP status. +type HTTPStatusError struct { + StatusCode int + Status string +} + +func (e HTTPStatusError) Error() string { + return e.Status +} + func CloseBody(resp *http.Response) { if resp != nil && resp.Body != nil { _ = resp.Body.Close() From 7e65e8ecc9802dc952565d523c20661c2a4aae26 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 24 Oct 2026 10:38:11 +0000 Subject: [PATCH 06/46] signalmeow/web: don't block read loop exit on full request queue (#685) --- pkg/signalmeow/web/signalwebsocket.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index 819fca7..e17c45b 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -474,7 +474,11 @@ func readLoop( Str("request_verb", *msg.Request.Verb). Str("request_path", *msg.Request.Path). Msg("Received WS request") - incomingRequestChan <- msg.Request + select { + case incomingRequestChan <- msg.Request: + case <-ctx.Done(): + return ctx.Err() + } } else if *msg.Type == signalpb.WebSocketMessage_RESPONSE { if msg.Response == nil { return errors.New("received response message with no response") From ccb17921200f871b3ce9f7d4ee41c5071fa7422d Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 06:23:57 -0400 Subject: [PATCH 07/46] signalmeow/senderkey: retry multi-recipient sends on server errors (#684) --- pkg/signalmeow/senderkey.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/signalmeow/senderkey.go b/pkg/signalmeow/senderkey.go index 9d46005..2eefadd 100644 --- a/pkg/signalmeow/senderkey.go +++ b/pkg/signalmeow/senderkey.go @@ -279,6 +279,11 @@ func (cli *Client) sendToGroupWithSenderKey( return cli.sendToGroupWithSenderKey(ctx, groupID, allRecipients, sec, content, messageTimestamp, retries+1) case 413, 428, 429: return nil, fmt.Errorf("%w (multi-recipient send returned %d)", ErrRateLimited, resp.GetStatus()) + case 500, 503: + log.Warn().Uint32("status_code", resp.GetStatus()). + Msg("Multi-recipient send failed with server error, retrying") + doUnlock() + return cli.sendToGroupWithSenderKey(ctx, groupID, allRecipients, sec, content, messageTimestamp, retries+1) default: return nil, fmt.Errorf("unexpected status code %d in multi-recipient send", resp.GetStatus()) } From 9f44495d18e58528f2bb6c79c89c9a3991be28be Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 26 Oct 2026 14:17:42 +0200 Subject: [PATCH 08/46] msgconv/from-signal: add support for encrypted direct media --- go.mod | 2 +- go.sum | 4 ++-- pkg/msgconv/from-signal.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 89ed63d..692d50d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.0 + maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae ) require ( diff --git a/go.sum b/go.sum index e5a7857..df2c86b 100644 --- a/go.sum +++ b/go.sum @@ -95,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.0 h1:xuAZzzyWce0pFkBhV/MCLFiJdVs2rFkY3Eep7hNng4I= -maunium.net/go/mautrix v0.32.0/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae h1:4SY5+nh2B968L5NmMGmF0WPRbV9D1yFLP8U+JASuHXg= +maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index 7c8a7a6..e34b04f 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -607,7 +607,7 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp if err != nil { return nil, err } - content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID) + content.URL, content.File, err = mc.Bridge.GenerateContentURI(ctx, getPortal(ctx).MXID, mediaID) } else { err = mc.actuallyReuploadAttachment(ctx, content, att, attMap) if err != nil { From 45d6a02f1220594230909c2a219ad8f630377cd9 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 09:54:39 -0400 Subject: [PATCH 09/46] matrixfmt: don't add trailing separator when joining entity strings (#688) --- pkg/msgconv/matrixfmt/html.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/matrixfmt/html.go b/pkg/msgconv/matrixfmt/html.go index b60c0ad..9a54274 100644 --- a/pkg/msgconv/matrixfmt/html.go +++ b/pkg/msgconv/matrixfmt/html.go @@ -134,12 +134,14 @@ func JoinEntityString(with string, strings ...*EntityString) *EntityString { continue } DebugLog(" + %q %+v\n", s.String, s.Entities) + if len(str) > 0 { + str = append(str, withUTF16...) + } for _, entity := range s.Entities { entity.Start += len(str) entities = append(entities, entity) } str = append(str, s.String...) - str = append(str, withUTF16...) } DebugLog(" -> %q %+v\n", str, entities) return &EntityString{ From 6c5841d1b758a19fe946ad1f4547b154a84aea46 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Mon, 26 Oct 2026 16:22:45 +0000 Subject: [PATCH 10/46] backfill: mark read from the newest item and pass ReadUpTo (#689) --- go.mod | 2 +- go.sum | 4 ++-- pkg/connector/backfill.go | 19 ++++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 692d50d..2847afc 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae + maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d ) require ( diff --git a/go.sum b/go.sum index df2c86b..0da95ea 100644 --- a/go.sum +++ b/go.sum @@ -95,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae h1:4SY5+nh2B968L5NmMGmF0WPRbV9D1yFLP8U+JASuHXg= -maunium.net/go/mautrix v0.32.1-0.20261026114918-8c92262134ae/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY= +maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index b7b2a23..b566850 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -77,8 +77,8 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM } // GetBackupChatItems returns in reverse chronological order, so flip the list slices.Reverse(items) - var firstDirectionfulProcessed bool var isRead bool + var readUpTo time.Time convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items)) attMap := make(msgconv.AttachmentMap) recipientMap := make(map[uint64]*backuppb.Recipient) @@ -123,20 +123,17 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM case *backuppb.ChatItem_Incoming: streamOrder = int64(dt.Incoming.GetDateServerSent()) prevStreamOrder = streamOrder - if !firstDirectionfulProcessed { - firstDirectionfulProcessed = true - isRead = dt.Incoming.Read + isRead = dt.Incoming.Read + if isRead { + readUpTo = time.UnixMilli(int64(item.DateSent)) } case *backuppb.ChatItem_Outgoing: streamOrder = int64(item.GetDateSent()) // Ensure stream order is higher than previous incoming item, but lower than next incoming item streamOrder = min(streamOrder, findNextStreamOrder(i+1)-1) streamOrder = max(streamOrder, prevStreamOrder+1) - - if !firstDirectionfulProcessed { - firstDirectionfulProcessed = true - isRead = true - } + isRead = true + readUpTo = time.UnixMilli(int64(item.DateSent)) } if len(attMap) > 0 { clear(attMap) @@ -178,11 +175,15 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM Reactions: convertedReactions, }) } + if isRead { + readUpTo = time.Time{} + } return &bridgev2.FetchMessagesResponse{ Messages: convertedMessages, HasMore: len(items) >= params.Count, Forward: params.Forward, MarkRead: isRead, + ReadUpTo: readUpTo, ApproxTotalCount: chat.TotalMessages, CompleteCallback: func() { // When reaching the last backwards backfill batch, delete the chat from the backup store. From 6dc9ac3e0c9a31a5ba59287091282d624e83c706 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 27 Oct 2026 12:28:43 +0000 Subject: [PATCH 11/46] signalmeow/web: make request counter atomic (#687) --- pkg/signalmeow/web/web.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/signalmeow/web/web.go b/pkg/signalmeow/web/web.go index 684cd66..9463071 100644 --- a/pkg/signalmeow/web/web.go +++ b/pkg/signalmeow/web/web.go @@ -28,6 +28,7 @@ import ( "net/http" "runtime" "strings" + "sync/atomic" "time" "github.com/rs/zerolog" @@ -99,7 +100,7 @@ type HTTPReqOpt struct { OverrideURL string // Override the full URL, if set ignores path and Host } -var httpReqCounter = 0 +var httpReqCounter atomic.Int64 func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPReqOpt) (*http.Response, error) { // Set defaults @@ -143,8 +144,7 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe req.SetBasicAuth(*opt.Username, *opt.Password) } - httpReqCounter++ - log = log.With().Int("request_number", httpReqCounter).Logger() + log = log.With().Int64("request_number", httpReqCounter.Add(1)).Logger() log.Trace().Msg("Sending HTTP request") start := time.Now() resp, err := SignalHTTPClient.Do(req) @@ -252,9 +252,8 @@ func GetAttachment(ctx context.Context, path string, cdnNumber uint32) (*http.Re req.Header.Add("Content-Type", "application/octet-stream") req.Header.Set("User-Agent", UserAgent) - httpReqCounter++ log = log.With(). - Int("request_number", httpReqCounter). + Int64("request_number", httpReqCounter.Add(1)). Str("url", urlStr). Logger() From c2b2792f0b4f82f11ed85f449ab55aad7a7f98b9 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 29 Oct 2026 12:59:59 +0000 Subject: [PATCH 12/46] client,signalmeow/web: log out on upgrade 401 and retry other upgrade errors (#693) Co-authored-by: Claude Opus 5.5 --- pkg/connector/client.go | 17 ++++++----------- pkg/connector/errors.go | 5 ----- pkg/signalmeow/web/signalwebsocket.go | 17 +++++++---------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index 61cc282..e6c5b5e 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -203,14 +203,8 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec s.UserLogin.BridgeState.Send(connectionErrorState(err)) case signalmeow.SignalConnectionEventFatalError: - if isUnauthorized(err) { - s.stopChatSync() - s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") - s.sendLoggedOutState(err) - } else { - s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") - s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "signal-connection-failed", Info: map[string]any{"go_error": err.Error()}}) - } + s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") + 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() { @@ -254,12 +248,13 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec 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") - if isUnauthorized(status.Err) { - s.sendLoggedOutState(status.Err) - } return fmt.Errorf("authed websocket errored: %w", status.Err) case web.SignalWebsocketConnectionEventCleanShutdown: log.Info().Msg("Authed websocket clean shutdown") diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go index 25092ff..b4783d4 100644 --- a/pkg/connector/errors.go +++ b/pkg/connector/errors.go @@ -153,11 +153,6 @@ func connectionErrorState(err error) status.BridgeState { return state } -func isUnauthorized(err error) bool { - var statusErr web.HTTPStatusError - return errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusUnauthorized -} - func (s *SignalClient) sendLoggedOutState(err error) { state := status.BridgeState{ StateEvent: status.StateBadCredentials, diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index e17c45b..0396307 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -256,6 +256,7 @@ func (s *SignalWebsocket) connectLoop( retrying := false errorCount := 0 isFirstConnect := true + var connectedAt time.Time wsURL := (&url.URL{ Scheme: "wss", Host: APIHostname, @@ -273,7 +274,7 @@ func (s *SignalWebsocket) connectLoop( case <-ctx.Done(): } backoff += backoffIncrement - } else if !isFirstConnect && s.basicAuth != nil { + } else if !isFirstConnect && (s.basicAuth != nil || time.Since(connectedAt) < time.Minute) { select { case <-time.After(initialBackoff): case <-ctx.Done(): @@ -293,17 +294,13 @@ func (s *SignalWebsocket) connectLoop( if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests { // We can try again if it's a 5xx or a rate limit s.pushStatus(ctx, SignalWebsocketConnectionEventDisconnected, fmt.Errorf("error opening websocket: %w", statusErr)) - } else if resp.StatusCode == 403 { + } else if resp.StatusCode == http.StatusForbidden || (resp.StatusCode == http.StatusUnauthorized && s.basicAuth != nil) { // We are logged out, so we should stop trying to reconnect - s.pushStatus(ctx, SignalWebsocketConnectionEventLoggedOut, fmt.Errorf("403 opening websocket, we are logged out")) - return // NOT RETRYING, KILLING THE CONNECTION LOOP - } else if resp.StatusCode > 0 && resp.StatusCode < 500 { - // Unexpected status code - s.pushStatus(ctx, SignalWebsocketConnectionEventFatalError, fmt.Errorf("unexpected status opening websocket: %w", statusErr)) + s.pushStatus(ctx, SignalWebsocketConnectionEventLoggedOut, fmt.Errorf("%w opening websocket, we are logged out", statusErr)) return // NOT RETRYING, KILLING THE CONNECTION LOOP } else { - // Something is very wrong - s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected error opening websocket: %w", statusErr)) + // Unexpected status code + s.pushStatus(ctx, SignalWebsocketConnectionEventError, fmt.Errorf("unexpected status opening websocket: %w", statusErr)) } // Retry the connection retrying = true @@ -326,7 +323,7 @@ func (s *SignalWebsocket) connectLoop( s.ws.Store(ws) retrying = false backoff = initialBackoff - connectedAt := time.Now() + connectedAt = time.Now() responseChannels := exsync.NewMap[uint64, chan *signalpb.WebSocketResponseMessage]() loopCtx, loopCancel := context.WithCancelCause(ctx) From 357eb58a446f90fa517dc0f08d7e0e732bc2b3d0 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 29 Oct 2026 13:22:11 +0000 Subject: [PATCH 13/46] directmedia: map upstream download failures to matrix errors (#694) Co-authored-by: Claude Opus 5.5 --- pkg/connector/directmedia.go | 6 +++--- pkg/connector/errors.go | 17 +++++++++++++++++ pkg/signalmeow/groups.go | 2 +- pkg/signalmeow/profile.go | 2 +- pkg/signalmeow/sticker.go | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 05e2a07..82ed16d 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -79,7 +79,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } case *signalid.DirectMediaProfileAvatar: log.Info(). @@ -107,7 +107,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } case *signalid.DirectMediaSticker: log.Info(). @@ -118,7 +118,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID) if err != nil { log.Err(err).Msg("Direct download failed") - return nil, err + return nil, wrapDirectMediaError(err) } default: return nil, fmt.Errorf("no downloader for direct media type: %T", info) diff --git a/pkg/connector/errors.go b/pkg/connector/errors.go index b4783d4..c06ee17 100644 --- a/pkg/connector/errors.go +++ b/pkg/connector/errors.go @@ -136,6 +136,23 @@ func wrapRespError(err error) error { 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, diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 493ecbb..0849f74 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -707,7 +707,7 @@ func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, g return nil, fmt.Errorf("failed to send request: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected response status: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } encryptedAvatar, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/signalmeow/profile.go b/pkg/signalmeow/profile.go index a515f5a..a0878b6 100644 --- a/pkg/signalmeow/profile.go +++ b/pkg/signalmeow/profile.go @@ -289,7 +289,7 @@ func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, pr } defer web.CloseBody(resp) if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected response status: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } encryptedAvatar, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/signalmeow/sticker.go b/pkg/signalmeow/sticker.go index e1364b1..f50fafc 100644 --- a/pkg/signalmeow/sticker.go +++ b/pkg/signalmeow/sticker.go @@ -72,7 +72,7 @@ func downloadStickerData(ctx context.Context, path string, packKey []byte) ([]by if err != nil { return nil, fmt.Errorf("failed to make request: %w", err) } else if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) + return nil, fmt.Errorf("unexpected status code: %w", web.HTTPStatusError{StatusCode: resp.StatusCode, Status: resp.Status}) } else if body, err = io.ReadAll(resp.Body); err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } else if decrypted, err = decryptSticker(packKey, body); err != nil { From e2319d6cde31534d7243933fa3eee8118d1435ec Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 29 Oct 2026 13:27:39 +0000 Subject: [PATCH 14/46] signalmeow: handle closed websocket status channels (#691) --- pkg/signalmeow/receiving.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index f0d69df..857bc4b 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -154,7 +154,10 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection case <-loopCtx.Done(): log.Info().Msg("Context done, exiting websocket status loop") return - case status := <-authChan: + case status, ok := <-authChan: + if !ok { + return + } lastAuthStatus = status currentStatus = status @@ -182,7 +185,11 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection default: } } - case status := <-unauthChan: + case status, ok := <-unauthChan: + if !ok { + unauthChan = nil + continue + } lastUnauthStatus = status currentStatus = status From 572a190db285cf22a9b817a9c2abec5532d4e354 Mon Sep 17 00:00:00 2001 From: Highest <91629626+highesttt@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:40:25 -0400 Subject: [PATCH 15/46] msgconv: clarify outgoing view-once placeholders (#679) --- pkg/msgconv/from-signal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index e34b04f..251e9dd 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -99,7 +99,9 @@ func (mc *MessageConverter) ToMatrix( } if dm.GetIsViewOnce() { var unavailableReason string - if len(dm.GetAttachments()) == 0 || sender == client.Store.ACI || + if sender == client.Store.ACI { + unavailableReason = "Sent a view-once message." + } else if len(dm.GetAttachments()) == 0 || !time.UnixMilli(int64(dm.GetTimestamp())).Add(45*24*time.Hour).After(time.Now()) { unavailableReason = "View-once media is no longer available." } else if len(dm.GetAttachments()) != 1 || dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil || From 1cc623ed01777a2170a69589ee9c0a21f650b817 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 30 Oct 2026 14:15:20 +0200 Subject: [PATCH 16/46] signalmeow/web: only log error responses by default Closes #696 --- pkg/signalmeow/web/signalwebsocket.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index 0396307..7ee54c7 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -493,9 +493,9 @@ func readLoop( logEvt := log.Debug(). Uint64("response_id", msg.Response.GetId()). Uint32("response_status", msg.Response.GetStatus()). - Str("response_message", msg.Response.GetMessage()) - if log.GetLevel() == zerolog.TraceLevel || len(msg.Response.Body) < 256 { - logEvt.Strs("response_headers", msg.Response.Headers) + Str("response_message", msg.Response.GetMessage()). + Strs("response_headers", msg.Response.Headers) + if log.GetLevel() == zerolog.TraceLevel || msg.Response.GetStatus() >= 400 { if json.Valid(msg.Response.Body) { logEvt.RawJSON("response_body", msg.Response.Body) } else { From 880ea0dceb5be9d4329501b59b959563202e9d3c Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sun, 1 Nov 2026 12:47:23 +0000 Subject: [PATCH 17/46] signalmeow/receiving: ignore hangups for calls accepted on another device (#700) --- pkg/signalmeow/receiving.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index 857bc4b..1f55b26 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -651,7 +651,7 @@ func (cli *Client) handleDecryptedResult( }) } case *signalpb.Content_CallMessage: - if !isBlocked && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) { + if !isBlocked && (content.CallMessage.Offer != nil || (content.CallMessage.Hangup != nil && content.CallMessage.Hangup.GetType() != signalpb.CallMessage_Hangup_HANGUP_ACCEPTED)) { handlerSuccess = cli.handleEvent(&events.Call{ Info: events.MessageInfo{ Sender: theirServiceID.UUID, From ac200339c5635e1dad1314696fba74dff3b4908c Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:06:19 -0500 Subject: [PATCH 18/46] signalmeow/receiving: use ciphertext message type for retry requests (#697) Co-authored-by: Indent --- pkg/signalmeow/receiving_decrypt.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/signalmeow/receiving_decrypt.go b/pkg/signalmeow/receiving_decrypt.go index f92647c..e6abff4 100644 --- a/pkg/signalmeow/receiving_decrypt.go +++ b/pkg/signalmeow/receiving_decrypt.go @@ -71,12 +71,15 @@ func (cli *Client) decryptEnvelope( } var result *DecryptionResult var bundleType string + var ciphertextType libsignalgo.CiphertextMessageType if *envelope.Type == signalpb.Envelope_PREKEY_MESSAGE { result, err = cli.prekeyDecrypt(ctx, destinationServiceID, sender, envelope.Content, envelope.GetServerTimestamp()) bundleType = "prekey bundle" + ciphertextType = libsignalgo.CiphertextMessageTypePreKey } else { result, err = cli.decryptCiphertextEnvelope(ctx, destinationServiceID, sender, envelope.Content, envelope.GetServerTimestamp()) bundleType = "ciphertext" + ciphertextType = libsignalgo.CiphertextMessageTypeWhisper } if err != nil { return DecryptionResult{ @@ -84,7 +87,7 @@ func (cli *Client) decryptEnvelope( Err: fmt.Errorf("failed to decrypt %s envelope: %w", bundleType, err), Retriable: true, // TODO should these ever be not retriable? Ciphertext: envelope.Content, - CiphertextType: libsignalgo.CiphertextMessageType(envelope.GetType()), + CiphertextType: ciphertextType, } } return *result From 9dbbb6e4eaea7e179e4efd42171b229a195738fe Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:11:32 -0500 Subject: [PATCH 19/46] signalmeow/keys: upload at most 100 prekeys of each type (#699) Co-authored-by: Indent --- pkg/signalmeow/keys.go | 7 +++++++ pkg/signalmeow/store/prekey_store.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/signalmeow/keys.go b/pkg/signalmeow/keys.go index 2a04c4e..1b2370a 100644 --- a/pkg/signalmeow/keys.go +++ b/pkg/signalmeow/keys.go @@ -64,6 +64,13 @@ func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore return fmt.Errorf("failed to get all kyber prekeys: %w", err) } + if len(preKeys) > PREKEY_BATCH_SIZE { + preKeys = preKeys[len(preKeys)-PREKEY_BATCH_SIZE:] + } + if len(kyberPreKeys) > PREKEY_BATCH_SIZE { + kyberPreKeys = kyberPreKeys[len(kyberPreKeys)-PREKEY_BATCH_SIZE:] + } + // We need to have some keys to upload if len(preKeys) == 0 && len(kyberPreKeys) == 0 { return fmt.Errorf("no prekeys to upload") diff --git a/pkg/signalmeow/store/prekey_store.go b/pkg/signalmeow/store/prekey_store.go index 37bca7e..2cf62a5 100644 --- a/pkg/signalmeow/store/prekey_store.go +++ b/pkg/signalmeow/store/prekey_store.go @@ -51,13 +51,13 @@ type PreKeyStore interface { } const ( - getAllPreKeysQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3` + getAllPreKeysQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3 ORDER BY key_id` getPreKeyQuery = `SELECT key_pair FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3 AND is_signed=$4` insertPreKeyQuery = `INSERT INTO signalmeow_pre_keys (account_id, service_id, key_id, is_signed, key_pair) VALUES ($1, $2, $3, $4, $5)` deletePreKeyQuery = `DELETE FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3 AND is_signed=$4` getLastPreKeyIDQuery = `SELECT COUNT(*), COALESCE(MAX(key_id), 0) FROM signalmeow_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_signed=$3` - getAllKyberPreKeysQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_last_resort=false` + getAllKyberPreKeysQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND is_last_resort=false ORDER BY key_id` getKyberPreKeyQuery = `SELECT key_pair FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3` insertKyberPreKeyQuery = `INSERT INTO signalmeow_kyber_pre_keys (account_id, service_id, key_id, key_pair, is_last_resort) VALUES ($1, $2, $3, $4, $5)` deleteKyberPreKeyQuery = `DELETE FROM signalmeow_kyber_pre_keys WHERE account_id=$1 AND service_id=$2 AND key_id=$3` From d3e8d560baaf87b121a4071cf253735ec132edb1 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:12:33 -0500 Subject: [PATCH 20/46] chatsync: stop waiting for transfer archive after it times out (#698) Co-authored-by: Indent --- pkg/connector/chatsync.go | 12 +++++++++++- pkg/signalmeow/backup.go | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/connector/chatsync.go b/pkg/connector/chatsync.go index 47963d7..ec05b36 100644 --- a/pkg/connector/chatsync.go +++ b/pkg/connector/chatsync.go @@ -19,6 +19,7 @@ package connector import ( "context" "encoding/base64" + "errors" "time" "github.com/google/uuid" @@ -28,6 +29,7 @@ import ( "go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/signalid" + "go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb" "go.mau.fi/mautrix-signal/pkg/signalmeow/types" ) @@ -48,7 +50,15 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) if s.Client.Store.EphemeralBackupKey != nil { zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats") meta, err := s.Client.WaitForTransfer(ctx) - if err != nil { + if errors.Is(err, signalmeow.ErrTransferTimedOut) { + zerolog.Ctx(ctx).Warn().Msg("Transfer archive wasn't uploaded in time, giving up") + s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced = true + err = s.UserLogin.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login metadata after transfer archive timed out") + } + return + } else if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to request transfer archive") return } else if meta.Error != "" { diff --git a/pkg/signalmeow/backup.go b/pkg/signalmeow/backup.go index f003e19..911a888 100644 --- a/pkg/signalmeow/backup.go +++ b/pkg/signalmeow/backup.go @@ -42,6 +42,7 @@ const transferArchiveFetchTimeout = 1 * time.Hour var ( ErrNoEphemeralBackupKey = errors.New("no ephemeral backup key") + ErrTransferTimedOut = errors.New("timed out waiting for transfer archive") ) const ( @@ -271,8 +272,8 @@ func (cli *Client) WaitForTransfer(ctx context.Context) (*TransferArchiveMetadat for { remainingTime := time.Until(timeout) - if remainingTime < 0 { - return nil, fmt.Errorf("timed out") + if remainingTime < time.Second { + return nil, ErrTransferTimedOut } reqStart := time.Now() reqTimeout := min(remainingTime, 5*time.Minute) From a82a7f38a408e28db910152f2638a52bf996249f Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:16:23 -0500 Subject: [PATCH 21/46] signalmeow/web: use a longer response header timeout for attachment uploads (#695) Co-authored-by: Indent --- pkg/connector/connector.go | 3 +++ pkg/signalmeow/attachments.go | 2 ++ pkg/signalmeow/web/web.go | 10 +++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 392a100..3a3823a 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -94,8 +94,11 @@ func (s *SignalConnector) ResetHTTPTransport() { settings = hs.GetHTTPClientSettings() } oldClient := web.SignalHTTPClient + oldUploadClient := web.SignalUploadHTTPClient web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile() + web.SignalUploadHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).WithResponseHeaderTimeout(2 * time.Minute).Compile() oldClient.CloseIdleConnections() + oldUploadClient.CloseIdleConnections() } func (s *SignalConnector) ResetNetworkConnections() { diff --git a/pkg/signalmeow/attachments.go b/pkg/signalmeow/attachments.go index 0b5f7e2..9bfc1fd 100644 --- a/pkg/signalmeow/attachments.go +++ b/pkg/signalmeow/attachments.go @@ -344,6 +344,7 @@ func (cli *Client) uploadAttachmentLegacy( ContentType: web.ContentTypeOctetStream, Username: &username, Password: &password, + Upload: true, }) web.CloseBody(resp) if err != nil { @@ -368,6 +369,7 @@ func (cli *Client) uploadAttachmentTUS( Body: encryptedWithMAC, ContentType: web.ContentTypeOffsetOctetStream, Headers: uploadAttributes.Headers, + Upload: true, }) web.CloseBody(resp) // TODO actually support resuming on error diff --git a/pkg/signalmeow/web/web.go b/pkg/signalmeow/web/web.go index 9463071..fc86457 100644 --- a/pkg/signalmeow/web/web.go +++ b/pkg/signalmeow/web/web.go @@ -75,6 +75,9 @@ var signalTransport = &http.Transport{ var SignalHTTPClient = &http.Client{ Transport: signalTransport, } +var SignalUploadHTTPClient = &http.Client{ + Transport: signalTransport, +} func init() { SignalCertPool.AddCert(exerrors.Must(x509.ParseCertificate(signalRootCertBytes))) @@ -98,6 +101,7 @@ type HTTPReqOpt struct { ContentType ContentType Headers map[string]string OverrideURL string // Override the full URL, if set ignores path and Host + Upload bool } var httpReqCounter atomic.Int64 @@ -146,8 +150,12 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe log = log.With().Int64("request_number", httpReqCounter.Add(1)).Logger() log.Trace().Msg("Sending HTTP request") + client := SignalHTTPClient + if opt.Upload { + client = SignalUploadHTTPClient + } start := time.Now() - resp, err := SignalHTTPClient.Do(req) + resp, err := client.Do(req) dur := time.Since(start) if err != nil { log.Err(err).Dur("duration", dur).Msg("Error sending request") From 4b5f4f74ca40813bd69e5655198964e5bee33816 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:28:08 -0500 Subject: [PATCH 22/46] groupinfo: allow reactions in announcement-only groups (#692) Co-authored-by: Indent --- pkg/connector/groupinfo.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go index d958463..345ba28 100644 --- a/pkg/connector/groupinfo.go +++ b/pkg/connector/groupinfo.go @@ -112,6 +112,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, From 403c050d3b35357a58f1e2f344fa7f7f6c4c6c98 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 31 Oct 2026 19:51:35 +0200 Subject: [PATCH 23/46] libsignal: update to v0.106.1 --- pkg/libsignalgo/libsignal | 2 +- pkg/libsignalgo/libsignal-ffi.h | 190 +++++++++++++++++++++-- pkg/libsignalgo/signalversion/version.go | 2 +- pkg/libsignalgo/update-ffi-docker.sh | 8 +- pkg/libsignalgo/update-ffi-version.sh | 33 ++++ pkg/libsignalgo/update-ffi.sh | 9 +- pkg/libsignalgo/version.go | 13 ++ 7 files changed, 228 insertions(+), 29 deletions(-) create mode 100755 pkg/libsignalgo/update-ffi-version.sh create mode 100644 pkg/libsignalgo/version.go diff --git a/pkg/libsignalgo/libsignal b/pkg/libsignalgo/libsignal index 4beb029..904c8dc 160000 --- a/pkg/libsignalgo/libsignal +++ b/pkg/libsignalgo/libsignal @@ -1 +1 @@ -Subproject commit 4beb029d8a941f81e7d9c6d8af1ed25a677569a8 +Subproject commit 904c8dc8216073b078a5f2f5090e3da8382e6a92 diff --git a/pkg/libsignalgo/libsignal-ffi.h b/pkg/libsignalgo/libsignal-ffi.h index b8cae20..d8e981e 100644 --- a/pkg/libsignalgo/libsignal-ffi.h +++ b/pkg/libsignalgo/libsignal-ffi.h @@ -520,6 +520,22 @@ static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfc_uchar17 typedef const SignalOwnedBuffer* SignalType_ConstPointer_SignalOwnedBuffer; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBuffer) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBuffer) == 8); +typedef SignalOwnedBuffer* SignalType_MutPointer_SignalOwnedBuffer; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalOwnedBuffer) == 8); +typedef struct { + SignalOwnedBuffer* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +typedef const SignalOwnedBufferOfMaxAlignedOwnedBuffer* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef enum { SignalAuthCheckResultFfiResultMatch, SignalAuthCheckResultFfiResultNoMatch, @@ -607,6 +623,33 @@ static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFf typedef const SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); +typedef enum { + SignalDeviceCapabilityInternalFfiResultStorage, + SignalDeviceCapabilityInternalFfiResultTransfer, + SignalDeviceCapabilityInternalFfiResultAttachmentBackfill, + SignalDeviceCapabilityInternalFfiResultSparsePostQuantumRatchet, + SignalDeviceCapabilityInternalFfiResultProfilesV2, + SignalDeviceCapabilityInternalFfiResultUsernameChangeSyncMessage, + SignalDeviceCapabilityInternalFfiResultOptionalPhoneNumber, +} SignalDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalDeviceCapabilityInternalFfiResult) == 4); +static_assert_64bit(alignof(SignalDeviceCapabilityInternalFfiResult) == 4); +typedef SignalDeviceCapabilityInternalFfiResult* SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalDeviceCapabilityInternalFfiResult) == 8); +typedef struct { + SignalDeviceCapabilityInternalFfiResult* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +typedef const SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); typedef struct { uint8_t id; SignalOwnedBuffer encrypted_name; @@ -869,19 +912,6 @@ static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedi32, length) == 8); static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedi32, size_bytes) == 16); static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedi32) == 24); static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedi32) == 8); -typedef SignalOwnedBuffer* SignalType_MutPointer_SignalOwnedBuffer; -static_assert_64bit(sizeof(SignalType_MutPointer_SignalOwnedBuffer) == 8); -static_assert_64bit(alignof(SignalType_MutPointer_SignalOwnedBuffer) == 8); -typedef struct { - SignalOwnedBuffer* base; - size_t length; - size_t size_bytes; -} SignalOwnedBufferOfMaxAlignedOwnedBuffer; -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, base) == 0); -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, length) == 8); -static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedOwnedBuffer, size_bytes) == 16); -static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 24); -static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef struct { SignalOwnedBuffer user_handle; SignalOwnedBufferOfMaxAlignedi32 allowed_algorithms; @@ -1072,6 +1102,59 @@ static_assert_64bit(alignof(SignalType_ConstPointer_SignalServerMessageAck) == 8 typedef const SignalUnauthenticatedChatConnection* SignalType_ConstPointer_SignalUnauthenticatedChatConnection; static_assert_64bit(sizeof(SignalType_ConstPointer_SignalUnauthenticatedChatConnection) == 8); static_assert_64bit(alignof(SignalType_ConstPointer_SignalUnauthenticatedChatConnection) == 8); +typedef struct { + int64_t level; + uint64_t expiration; +} SignalBackupEntitlementFfiResult; +static_assert_64bit(offsetof(SignalBackupEntitlementFfiResult, level) == 0); +static_assert_64bit(offsetof(SignalBackupEntitlementFfiResult, expiration) == 8); +static_assert_64bit(sizeof(SignalBackupEntitlementFfiResult) == 16); +static_assert_64bit(alignof(SignalBackupEntitlementFfiResult) == 8); +typedef SignalBackupEntitlementFfiResult MaybeUninitOfBackupEntitlementFfiResult; +static_assert_64bit(sizeof(MaybeUninitOfBackupEntitlementFfiResult) == 16); +static_assert_64bit(alignof(MaybeUninitOfBackupEntitlementFfiResult) == 8); +typedef struct { + bool present; + MaybeUninitOfBackupEntitlementFfiResult value; +} SignalOptionalOfBackupEntitlementFfiResult; +static_assert_64bit(offsetof(SignalOptionalOfBackupEntitlementFfiResult, present) == 0); +static_assert_64bit(offsetof(SignalOptionalOfBackupEntitlementFfiResult, value) == 8); +static_assert_64bit(sizeof(SignalOptionalOfBackupEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalOptionalOfBackupEntitlementFfiResult) == 8); +typedef struct { + const int8_t* badge_id; + uint64_t expiration; + bool visible; +} SignalBadgeEntitlementFfiResult; +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, badge_id) == 0); +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, expiration) == 8); +static_assert_64bit(offsetof(SignalBadgeEntitlementFfiResult, visible) == 16); +static_assert_64bit(sizeof(SignalBadgeEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalBadgeEntitlementFfiResult) == 8); +typedef SignalBadgeEntitlementFfiResult* SignalType_MutPointer_SignalBadgeEntitlementFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalBadgeEntitlementFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalBadgeEntitlementFfiResult) == 8); +typedef struct { + SignalBadgeEntitlementFfiResult* base; + size_t length; + size_t size_bytes; +} SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult; +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, base) == 0); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, length) == 8); +static_assert_64bit(offsetof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult, size_bytes) == 16); +static_assert_64bit(sizeof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult) == 24); +static_assert_64bit(alignof(SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult) == 8); +typedef struct { + SignalOwnedBufferOfMaxAlignedBadgeEntitlementFfiResult badges; + SignalOptionalOfBackupEntitlementFfiResult backup; +} SignalAccountEntitlementsFfiResult; +static_assert_64bit(offsetof(SignalAccountEntitlementsFfiResult, badges) == 0); +static_assert_64bit(offsetof(SignalAccountEntitlementsFfiResult, backup) == 24); +static_assert_64bit(sizeof(SignalAccountEntitlementsFfiResult) == 48); +static_assert_64bit(alignof(SignalAccountEntitlementsFfiResult) == 8); +typedef const SignalAccountEntitlementsFfiResult* SignalType_ConstPointer_SignalAccountEntitlementsFfiResult; +static_assert_64bit(sizeof(SignalType_ConstPointer_SignalAccountEntitlementsFfiResult) == 8); +static_assert_64bit(alignof(SignalType_ConstPointer_SignalAccountEntitlementsFfiResult) == 8); typedef SignalPairOfCStringPtrCStringPtr* SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr; static_assert_64bit(sizeof(SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalPairOfCStringPtrCStringPtr) == 8); @@ -2667,6 +2750,22 @@ static_assert_64bit(alignof(SignalCPromiseOwnedBuffer) == 8); typedef SignalCPromiseOwnedBuffer* SignalType_MutPointer_SignalCPromiseOwnedBuffer; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBuffer) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBuffer) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedOwnedBuffer_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 24); +static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +typedef SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedPairOfCStringPtrAuthCheckResultFfiResult_ConstPointer_void) == 8); @@ -2699,6 +2798,22 @@ static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmed typedef SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedBridgeConfirmedMfaKeyFfiResult) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 24); +static_assert_64bit(alignof(SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +typedef SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalOwnedBufferOfMaxAlignedLinkedDeviceInternalFfiResult_ConstPointer_void) == 8); @@ -2891,6 +3006,22 @@ static_assert_64bit(alignof(SignalCPromiseDeleteBackupMediaNextChunkFfiResult) = typedef SignalCPromiseDeleteBackupMediaNextChunkFfiResult* SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult; static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult) == 8); static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseDeleteBackupMediaNextChunkFfiResult) == 8); +typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalAccountEntitlementsFfiResult, SignalType_ConstPointer_void); +static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void) == 8); +static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void) == 8); +typedef struct { + SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalAccountEntitlementsFfiResult_ConstPointer_void complete; + const void* context; + uint64_t cancellation_id; +} SignalCPromiseAccountEntitlementsFfiResult; +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, complete) == 0); +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, context) == 8); +static_assert_64bit(offsetof(SignalCPromiseAccountEntitlementsFfiResult, cancellation_id) == 16); +static_assert_64bit(sizeof(SignalCPromiseAccountEntitlementsFfiResult) == 24); +static_assert_64bit(alignof(SignalCPromiseAccountEntitlementsFfiResult) == 8); +typedef SignalCPromiseAccountEntitlementsFfiResult* SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult; +static_assert_64bit(sizeof(SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult) == 8); +static_assert_64bit(alignof(SignalType_MutPointer_SignalCPromiseAccountEntitlementsFfiResult) == 8); typedef void (*SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void)(SignalType_MutPointer_SignalFfiError, SignalType_ConstPointer_SignalCurrencyConversionsInternalFfiResult, SignalType_ConstPointer_void); static_assert_64bit(sizeof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void) == 8); static_assert_64bit(alignof(SignalType_FunctionPointer_void_MutPointer_SignalFfiError_ConstPointer_SignalCurrencyConversionsInternalFfiResult_ConstPointer_void) == 8); @@ -3610,6 +3741,7 @@ typedef enum { SignalErrorCodeMfaKeyNotFound = 237, SignalErrorCodeWebAuthnRegistrationUnsuccessful = 238, SignalErrorCodeProfileNotFound = 239, + SignalErrorCodeReceiptAlreadyRedeemed = 240, } SignalErrorCode; static_assert_64bit(sizeof(SignalErrorCode) == 4); static_assert_64bit(alignof(SignalErrorCode) == 4); @@ -3633,6 +3765,13 @@ enum SignalSvr2CredentialsResult { typedef uint8_t SignalSvr2CredentialsResult; static_assert_64bit(sizeof(SignalSvr2CredentialsResult) == 1); static_assert_64bit(alignof(SignalSvr2CredentialsResult) == 1); +typedef enum { + SignalBadgeVisibilityFfiArgDoNotSet, + SignalBadgeVisibilityFfiArgSetVisible, + SignalBadgeVisibilityFfiArgSetPrimary, +} SignalBadgeVisibilityFfiArg; +static_assert_64bit(sizeof(SignalBadgeVisibilityFfiArg) == 4); +static_assert_64bit(alignof(SignalBadgeVisibilityFfiArg) == 4); typedef enum { SignalBridgeMfaVerificationCredentialFfiArgTotp, SignalBridgeMfaVerificationCredentialFfiArgWebAuthn, @@ -3915,6 +4054,13 @@ SignalFfiError* signal_authenticated_chat_connection_connect( bool receive_stories, SignalBorrowedBytestringArray languages ); +SignalFfiError* signal_authenticated_chat_connection_create_donation_permits( + SignalCPromiseOwnedBufferOfMaxAlignedOwnedBuffer* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat, + SignalBorrowedBuffer request_context, + SignalConstPointerServerPublicParams server_params +); SignalFfiError* signal_authenticated_chat_connection_delete_account( SignalCPromisebool* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -3960,6 +4106,11 @@ SignalFfiError* signal_authenticated_chat_connection_generate_totp_key( SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat ); +SignalFfiError* signal_authenticated_chat_connection_get_capabilities( + SignalCPromiseOwnedBufferOfMaxAlignedDeviceCapabilityInternalFfiResult* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat +); SignalFfiError* signal_authenticated_chat_connection_get_currency_conversions( SignalCPromiseCurrencyConversionsInternalFfiResult* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -3970,6 +4121,11 @@ SignalFfiError* signal_authenticated_chat_connection_get_devices( SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat ); +SignalFfiError* signal_authenticated_chat_connection_get_entitlements( + SignalCPromiseAccountEntitlementsFfiResult* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat +); SignalFfiError* signal_authenticated_chat_connection_get_pre_key_count( SignalCPromiseBridgePreKeyCountsFfiResult* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -4012,6 +4168,13 @@ SignalFfiError* signal_authenticated_chat_connection_redeem_backup_receipt( SignalConstPointerAuthenticatedChatConnection chat, const SignalType_FixedArray329_uint8_t* presentation ); +SignalFfiError* signal_authenticated_chat_connection_redeem_donation_receipt( + SignalCPromisebool* promise, + SignalConstPointerTokioAsyncContext async_runtime, + SignalConstPointerAuthenticatedChatConnection chat, + const SignalType_FixedArray329_uint8_t* presentation, + SignalBadgeVisibilityFfiArg visibility +); SignalFfiError* signal_authenticated_chat_connection_remove_device( SignalCPromisebool* promise, SignalConstPointerTokioAsyncContext async_runtime, @@ -4859,6 +5022,7 @@ SignalFfiError* signal_expiring_profile_key_credential_get_expiration_time( SignalFfiError* signal_expiring_profile_key_credential_response_check_valid_contents( SignalBorrowedBuffer buffer ); +void signal_ffi_version_0_106_1(void); SignalFfiError* signal_fingerprint_clone( SignalMutPointerFingerprint* new_obj, SignalConstPointerFingerprint obj diff --git a/pkg/libsignalgo/signalversion/version.go b/pkg/libsignalgo/signalversion/version.go index b935b64..367a3d5 100644 --- a/pkg/libsignalgo/signalversion/version.go +++ b/pkg/libsignalgo/signalversion/version.go @@ -2,4 +2,4 @@ package signalversion -const Version = "v0.105.0" +const Version = "v0.106.1" diff --git a/pkg/libsignalgo/update-ffi-docker.sh b/pkg/libsignalgo/update-ffi-docker.sh index 73f8947..e15e7f6 100755 --- a/pkg/libsignalgo/update-ffi-docker.sh +++ b/pkg/libsignalgo/update-ffi-docker.sh @@ -1,9 +1,3 @@ #!/bin/bash docker run --rm -itv $(pwd):/data rust:1-alpine /data/update-ffi-docker-inner.sh - -echo "// Generated by update-ffi.sh; DO NOT EDIT." > signalversion/version.go -echo >> signalversion/version.go -echo "package signalversion" >> signalversion/version.go -echo >> signalversion/version.go -cd libsignal -echo "const Version = \"$(git describe --tags --always)\"" >> ../signalversion/version.go +./update-ffi-version.sh diff --git a/pkg/libsignalgo/update-ffi-version.sh b/pkg/libsignalgo/update-ffi-version.sh new file mode 100755 index 0000000..23ae12c --- /dev/null +++ b/pkg/libsignalgo/update-ffi-version.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +pushd libsignal + +LIBSIGNAL_TAG=$(git describe --tags --always) +LIBSIGNAL_VERSION_FUNC=$(echo $LIBSIGNAL_TAG | tr -d 'v' | sed 's/\./_/g') + +popd + +cat > signalversion/version.go < version.go < signalversion/version.go -echo >> signalversion/version.go -echo "package signalversion" >> signalversion/version.go -echo >> signalversion/version.go - # Store the current working directory ORIGINAL_DIR="$(pwd)" # Navigate to libsignal directory cd "$LIBSIGNAL_DIRECTORY" -echo "const Version = \"$(git describe --tags --always)\"" >> ../signalversion/version.go - # Build libsignal cargo build -p libsignal-ffi --release @@ -35,4 +28,6 @@ cd "$ORIGINAL_DIR" cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" . cp "${LIBSIGNAL_DIRECTORY}/swift/Sources/SignalFfi/signal_ffi.h" libsignal-ffi.h +./update-ffi-version.sh + echo "Files copied successfully." diff --git a/pkg/libsignalgo/version.go b/pkg/libsignalgo/version.go new file mode 100644 index 0000000..b5732cb --- /dev/null +++ b/pkg/libsignalgo/version.go @@ -0,0 +1,13 @@ +// Generated by update-ffi.sh; DO NOT EDIT. + +package libsignalgo + +/* +#include "./libsignal-ffi.h" +*/ +import "C" + +func init() { + // Compile time check to ensure the libsignal version matches + C.signal_ffi_version_0_106_1() +} From 020738bc24e414343eac0851c2cd5ec97f2ecb38 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 31 Oct 2026 20:11:58 +0200 Subject: [PATCH 24/46] changelog: update --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ba2ca..4e00a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v26.11 (unreleased) + +* Updated libsignal to v0.106.1. +* Added support for sending more than 2048 characters of text to Signal by + uploading the rest of the text as an attachment, like official clients do. +* Added automatic retrying of group message sends if the server returns a 500 + or 503 error. +* Added support for encrypting direct media with FLOE ([MSC4571]). +* Fixed memory leak in libsignal store implementation. + +[MSC4571]: https://github.com/matrix-org/matrix-spec-proposals/pull/4571 + # v26.10 * Updated libsignal to v0.105.0. From b0346e66988e34efaa125bc64abb960a82e3ef10 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Tue, 3 Nov 2026 21:36:55 +0000 Subject: [PATCH 25/46] pre-commit: add bridgelint hook (#701) --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9cc6a3..8ea1c20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,9 @@ 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 + verbose: true From e2e994f99f8dfffe5ad1cc88a2d36449639a609e Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 4 Nov 2026 22:28:39 +0000 Subject: [PATCH 26/46] signalmeow/attachments: send uploadLength when requesting upload form (#703) --- pkg/signalmeow/attachments.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/signalmeow/attachments.go b/pkg/signalmeow/attachments.go index 9bfc1fd..c4317ad 100644 --- a/pkg/signalmeow/attachments.go +++ b/pkg/signalmeow/attachments.go @@ -277,7 +277,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb } // Get upload attributes from Signal server - attributesPath := "/v4/attachments/form/upload" + attributesPath := fmt.Sprintf("/v4/attachments/form/upload?uploadLength=%d", len(encryptedWithMAC)) resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil) if err != nil { log.Err(err).Msg("Failed to request upload attributes") From 7501c40cba38fafeec636ccae6eb4eb870ea27e3 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:17:03 -0500 Subject: [PATCH 27/46] handlesignal: resync group info once for portals synced before reaction overrides (#702) Co-authored-by: Indent --- pkg/connector/groupinfo.go | 3 +++ pkg/connector/handlesignal.go | 5 ++++- pkg/signalid/dbmeta.go | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go index 345ba28..cd50964 100644 --- a/pkg/connector/groupinfo.go +++ b/pkg/connector/groupinfo.go @@ -184,9 +184,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 } diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go index 3704800..ed2d33e 100644 --- a/pkg/connector/handlesignal.go +++ b/pkg/connector/handlesignal.go @@ -233,13 +233,16 @@ func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) if !ok || dataMsg.GroupV2 == nil { return } - portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision + portalMeta := portal.Metadata.(*signalid.PortalMetadata) + portalRev := portalMeta.Revision if evt.Info.GroupRevision > portalRev { toRevision := evt.Info.GroupRevision if dataMsg.GetGroupV2().GetGroupChange() != nil { toRevision-- } evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp()) + } else if portalMeta.InfoVersion < currentPortalInfoVersion && portal.MXID != "" { + evt.s.catchUpGroup(ctx, portal, 0, portalRev, dataMsg.GetTimestamp()) } } diff --git a/pkg/signalid/dbmeta.go b/pkg/signalid/dbmeta.go index 916aec5..0c35c0d 100644 --- a/pkg/signalid/dbmeta.go +++ b/pkg/signalid/dbmeta.go @@ -24,7 +24,8 @@ type PortalMetadata struct { Revision uint32 `json:"revision,omitempty"` ExpirationTimerVersion uint32 `json:"expiration_timer_version,omitempty"` // Lazy resync tracking - LastSync jsontime.Unix `json:"last_sync,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` + InfoVersion uint32 `json:"info_version,omitempty"` } type MessageMetadata struct { From a630835d70e59d28b29ee6316da589a27a155ba5 Mon Sep 17 00:00:00 2001 From: Nick Mills-Barrett Date: Thu, 5 Nov 2026 13:09:30 +0000 Subject: [PATCH 28/46] pre-commit: install bridgelint with the CI Go toolchain (#706) --- .github/workflows/go.yml | 1 + .pre-commit-config.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8b988db..df15747 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,6 +30,7 @@ jobs: run: | go install golang.org/x/tools/cmd/goimports@latest go install honnef.co/go/tools/cmd/staticcheck@latest + go install github.com/beeper/pre-commit-go/bridgelint@latest export PATH="$HOME/go/bin:$PATH" - name: Run pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ea1c20..1f1fe9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,4 +27,5 @@ repos: - id: zerolog-ban-msgf - id: zerolog-use-stringer - id: bridgelint + language: system verbose: true From 34b4187e0796ee399245434e7a2657d97d25cd40 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:33:19 +0000 Subject: [PATCH 29/46] chatinfo: stop listing contacts after request cancellation (#705) Co-authored-by: Indent --- pkg/connector/chatinfo.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 4668cd7..dbc524d 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -445,11 +445,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 { @@ -458,7 +462,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 { From c820321548483db3266dd130901123a87c124176 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:34:19 +0000 Subject: [PATCH 30/46] signalmeow/websocket: log alerts sent by the server when connecting (#709) Co-authored-by: Indent --- pkg/signalmeow/web/signalwebsocket.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index 7ee54c7..bade56b 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -319,6 +319,11 @@ func (s *SignalWebsocket) connectLoop( } // Succssfully connected + if resp != nil { + if alerts := resp.Header.Values("X-Signal-Alert"); len(alerts) > 0 { + log.Warn().Strs("alerts", alerts).Msg("Signal server sent alerts when opening websocket") + } + } s.pushStatus(ctx, SignalWebsocketConnectionEventConnected, nil) s.ws.Store(ws) retrying = false From c827c97adb25656dfa6a1bcef609607c6fa6f0b4 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:41:17 +0000 Subject: [PATCH 31/46] chatinfo: fall back to profile avatar when contact avatar isn't loaded (#710) Co-authored-by: Indent --- pkg/connector/chatinfo.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index dbc524d..669c015 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -125,9 +125,10 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec } name := s.Main.Config.FormatDisplayname(contact, username) ui.Name = &name - if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" { + contactAvatarID := networkid.AvatarID("hash:" + contact.ContactAvatar.Hash) + if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" && s.canUseContactAvatar(ctx, contact, contactAvatarID) { ui.Avatar = &bridgev2.Avatar{ - ID: networkid.AvatarID("hash:" + contact.ContactAvatar.Hash), + ID: contactAvatarID, Get: func(ctx context.Context) ([]byte, error) { if contact.ContactAvatar.Image == nil { return nil, fmt.Errorf("contact avatar not available") @@ -172,6 +173,20 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec return ui, nil } +func (s *SignalClient) canUseContactAvatar(ctx context.Context, contact *types.Recipient, avatarID networkid.AvatarID) bool { + if contact.ContactAvatar.Image != nil { + return true + } else if contact.ACI == uuid.Nil { + return false + } + ghost, err := s.Main.Bridge.GetExistingGhostByID(ctx, signalid.MakeUserID(contact.ACI)) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost to check contact avatar") + return true + } + return ghost != nil && ghost.AvatarID == avatarID && ghost.AvatarMXC != "" && ghost.AvatarSet +} + func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool { _, err := signalid.ParseUserIDAsServiceID(id) return err == nil From d451962ba2e88f5888a098d0a29d1d282bfddeb0 Mon Sep 17 00:00:00 2001 From: "indent[bot]" <297468268+indent[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:42:33 +0000 Subject: [PATCH 32/46] msgconv: download stickers from their pack when the attachment is gone (#708) Co-authored-by: Indent --- pkg/msgconv/from-signal.go | 50 +++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index 251e9dd..0bf2d3b 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -502,6 +502,11 @@ func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker, attMap AttachmentMap) *bridgev2.ConvertedMessagePart { converted, err := mc.reuploadAttachment(ctx, sticker.GetData(), attMap) + hasPack := len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) + if err != nil && hasPack && (errors.Is(err, signalmeow.ErrAttachmentNotFound) || errors.Is(err, ErrAttachmentNotInBackup) || errors.Is(err, ErrBackupNotSupported)) { + zerolog.Ctx(ctx).Debug().Err(err).Msg("Sticker attachment is unavailable, downloading it from the sticker pack") + converted, err = mc.reuploadStickerFromPack(ctx, sticker) + } if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker") return &bridgev2.ConvertedMessagePart{ @@ -519,7 +524,7 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker converted.Content.Info.Height = 200 } converted.Content.Body = sticker.GetEmoji() - if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) { + if hasPack { converted.Content.Info.BridgedSticker = &event.BridgedSticker{ Network: StickerSourceID, ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10), @@ -532,6 +537,49 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker return converted } +func (mc *MessageConverter) reuploadStickerFromPack(ctx context.Context, sticker *signalpb.DataMessage_Sticker) (*bridgev2.ConvertedMessagePart, error) { + att := sticker.GetData() + content := &event.MessageEventContent{ + Body: att.GetFileName(), + Info: &event.FileInfo{ + MimeType: att.GetContentType(), + Width: int(att.GetWidth()), + Height: int(att.GetHeight()), + }, + } + if mc.DirectMedia { + mediaID, err := signalid.DirectMediaSticker{ + PackID: sticker.GetPackId(), + PackKey: sticker.GetPackKey(), + StickerID: sticker.GetStickerId(), + }.AsMediaID() + if err != nil { + return nil, err + } + content.URL, content.File, err = mc.Bridge.GenerateContentURI(ctx, getPortal(ctx).MXID, mediaID) + if err != nil { + return nil, err + } + } else { + data, err := signalmeow.DownloadStickerPackItem(ctx, sticker.GetPackId(), sticker.GetPackKey(), sticker.GetStickerId()) + if err != nil { + return nil, fmt.Errorf("failed to download sticker from pack: %w", err) + } + if content.Info.MimeType == "" { + content.Info.MimeType = http.DetectContentType(data) + } + content.Info.Size = len(data) + content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, content.Body, content.Info.MimeType) + if err != nil { + return nil, fmt.Errorf("failed to upload sticker from pack: %w", err) + } + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: content, + }, nil +} + func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) { data, err := mc.downloadAttachment(ctx, att, attMap, nil) if err != nil { From 8cb696dcfcf248a34e1fe6af5510586753485dc6 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:04:39 +0000 Subject: [PATCH 33/46] signalmeow/groups: decode announcements-only flag in full group state (#727) --- pkg/signalmeow/groups.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 0849f74..411427a 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -497,6 +497,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast // These aren't encrypted decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.Revision = encryptedGroup.Version + decryptedGroup.AnnouncementsOnly = encryptedGroup.AnnouncementsOnly // Decrypt members for _, member := range encryptedGroup.Members { From 74b4c70ee4182191333bea0d0432dfa18d79467f Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:16:58 +0000 Subject: [PATCH 34/46] handlesignal: send a notice in DMs when a safety number changes (#723) --- pkg/connector/config.go | 2 ++ pkg/connector/example-config.yaml | 2 ++ pkg/connector/handlesignal.go | 40 ++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 6e9f2e0..9a0d65a 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -42,6 +42,7 @@ type SignalConfig struct { NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` LocationFormat string `yaml:"location_format"` ExtEvPolls bool `yaml:"extev_polls"` + IdentityChangeNotices bool `yaml:"identity_change_notices"` displaynameTemplate *template.Template `yaml:"-"` } @@ -106,6 +107,7 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Str, "note_to_self_avatar") helper.Copy(up.Str, "location_format") helper.Copy(up.Bool, "extev_polls") + helper.Copy(up.Bool, "identity_change_notices") } func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index f67fbc8..0a818d1 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -25,3 +25,5 @@ note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s' # Should polls be sent using unstable MSC3381 event types? extev_polls: false +# Should another user's safety number changing send a message to Matrix? +identity_change_notices: false diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go index ed2d33e..389f02c 100644 --- a/pkg/connector/handlesignal.go +++ b/pkg/connector/handlesignal.go @@ -75,7 +75,7 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool { case *events.LoggedOut: 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") } @@ -161,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 From 0a4f1d81c8cf670d550b8dead774c70dde7450d3 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:17:31 +0000 Subject: [PATCH 35/46] signalmeow/web: don't return an error when closing an already closed websocket (#721) --- pkg/signalmeow/web/signalwebsocket.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/signalmeow/web/signalwebsocket.go b/pkg/signalmeow/web/signalwebsocket.go index bade56b..2ee9ac8 100644 --- a/pkg/signalmeow/web/signalwebsocket.go +++ b/pkg/signalmeow/web/signalwebsocket.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/http" "net/url" "strings" @@ -116,6 +117,9 @@ func (s *SignalWebsocket) Close() (err error) { s.closeCalled.Store(true) if ws := s.ws.Swap(nil); ws != nil { err = ws.Close(websocket.StatusNormalClosure, "") + if errors.Is(err, net.ErrClosed) { + err = nil + } } if cancelLoop := s.cancel.Swap(nil); cancelLoop != nil { (*cancelLoop)() From c6abc27db6490c0c330953d961f400501fba0b41 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:17:39 +0000 Subject: [PATCH 36/46] signalmeow/storageservice: only fetch new records after the first sync (#719) --- pkg/signalmeow/client.go | 2 ++ pkg/signalmeow/storageservice.go | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/signalmeow/client.go b/pkg/signalmeow/client.go index c8898e3..fad3f8e 100644 --- a/pkg/signalmeow/client.go +++ b/pkg/signalmeow/client.go @@ -72,6 +72,8 @@ type Client struct { storageSyncLock sync.Mutex storageSyncQueued atomic.Bool + storageVersion uint64 + storageRecords []string writeCallbackCounter chan time.Time } diff --git a/pkg/signalmeow/storageservice.go b/pkg/signalmeow/storageservice.go index cfc2951..ea45627 100644 --- a/pkg/signalmeow/storageservice.go +++ b/pkg/signalmeow/storageservice.go @@ -55,18 +55,30 @@ func (cli *Client) StorageSync(ctx context.Context) { cli.storageSyncQueued.Store(false) log := cli.Log.With().Str("action", "sync storage").Logger() ctx = log.WithContext(ctx) - // TODO only fetch changed entries - update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, 0, nil) + update, err := cli.FetchStorage(ctx, cli.Store.MasterKey, cli.storageVersion, cli.storageRecords) if err != nil { log.Err(err).Msg("Failed to fetch storage") return + } else if update == nil { + log.Debug().Uint64("version", cli.storageVersion).Msg("Storage manifest is already up to date") + return } err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error { return cli.processStorageInTxn(ctx, update) }) if err != nil { log.Err(err).Msg("Failed to process storage update") + return } + log.Debug(). + Uint64("previous_version", cli.storageVersion). + Uint64("version", update.Version). + Int("new_records", len(update.NewRecords)). + Int("removed_records", len(update.RemovedRecords)). + Int("missing_records", len(update.MissingRecords)). + Msg("Processed storage update") + cli.storageVersion = update.Version + cli.storageRecords = update.KnownRecords } func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdate) error { @@ -178,6 +190,7 @@ type StorageUpdate struct { NewRecords []*DecryptedStorageRecord RemovedRecords []string MissingRecords []string + KnownRecords []string } func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVersion uint64, existingKeys []string) (*StorageUpdate, error) { @@ -189,6 +202,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe return nil, nil } removedKeys := make([]string, 0) + knownKeys := make([]string, 0, len(manifest.GetIdentifiers())) newKeys := manifestRecordToMap(manifest.GetIdentifiers()) slices.Sort(existingKeys) existingKeys = slices.Compact(existingKeys) @@ -196,6 +210,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe _, isStillThere := newKeys[key] if isStillThere { delete(newKeys, key) + knownKeys = append(knownKeys, key) } else { removedKeys = append(removedKeys, key) } @@ -205,11 +220,15 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe if err != nil { return nil, err } + for _, record := range newRecords { + knownKeys = append(knownKeys, record.StorageID) + } return &StorageUpdate{ Version: manifest.GetVersion(), NewRecords: newRecords, RemovedRecords: removedKeys, MissingRecords: missingKeys, + KnownRecords: knownKeys, }, nil } From 5c740406dbd067dab3ce2b95cea941a96b2b084b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:17:50 +0000 Subject: [PATCH 37/46] signalmeow: handle server delivery receipts as delivery receipts (#718) --- pkg/signalmeow/receiving.go | 13 +++++++++++++ pkg/signalmeow/receiving_decrypt.go | 3 --- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index 1f55b26..e99047d 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -378,6 +378,19 @@ func (cli *Client) incomingAPIMessageHandler(ctx context.Context, req *signalpb. Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]). Msg("Received envelope") + if envelope.GetType() == signalpb.Envelope_SERVER_DELIVERY_RECEIPT { + if sourceServiceID.Type == libsignalgo.ServiceIDTypeACI && sourceServiceID.UUID != uuid.Nil && !cli.handleEvent(&events.Receipt{ + Sender: sourceServiceID.UUID, + Content: &signalpb.ReceiptMessage{ + Type: signalpb.ReceiptMessage_DELIVERY.Enum(), + Timestamp: []uint64{envelope.GetClientTimestamp()}, + }, + }) { + return nil, ErrHandlerFailed + } + return &web.SimpleResponse{Status: 200}, nil + } + result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID) err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID) diff --git a/pkg/signalmeow/receiving_decrypt.go b/pkg/signalmeow/receiving_decrypt.go index e6abff4..8ad3e15 100644 --- a/pkg/signalmeow/receiving_decrypt.go +++ b/pkg/signalmeow/receiving_decrypt.go @@ -107,9 +107,6 @@ func (cli *Client) decryptEnvelope( Unencrypted: true, } - case signalpb.Envelope_SERVER_DELIVERY_RECEIPT: - return DecryptionResult{Err: fmt.Errorf("server delivery receipt envelopes are not yet supported")} - case signalpb.Envelope_UNKNOWN: return DecryptionResult{Err: fmt.Errorf("unknown envelope type")} From cd8bc557d6fe17a2afec00efa48f5a856a9ccd5c Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:17:56 +0000 Subject: [PATCH 38/46] signalmeow/storageservice: ignore record types the bridge doesn't use (#717) --- pkg/signalmeow/storageservice.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/signalmeow/storageservice.go b/pkg/signalmeow/storageservice.go index ea45627..1eb3069 100644 --- a/pkg/signalmeow/storageservice.go +++ b/pkg/signalmeow/storageservice.go @@ -170,7 +170,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat return fmt.Errorf("failed to save device after receiving account record: %w", err) } log.Debug().Msg("Saved device after receiving account record") - case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList: + case *signalpb.StorageRecord_GroupV1, *signalpb.StorageRecord_StoryDistributionList, *signalpb.StorageRecord_StickerPack, + *signalpb.StorageRecord_CallLink, *signalpb.StorageRecord_ChatFolder, *signalpb.StorageRecord_NotificationProfile, + *signalpb.StorageRecord_FavoriteSticker: // irrelevant data default: log.Warn().Type("type", data).Str("item_id", record.StorageID).Msg("Unknown storage record type") From d59791e7d3a070ea4d44e44008fcaa06a5244020 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:18:01 +0000 Subject: [PATCH 39/46] signalmeow: send typing messages as online-only (#716) --- pkg/signalmeow/senderkey.go | 4 ++-- pkg/signalmeow/sending.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/senderkey.go b/pkg/signalmeow/senderkey.go index 2eefadd..322fcc8 100644 --- a/pkg/signalmeow/senderkey.go +++ b/pkg/signalmeow/senderkey.go @@ -221,8 +221,8 @@ func (cli *Client) sendToGroupWithSenderKey( header.Set("Unidentified-Access-Key", xak.String()) } path := fmt.Sprintf( - "/v1/messages/multi_recipient?ts=%d&urgent=%t&online=false", - messageTimestamp, isUrgent(content), + "/v1/messages/multi_recipient?ts=%d&urgent=%t&online=%t", + messageTimestamp, isUrgent(content), content.GetTypingMessage() != nil, ) log.Debug(). Any("recipients", ski.SharedWith). diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index 115b7e5..dce4eea 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -964,7 +964,7 @@ func (cli *Client) sendContent( outgoingMessages := MyMessages{ Timestamp: messageTimestamp, - Online: false, + Online: content.GetTypingMessage() != nil, Urgent: isUrgent(content), Messages: messages, } From fa622888a97e4f8f865300dbf76407ea8397c1d6 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:18:04 +0000 Subject: [PATCH 40/46] client: don't connect logins that are already logged out (#713) --- pkg/connector/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/connector/client.go b/pkg/connector/client.go index e6c5b5e..b5531b5 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -221,12 +221,18 @@ func (s *SignalClient) Connect(ctx context.Context) { if s.Client == nil { 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) s.tryConnect(ctx, 0, true) } 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 { From 15674cc755ef6cc446c4418e5fb1c1bd56e96aa9 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:18:13 +0000 Subject: [PATCH 41/46] signalmeow/groupcache: keep send endorsements for group changes without a response (#714) --- pkg/signalmeow/groupcache.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/signalmeow/groupcache.go b/pkg/signalmeow/groupcache.go index ee8259f..7f5d282 100644 --- a/pkg/signalmeow/groupcache.go +++ b/pkg/signalmeow/groupcache.go @@ -203,6 +203,7 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi delete(gc.data, id) return nil } + prevMembers := cached.getMemberServiceIDs() // Pending member adds, promotes and removes cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...) @@ -319,6 +320,12 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi cached.UpdatedAt = time.Now() cached.Revision = change.Revision + if len(endorsementResponse) == 0 { + if !slices.Equal(prevMembers, cached.getMemberServiceIDs()) { + delete(gc.data, id) + } + return nil + } endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs( cached.getMemberServiceIDs(), gc.serviceID, From 27e3fc25829ed091660656a4197a628a0f3544c5 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:18:23 +0000 Subject: [PATCH 42/46] signalmeow/storageservice: fix unnecessarily marking recipients as changed (#712) --- pkg/signalmeow/storageservice.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/storageservice.go b/pkg/signalmeow/storageservice.go index 1eb3069..f3b9aac 100644 --- a/pkg/signalmeow/storageservice.go +++ b/pkg/signalmeow/storageservice.go @@ -113,8 +113,9 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat recipient.Profile.Name = strings.TrimSpace(fmt.Sprintf("%s %s", contact.GivenName, contact.FamilyName)) } if contact.SystemGivenName != "" || contact.SystemFamilyName != "" { - changed = true - recipient.ContactName = strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName)) + newContactName := strings.TrimSpace(fmt.Sprintf("%s %s", contact.SystemGivenName, contact.SystemFamilyName)) + changed = changed || recipient.ContactName != newContactName + recipient.ContactName = newContactName } newNickname := "" if contact.Nickname != nil { @@ -132,7 +133,7 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat changed = true recipient.Blocked = contact.Blocked } - if !ptr.Val(recipient.Whitelisted) { + if !ptr.Val(recipient.Whitelisted) && (recipient.Whitelisted == nil || contact.Whitelisted) { changed = true recipient.Whitelisted = &contact.Whitelisted } From 59a4ff2afc39f5467adff794ec1ac82c491fd178 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Tue, 10 Nov 2026 23:18:34 +0000 Subject: [PATCH 43/46] msgconv/from-signal: send notice when a message is pinned/unpinned (#724) --- pkg/connector/handlesignal.go | 1 + pkg/msgconv/from-signal.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go index 389f02c..b6e746f 100644 --- a/pkg/connector/handlesignal.go +++ b/pkg/connector/handlesignal.go @@ -223,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 diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go index 0bf2d3b..2140db9 100644 --- a/pkg/msgconv/from-signal.go +++ b/pkg/msgconv/from-signal.go @@ -153,6 +153,12 @@ func (mc *MessageConverter) ToMatrix( cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate)) return cm } + if dm.PinMessage != nil || dm.UnpinMessage != nil { + var part *bridgev2.ConvertedMessagePart + part, cm.ReplyTo = mc.convertPinToMatrix(dm) + cm.Parts = append(cm.Parts, part) + return cm + } if dm.GetIsViewOnce() { if !mc.Bridge.Config.ViewLimitedMedia && (cm.Disappear.Timer == 0 || cm.Disappear.Timer > 5*time.Minute) { cm.Disappear = database.DisappearingSetting{ @@ -841,6 +847,28 @@ func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, se } } +func (mc *MessageConverter) convertPinToMatrix(dm *signalpb.DataMessage) (*bridgev2.ConvertedMessagePart, *networkid.MessageOptionalPartID) { + body := "Pinned a message" + targetAuthor, targetTS := dm.GetPinMessage().GetTargetAuthorAciBinary(), dm.GetPinMessage().GetTargetSentTimestamp() + if dm.PinMessage == nil { + body = "Unpinned a message" + targetAuthor, targetTS = dm.GetUnpinMessage().GetTargetAuthorAciBinary(), dm.GetUnpinMessage().GetTargetSentTimestamp() + } + var replyTo *networkid.MessageOptionalPartID + if len(targetAuthor) == 16 { + replyTo = &networkid.MessageOptionalPartID{ + MessageID: signalid.MakeMessageID(uuid.UUID(targetAuthor), targetTS), + } + } + return &bridgev2.ConvertedMessagePart{ + Type: event.EventMessage, + Content: &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: body, + }, + }, replyTo +} + var invalidPollVote = &bridgev2.ConvertedMessagePart{ Type: event.EventUnstablePollResponse, Content: &event.MessageEventContent{}, From 03c8d29ae7ef6441ce0f5d33822710724cb90d2e Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 11 Nov 2026 02:36:40 +0000 Subject: [PATCH 44/46] signalmeow/receiving: fetch PNI identity from the primary device when verifying PNI signatures (#720) --- pkg/signalmeow/receiving.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index e99047d..dcc748a 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -922,7 +922,7 @@ func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsign Stringer("aci", sender.UUID). Stringer("pni", pni). Msg("Fetching PNI identity for signature verification as it wasn't found in store") - err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 0) + err = cli.FetchAndProcessPreKey(ctx, pniServiceID, 1) if err != nil { return fmt.Errorf("failed to fetch prekey for PNI %s after identity wasn't found in store: %w", pni, err) } else if pniIdentity, err = cli.Store.IdentityKeyStore.GetIdentityKey(ctx, pniServiceID); err != nil { From 561064e8f572908019f16136b129d0187be71e1a Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 11 Nov 2026 02:36:43 +0000 Subject: [PATCH 45/46] handlematrix: send admin deletes for other members' messages in groups (#722) --- pkg/connector/handlematrix.go | 46 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index b384d58..ca7af09 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -302,22 +302,50 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid } 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 errDeleteOthersMessage } - 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), - }, - })) + } + } 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 { if !receipt.ReadUpTo.After(receipt.LastRead) { return nil From a96f214609cbe04426c637845902bc2fa5a0c537 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Wed, 11 Nov 2026 02:36:47 +0000 Subject: [PATCH 46/46] groupinfo,signalmeow: make ended groups read-only (#725) --- pkg/connector/groupinfo.go | 25 ++++++++++++++++++++++++- pkg/signalmeow/groupcache.go | 3 +++ pkg/signalmeow/groups.go | 10 +++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go index cd50964..fa75743 100644 --- a/pkg/connector/groupinfo.go +++ b/pkg/connector/groupinfo.go @@ -39,6 +39,7 @@ import ( var defaultPL = 0 var moderatorPL = 50 +var terminatedPL = 100 func roleToPL(role signalmeow.GroupMemberRole) *int { switch role { @@ -70,6 +71,21 @@ func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess si plc.Events[event.StateBeeperDisappearingTimer] = attributePL } +func applyTerminated(plc *bridgev2.PowerLevelOverrides) { + plc.EventsDefault = &terminatedPL + plc.StateDefault = &terminatedPL + plc.Invite = &terminatedPL + plc.Kick = &terminatedPL + plc.Ban = &terminatedPL + plc.Redact = &terminatedPL + for _, evtType := range []event.Type{ + event.EventReaction, event.EventRedaction, event.StatePowerLevels, event.StateRoomName, + event.StateRoomAvatar, event.StateTopic, event.StateBeeperDisappearingTimer, + } { + plc.Events[evtType] = terminatedPL + } +} + func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) { if memberAccess == signalmeow.AccessControl_ADMINISTRATOR { plc.Invite = &moderatorPL @@ -125,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), @@ -265,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) @@ -276,6 +296,9 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID if groupChange.ModifyMemberAccess != nil { applyMembersAccess(pls, *groupChange.ModifyMemberAccess) } + if groupChange.TerminateGroup { + applyTerminated(pls) + } } if groupChange.ModifyAddFromInviteLinkAccess != nil { ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{ diff --git a/pkg/signalmeow/groupcache.go b/pkg/signalmeow/groupcache.go index 7f5d282..3a676fb 100644 --- a/pkg/signalmeow/groupcache.go +++ b/pkg/signalmeow/groupcache.go @@ -305,6 +305,9 @@ func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsi if change.ModifyAnnouncementsOnly != nil { cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly } + if change.TerminateGroup { + cached.Terminated = true + } if change.ModifyDisappearingMessagesDuration != nil { cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration } diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 411427a..882e048 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -81,6 +81,7 @@ type Group struct { Members []*GroupMember Description string AnnouncementsOnly bool + Terminated bool Revision uint32 DisappearingMessagesDuration uint32 AccessControl *GroupAccessControl @@ -206,6 +207,7 @@ type GroupChange struct { DeleteBannedMembers []*libsignalgo.ServiceID PromotePendingPniAciMembers []*PromotePendingPniAciMember ModifyInviteLinkPassword *types.SerializedInviteLinkPassword + TerminateGroup bool } func (groupChange *GroupChange) isEmpty() bool { @@ -226,7 +228,8 @@ func (groupChange *GroupChange) isEmpty() bool { len(groupChange.PromoteRequestingMembers) == 0 && groupChange.ModifyDescription == nil && groupChange.ModifyAnnouncementsOnly == nil && - len(groupChange.AddBannedMembers) == 0 + len(groupChange.AddBannedMembers) == 0 && + !groupChange.TerminateGroup } func (groupChange *GroupChange) resolveConflict(group *Group) { @@ -497,6 +500,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast // These aren't encrypted decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.Revision = encryptedGroup.Version + decryptedGroup.Terminated = encryptedGroup.Terminated decryptedGroup.AnnouncementsOnly = encryptedGroup.AnnouncementsOnly // Decrypt members @@ -1077,6 +1081,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange if encryptedActions.ModifyAnnouncementsOnly != nil { decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly } + decryptedGroupChange.TerminateGroup = encryptedActions.TerminateGroup != nil if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 { timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer) if err != nil { @@ -1802,8 +1807,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent "Cached-Send-Endorsements": "0", }, } - // highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false - path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState) + path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 7, includeFirstState) response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts) defer web.CloseBody(response) if err != nil {