Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-11-09 07:39:20 -05:00

Compare commits

..
Author SHA1 Message Date
Kishan Bagaria
490f5af343 - 2026-03-19 10:52:30 -07:00
212 changed files with 7948 additions and 54515 deletions

View file

@ -7,12 +7,10 @@ type: Bug
---
<!-- Include relevant logs, the bridge version and other important details here -->
<!--
Remember to include relevant logs, the bridge version and any other details.
### Checklist
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!signal version` command output is: ``
It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
ignored or closed immediately.
-->

View file

@ -11,14 +11,14 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.26", "1.27"]
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
@ -30,7 +30,6 @@ 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
@ -41,14 +40,14 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.26", "1.27"]
name: Test ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
go-version: ["1.25", "1.26"]
name: Test ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true

View file

@ -22,10 +22,7 @@ repos:
# TODO: reenable this and fix all the problems
- repo: https://github.com/beeper/pre-commit-go
rev: v0.5.0
rev: v0.4.2
hooks:
- id: zerolog-ban-msgf
- id: zerolog-use-stringer
- id: bridgelint
language: system
verbose: true

View file

@ -1,77 +1,3 @@
# 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.
* Added support for phonenumberless accounts.
* Added support for starting chats with usernames and username links.
* Added support for blocking and unblocking users on Signal.
* Added support for new view-limited media interface in bridgev2.
* Added proper sender certificate validation.
* Changed storage sync to only sync once if repeated fetch latest notices are
received.
* Fixed contact syncs not happening even when `sync_contacts_on_startup` was
set to true.
* Fixed Signal encryption using wrong timestamp precision for session expiry.
* Fixed sender key sharing not handling removed users properly.
* Fixed handling retry receipts with no sender key info.
* Fixed deadlock in prekey upload loop if the client gets logged out.
* Fixed infinite loop when importing sticker packs with duplicate emoji
shortcuts.
# v26.09
* Bumped minimum Go version to 1.26.
* Updated libsignal to v0.102.2.
* Fixed handling messages with invalid values in formatting body ranges.
* Fixed websocket request retrying not working correctly if the first attempt
times out.
* Fixed another potential race condition with edited message bridging.
# v26.08
* Updated libsignal to v0.100.0
* Added support for handling own profile key changes.
* Changed attachment bridging behavior to match Signal Desktop.
* Fixed edited messages being bridged twice if certain race conditions occurred.
# v26.07
* Updated Docker image to Alpine 3.24.
* Updated libsignal to v0.97.2.
* Added automatic retry when sender key send fails due to missing session.
* Fixed changing poll votes from Matrix.
# v26.06
* Updated libsignal to v0.94.4.
* Updated mrenclave to fix looking up phone numbers.
* Changed knock accept handling to auto-join the ghost user afterwards.
# v26.05
* Updated libsignal to v0.93.2.
* Added support for importing sticker packs from Signal.
# v26.04
* Updated libsignal to v0.92.1.
* Added support for admin message deletes from Signal.
* Added support for binary service IDs in storage service.
* Fixed `private_chat_portal_meta` option not setting DM room names correctly.
* Fixed panic if user is logged out during initial chat sync.
* Fixed avatar upload failing when creating new Signal group.
# v26.03
* Switched to sending binary service ID fields in outgoing messages.

View file

@ -1 +0,0 @@
See <https://docs.mau.fi/bridges/general/contributing.html>

View file

@ -1,6 +1,6 @@
# -- Build libsignal (with Rust) --
FROM rust:1-alpine AS rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev protobuf-dev
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
WORKDIR /build
# Copy all files needed for Rust build, and no Go files
@ -10,7 +10,7 @@ COPY build-rust.sh .
RUN ./build-rust.sh
# -- Build mautrix-signal (with Go) --
FROM golang:1-alpine3.24 AS go-builder
FROM golang:1-alpine3.23 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev zlib-dev
WORKDIR /build
@ -32,7 +32,7 @@ EOF
RUN ./build-go.sh
# -- Run mautrix-signal --
FROM alpine:3.24
FROM alpine:3.23
ENV UID=1337 \
GID=1337

View file

@ -1,6 +1,6 @@
ARG DOCKER_HUB="docker.io"
FROM ${DOCKER_HUB}/alpine:3.24
FROM ${DOCKER_HUB}/alpine:3.23
ENV UID=1337 \
GID=1337

View file

@ -0,0 +1,147 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2024 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 istributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/id"
)
func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request, create bool) {
login := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if login == nil {
return
}
api := login.Client.(bridgev2.IdentifierResolvingNetworkAPI)
phonenum := r.PathValue("phonenum")
resp, err := api.ResolveIdentifier(r.Context(), phonenum, create)
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to resolve identifier")
JSONResponse(w, http.StatusInternalServerError, &Error{
Error: fmt.Sprintf("Failed to resolve identifier: %v", err),
ErrCode: "M_UNKNOWN",
})
return
} else if resp == nil {
JSONResponse(w, http.StatusNotFound, &Error{
ErrCode: mautrix.MNotFound.ErrCode,
Error: "User not found on Signal",
})
return
}
status := http.StatusOK
apiResp := &ResolveIdentifierResponse{
ChatID: ResolveIdentifierResponseChatID{
UUID: string(resp.UserID),
Number: phonenum,
},
}
if resp.Ghost != nil {
if resp.UserInfo != nil {
resp.Ghost.UpdateInfo(r.Context(), resp.UserInfo)
}
apiResp.OtherUser = &ResolveIdentifierResponseOtherUser{
MXID: resp.Ghost.Intent.GetMXID(),
DisplayName: resp.Ghost.Name,
AvatarURL: resp.Ghost.AvatarMXC.ParseOrIgnore(),
}
}
if resp.Chat != nil {
if resp.Chat.Portal == nil {
resp.Chat.Portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to get portal")
JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
Err: "Failed to get portal",
ErrCode: "M_UNKNOWN",
})
return
}
}
if create && resp.Chat.Portal.MXID == "" {
apiResp.JustCreated = true
status = http.StatusCreated
err = resp.Chat.Portal.CreateMatrixRoom(r.Context(), login, resp.Chat.PortalInfo)
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to create portal room")
JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
Err: "Failed to create portal room",
ErrCode: "M_UNKNOWN",
})
return
}
}
apiResp.RoomID = resp.Chat.Portal.MXID
}
JSONResponse(w, status, &Response{
Success: true,
Status: "ok",
ResolveIdentifierResponse: apiResp,
})
}
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
legacyResolveIdentifierOrStartChat(w, r, false)
}
func legacyProvPM(w http.ResponseWriter, r *http.Request) {
legacyResolveIdentifierOrStartChat(w, r, true)
}
func JSONResponse(w http.ResponseWriter, status int, response any) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(response)
}
type Error struct {
Success bool `json:"success"`
Error string `json:"error"`
ErrCode string `json:"errcode"`
}
type Response struct {
Success bool `json:"success"`
Status string `json:"status"`
// For response in ResolveIdentifier
*ResolveIdentifierResponse
}
type ResolveIdentifierResponse struct {
RoomID id.RoomID `json:"room_id"`
ChatID ResolveIdentifierResponseChatID `json:"chat_id"`
JustCreated bool `json:"just_created"`
OtherUser *ResolveIdentifierResponseOtherUser `json:"other_user,omitempty"`
}
type ResolveIdentifierResponseChatID struct {
UUID string `json:"uuid"`
Number string `json:"number"`
}
type ResolveIdentifierResponseOtherUser struct {
MXID id.UserID `json:"mxid"`
DisplayName string `json:"displayname"`
AvatarURL id.ContentURI `json:"avatar_url"`
}

View file

@ -37,7 +37,7 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.",
Version: "26.10",
Version: "26.03",
SemCalVer: true,
Connector: &connector.SignalConnector{},
@ -45,6 +45,12 @@ var m = mxmain.BridgeMain{
func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v2/pm/{phonenum}", legacyProvPM)
}
}
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}

46
go.mod
View file

@ -1,51 +1,51 @@
module go.mau.fi/mautrix-signal
go 1.26.0
go 1.25.0
toolchain go1.27.2
toolchain go1.26.1
tool go.mau.fi/util/cmd/maubuild
require (
github.com/coder/websocket v1.8.15
github.com/coder/websocket v1.8.14
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
github.com/google/uuid v1.6.0
github.com/mattn/go-pointer v0.0.1
github.com/rs/zerolog v1.35.1
github.com/stretchr/testify v1.12.0
github.com/tidwall/gjson v1.20.0
go.mau.fi/util v0.11.0
golang.org/x/crypto v0.58.0
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e
golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.7
golang.org/x/crypto v0.49.0
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/net v0.52.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d
maunium.net/go/mautrix v0.26.4
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.52 // indirect
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.6 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.42.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect

96
go.sum
View file

@ -2,15 +2,17 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -22,33 +24,39 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@ -57,35 +65,31 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY=
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -95,5 +99,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.20261026145030-920bc073ba8d h1:DsMn0i7cBKcQZ0AVxJm9hfWAu6xJdjRrf457EtXxyNY=
maunium.net/go/mautrix v0.32.1-0.20261026145030-920bc073ba8d/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU=
maunium.net/go/mautrix v0.26.4 h1:enHSnkf0L2V9+VnfJfNhKSReSW6pBKS/x3Su+v+Vovs=
maunium.net/go/mautrix v0.26.4/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=

View file

@ -77,8 +77,8 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
}
// GetBackupChatItems returns in reverse chronological order, so flip the list
slices.Reverse(items)
var firstDirectionfulProcessed bool
var isRead bool
var readUpTo time.Time
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items))
attMap := make(msgconv.AttachmentMap)
recipientMap := make(map[uint64]*backuppb.Recipient)
@ -123,17 +123,20 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
case *backuppb.ChatItem_Incoming:
streamOrder = int64(dt.Incoming.GetDateServerSent())
prevStreamOrder = streamOrder
isRead = dt.Incoming.Read
if isRead {
readUpTo = time.UnixMilli(int64(item.DateSent))
if !firstDirectionfulProcessed {
firstDirectionfulProcessed = true
isRead = dt.Incoming.Read
}
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)
isRead = true
readUpTo = time.UnixMilli(int64(item.DateSent))
if !firstDirectionfulProcessed {
firstDirectionfulProcessed = true
isRead = true
}
}
if len(attMap) > 0 {
clear(attMap)
@ -175,20 +178,16 @@ 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.
// If backwards backfilling isn't enabled, delete immediately after the first backfill request.
if (!params.Forward && len(items) < params.Count) || !s.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
if (!params.Forward && len(items) < params.Count) || (!s.Main.Bridge.Config.Backfill.Queue.Enabled && !s.Main.Bridge.Config.Backfill.WillPaginateManually) {
err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")

View file

@ -23,13 +23,11 @@ import (
"go.mau.fi/util/ffmpeg"
"go.mau.fi/util/jsontime"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalid"
)
func supportedIfFFmpeg() event.CapabilitySupportLevel {
@ -40,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.signal.capabilities.2026_10_21"
base := "fi.mau.signal.capabilities.2025_12_09"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -48,7 +46,7 @@ func capID() string {
}
const MaxFileSize = 100 * 1024 * 1024
const MaxTextLength = 64 * 1024
const MaxTextLength = 2000
var signalCaps = &event.RoomFeatures{
ID: capID(),
@ -79,11 +77,9 @@ var signalCaps = &event.RoomFeatures{
"image/jpeg": event.CapLevelFullySupported,
"image/webp": event.CapLevelFullySupported,
"image/bmp": event.CapLevelFullySupported,
"image/avif": event.CapLevelFullySupported,
},
MaxWidth: 4096,
MaxHeight: 4096,
ViewLimitedTypes: []*event.BeeperViewLimitedMedia{{Type: "count", Count: 1}},
MaxSize: MaxFileSize,
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxTextLength,
@ -94,7 +90,6 @@ var signalCaps = &event.RoomFeatures{
"video/ogg": event.CapLevelFullySupported,
"video/webm": event.CapLevelFullySupported,
},
ViewLimitedTypes: []*event.BeeperViewLimitedMedia{{Type: "count", Count: 1}},
MaxSize: MaxFileSize,
Caption: event.CapLevelFullySupported,
MaxCaptionLength: MaxTextLength,
@ -103,10 +98,6 @@ var signalCaps = &event.RoomFeatures{
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/aac": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported,
"audio/mp4": event.CapLevelFullySupported,
"audio/wav": event.CapLevelFullySupported,
},
MaxSize: MaxFileSize,
},
@ -120,8 +111,7 @@ var signalCaps = &event.RoomFeatures{
},
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
// Signal clients will only render static webp, so apng is preferred
"image/webp": event.CapLevelPartialSupport,
"image/webp": event.CapLevelFullySupported,
"image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(),
@ -160,23 +150,18 @@ var signalCaps = &event.RoomFeatures{
event.MemberActionBan: event.CapLevelFullySupported,
event.MemberActionKick: event.CapLevelFullySupported,
},
MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelFullySupported,
PollMaxOptions: 10,
PollOptionMaxLength: 100,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
EditMaxCount: 10,
EditMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
Delete: event.CapLevelFullySupported,
DeleteForMe: false,
DeleteMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
DisappearingTimer: signalDisappearingCap,
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelRejected,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
EditMaxCount: 10,
EditMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
Delete: event.CapLevelFullySupported,
DeleteForMe: false,
DeleteMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
DisappearingTimer: signalDisappearingCap,
Reaction: event.CapLevelFullySupported,
ReactionCount: 1,
@ -198,7 +183,6 @@ var signalDisappearingCap = &event.DisappearingTimerCapability{
var signalCapsNoteToSelf *event.RoomFeatures
var signalCapsDM *event.RoomFeatures
var signalCapsPNIDM *event.RoomFeatures
func init() {
signalCapsDM = ptr.Clone(signalCaps)
@ -207,16 +191,7 @@ func init() {
signalCapsDM.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
}
signalCapsPNIDM = ptr.Clone(signalCapsDM)
signalCapsPNIDM.ID += "+pni"
signalCapsDM.BlockUser = true
signalCapsDM.ReportSpam = true
signalCapsNoteToSelf = ptr.Clone(signalCapsDM)
signalCapsNoteToSelf.BlockUser = false
signalCapsNoteToSelf.ReportSpam = false
signalCapsNoteToSelf.EditMaxAge = nil
signalCapsNoteToSelf.DeleteMaxAge = nil
signalCapsNoteToSelf.ID = capID() + "+note_to_self"
@ -226,10 +201,6 @@ func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Por
if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
return signalCapsNoteToSelf
} else if portal.RoomType == database.RoomTypeDM {
userID, _, _ := signalid.ParsePortalID(portal.ID)
if userID.Type == libsignalgo.ServiceIDTypePNI {
return signalCapsPNIDM
}
return signalCapsDM
}
return signalCaps
@ -240,11 +211,10 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
LookupUsername: true,
LookupUsername: false, // TODO implement
ContactList: true,
},
GroupCreation: map[string]bridgev2.GroupTypeCapabilities{
@ -265,5 +235,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 15
return 1, 7
}

View file

@ -75,7 +75,7 @@ func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *b
if userID.Type != libsignalgo.ServiceIDTypePNI && (!s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt)) {
return nil, nil
}
return s.contactToUserInfo(ctx, contact, meta.Username, false)
return s.contactToUserInfo(ctx, contact)
}
func (s *SignalClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
@ -99,7 +99,7 @@ func (s *SignalClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal)
}
}
func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Recipient, username string, saveUsername bool) (*bridgev2.UserInfo, error) {
func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Recipient) (*bridgev2.UserInfo, error) {
isBot := false
ui := &bridgev2.UserInfo{
IsBot: &isBot,
@ -110,20 +110,13 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
changed = meta.ProfileFetchedAt.IsZero() && !contact.Profile.FetchedAt.IsZero()
meta.ProfileFetchedAt.Time = contact.Profile.FetchedAt
}
if saveUsername && username != "" && meta.Username != username {
meta.Username = username
changed = true
}
return
return false
},
}
if contact.E164 != "" {
ui.Identifiers = append(ui.Identifiers, "tel:"+contact.E164)
}
if username != "" {
ui.Identifiers = append(ui.Identifiers, "signal:"+username)
}
name := s.Main.Config.FormatDisplayname(contact, username)
name := s.Main.Config.FormatDisplayname(contact)
ui.Name = &name
if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" {
ui.Avatar = &bridgev2.Avatar{
@ -202,93 +195,45 @@ func (s *SignalClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.
return resp.Chat, nil
}
func (s *SignalClient) resolvePhone(ctx context.Context, number string) (aci, pni uuid.UUID, e164Number uint64, recipient *types.Recipient, err error) {
number, err = bridgev2.CleanPhoneNumber(number)
if err != nil {
err = bridgev2.WrapRespErr(err, mautrix.MInvalidParam)
return
}
e164Number, err = strconv.ParseUint(strings.TrimPrefix(number, "+"), 10, 64)
if err != nil {
err = bridgev2.WrapRespErr(fmt.Errorf("error parsing phone number: %w", err), mautrix.MInvalidParam)
return
}
e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
err = fmt.Errorf("error looking up number in local contact list: %w", err)
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
aci = recipient.ACI
pni = recipient.PNI
} else if resp, lookupErr := s.Client.LookupPhone(ctx, e164Number); lookupErr != nil {
err = wrapRespError(fmt.Errorf("error looking up number on server: %w", lookupErr))
} else {
aci = resp[e164Number].ACI
pni = resp[e164Number].PNI
if aci == uuid.Nil && pni == uuid.Nil {
recipient = nil
return
}
recipient, err = s.Client.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, e164String)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
}
aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
}
return
}
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()
return
}
const signalUserLinkPrefix = "https://signal.me/#eu/"
func (s *SignalClient) resolveLink(ctx context.Context, linkHandle string) (aci, pni uuid.UUID, username string, err error) {
username, err = s.Client.ResolveUsernameLink(ctx, linkHandle)
if err != nil {
err = bridgev2.WrapRespErr(err, mautrix.MInvalidParam)
return
} else if username == "" {
return
}
aci, pni, err = s.resolveUsername(ctx, username)
return
}
func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ bool) (*bridgev2.ResolveIdentifierResponse, error) {
var aci, pni uuid.UUID
var e164Number uint64
var recipient *types.Recipient
var username string
serviceID, err := signalid.ParseUserIDAsServiceID(networkid.UserID(number))
if err == nil {
aci, pni = serviceID.ToACIAndPNI()
} else if strings.HasPrefix(number, "+") {
aci, pni, e164Number, recipient, err = s.resolvePhone(ctx, number)
} else if signalmeow.SignalUsernameRegex.MatchString(number) {
username = strings.TrimPrefix(number, "@")
aci, pni, err = s.resolveUsername(ctx, number)
} else if handle, ok := strings.CutPrefix(number, signalUserLinkPrefix); ok {
aci, pni, username, err = s.resolveLink(ctx, handle)
} else {
err = bridgev2.RespError(mautrix.MInvalidParam.WithMessage("Input must be an international phone number, username, link or user ID"))
}
if err != nil {
return nil, err
}
if recipient == nil {
if aci == uuid.Nil && pni == uuid.Nil {
return nil, nil
number, err = bridgev2.CleanPhoneNumber(number)
if err != nil {
return nil, bridgev2.WrapRespErr(err, mautrix.MInvalidParam)
}
e164Number, err = strconv.ParseUint(strings.TrimPrefix(number, "+"), 10, 64)
if err != nil {
return nil, bridgev2.WrapRespErr(fmt.Errorf("error parsing phone number: %w", err), mautrix.MInvalidParam)
}
e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
return nil, fmt.Errorf("error looking up number in local contact list: %w", err)
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
aci = recipient.ACI
pni = recipient.PNI
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
return nil, fmt.Errorf("error looking up number on server: %w", err)
} else {
aci = resp[e164Number].ACI
pni = resp[e164Number].PNI
if aci == uuid.Nil && pni == uuid.Nil {
return nil, nil
}
recipient, err = s.Client.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, e164String)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
}
aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
}
} else {
aci, pni = serviceID.ToACIAndPNI()
recipient, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
if err != nil {
return nil, fmt.Errorf("error loading recipient: %w", err)
@ -300,7 +245,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
Stringer("pni", pni).
Msg("Found resolve identifier target user")
userInfo, err := s.contactToUserInfo(ctx, recipient, username, true)
userInfo, err := s.contactToUserInfo(ctx, recipient)
if err != nil {
return nil, fmt.Errorf("failed to convert contact: %w", err)
}
@ -387,9 +332,9 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
if err != nil {
return nil, fmt.Errorf("failed to download avatar: %w", err)
}
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier)
if err != nil {
return nil, wrapRespError(fmt.Errorf("failed to upload avatar: %w", err))
return nil, fmt.Errorf("failed to upload avatar: %w", err)
}
}
portal, err := s.Main.Bridge.GetPortalByKey(ctx, s.makePortalKey(string(group.GroupIdentifier)))
@ -417,9 +362,9 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
return nil, fmt.Errorf("failed to set portal room ID: %w", err)
}
}
resp, err := s.Client.CreateGroup(ctx, group)
resp, err := s.Client.CreateGroup(ctx, group, avatarBytes)
if err != nil {
return nil, wrapRespError(fmt.Errorf("failed to create group: %w", err))
return nil, fmt.Errorf("failed to create group: %w", err)
}
if params.RoomID != "" {
// UpdateMatrixRoomID could do this for us if we passed ChatInfoSource to it,
@ -445,32 +390,23 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
}
resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients))
for i, recipient := range recipients {
if err = ctx.Err(); err != nil {
return nil, err
userInfo, err := s.contactToUserInfo(ctx, recipient)
if err != nil {
return nil, fmt.Errorf("failed to convert contact: %w", err)
}
recipientResp := &bridgev2.ResolveIdentifierResponse{
UserInfo: userInfo,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}
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 {
return nil, fmt.Errorf("failed to get ghost for %s: %w", recipient.ACI, err)
}
recipientResp.Ghost = ghost
username = ghost.Metadata.(*signalid.GhostMetadata).Username
} else {
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 {
return nil, fmt.Errorf("failed to convert contact: %w", err)
recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
}
resp[i] = recipientResp
}
@ -478,7 +414,7 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
}
func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
namePtr := bridgev2.DefaultChatName
name := ""
topic := PrivateChatTopic
selfUser := s.makeEventSender(s.Client.Store.ACI)
members := &bridgev2.ChatMemberList{
@ -505,7 +441,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
var serviceID libsignalgo.ServiceID
var avatar *bridgev2.Avatar
if recipient.ACI == uuid.Nil {
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient, ""))
name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else {
if backupChat == nil {
@ -517,7 +453,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
}
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI {
namePtr = ptr.Ptr(NoteToSelfName)
name = NoteToSelfName
avatar = &bridgev2.Avatar{
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
@ -538,13 +474,12 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
return &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(serviceID),
PortalInfo: &bridgev2.ChatInfo{
Name: namePtr,
Name: &name,
Avatar: avatar,
Topic: &topic,
Members: members,
Type: ptr.Ptr(database.RoomTypeDM),
UserBlocked: ptr.Ptr(recipient.Blocked),
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
CanBackfill: backupChat != nil,
ExtraUpdates: updatePortalSyncMeta,

View file

@ -19,7 +19,6 @@ package connector
import (
"context"
"encoding/base64"
"errors"
"time"
"github.com/google/uuid"
@ -29,36 +28,18 @@ 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"
)
func (s *SignalClient) stopChatSync() {
if cancel := s.cancelChatSync.Swap(nil); cancel != nil {
(*cancel)()
}
}
func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) {
defer cancel()
func (s *SignalClient) syncChats(ctx context.Context) {
if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced {
return
}
if s.Client.Store.EphemeralBackupKey != nil {
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
meta, err := s.Client.WaitForTransfer(ctx)
if errors.Is(err, signalmeow.ErrTransferTimedOut) {
zerolog.Ctx(ctx).Warn().Msg("Transfer archive wasn't uploaded in time, giving up")
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced = true
err = s.UserLogin.Save(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login metadata after transfer archive timed out")
}
return
} else if err != nil {
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to request transfer archive")
return
} else if meta.Error != "" {
@ -84,22 +65,10 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
}
zerolog.Ctx(ctx).Info().Int("chat_count", len(chats)).Msg("Fetched chats to sync from database")
for _, chat := range chats {
if ctx.Err() != nil {
zerolog.Ctx(ctx).Debug().
AnErr("ctx_err", ctx.Err()).
Msg("Context cancelled while syncing chats, stopping")
return
}
recipient, err := s.Client.Store.BackupStore.GetBackupRecipient(ctx, chat.RecipientId)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat")
continue
} else if recipient == nil {
zerolog.Ctx(ctx).Warn().
Uint64("backup_chat_id", chat.Id).
Uint64("backup_recipient_id", chat.RecipientId).
Msg("No recipient found for chat")
continue
}
resyncEvt := &simplevent.ChatResync{
EventMeta: simplevent.EventMeta{
@ -161,10 +130,7 @@ func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc)
groupID := types.GroupIdentifier(base64.StdEncoding.EncodeToString(rawGroupID[:]))
groupInfo, err := s.getGroupInfo(ctx, groupID, dest.Group.GetSnapshot().GetVersion(), chat)
if err != nil {
zerolog.Ctx(ctx).Err(err).
Uint64("recipient_id", recipient.Id).
Stringer("group_id", groupID).
Msg("Failed to get full group info")
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full group info")
continue
}
resyncEvt.PortalKey = s.makePortalKey(string(groupID))

View file

@ -19,7 +19,6 @@ package connector
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/rs/zerolog"
@ -27,7 +26,6 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
@ -40,15 +38,12 @@ type SignalClient struct {
Client *signalmeow.Client
Ghost *bridgev2.Ghost
recentMessageRooms *exsync.RingBuffer[networkid.MessageID, networkid.PortalKey]
queueEmptyWaiter *exsync.Event
cancelChatSync atomic.Pointer[context.CancelFunc]
queueEmptyWaiter *exsync.Event
}
var (
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@ -79,19 +74,10 @@ func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType b
}
}
func (s *SignalClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
return s.Main.MsgConv.DownloadImagePack(ctx, url)
}
func (s *SignalClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
return []*event.ImagePackMetadata{}, nil
}
func (s *SignalClient) LogoutRemote(ctx context.Context) {
if s.Client == nil {
return
}
s.stopChatSync()
err := s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
@ -182,17 +168,20 @@ 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, Error: "signal-disconnected"})
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect})
} else {
s.UserLogin.BridgeState.Send(connectionErrorState(err))
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
}
}
}
case signalmeow.SignalConnectionEventLoggedOut:
s.stopChatSync()
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
s.sendLoggedOutState(err)
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()})
}
err = s.Client.ClearKeysAndDisconnect(context.TODO())
if err != nil {
s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect")
@ -200,18 +189,18 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
case signalmeow.SignalConnectionEventError:
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState")
s.UserLogin.BridgeState.Send(connectionErrorState(err))
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
case signalmeow.SignalConnectionEventFatalError:
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.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: 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.sendLoggedOutState(nil)
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
}
}
}
@ -219,7 +208,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, Error: "signal-not-logged-in", UserAction: status.UserActionRelogin})
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You're not logged into Signal"})
return
}
s.updateRemoteProfile(ctx, false)
@ -247,11 +236,6 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
log.Err(status.Err).Msg("Authed websocket disconnected")
case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Authed websocket logged out")
s.sendLoggedOutState(status.Err)
err = s.Client.ClearKeysAndDisconnect(ctx)
if err != nil {
log.Err(err).Msg("Failed to clear keys and disconnect")
}
return fmt.Errorf("authed websocket logged out: %w", status.Err)
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError:
log.Err(status.Err).Msg("Authed websocket error")
@ -290,7 +274,6 @@ func (s *SignalClient) Disconnect() {
if s.Client == nil {
return
}
s.stopChatSync()
err := s.Client.StopReceiveLoops()
if err != nil {
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
@ -298,25 +281,32 @@ func (s *SignalClient) Disconnect() {
}
func (s *SignalClient) postLoginConnect() {
ctx := s.UserLogin.Log.WithContext(s.Main.Bridge.BackgroundCtx)
ctx := s.UserLogin.Log.WithContext(context.Background())
// TODO it would be more proper to only connect after syncing,
// but currently syncing will fetch group info online, so it has to be connected.
s.tryConnect(ctx, 0, false)
if s.Client.Store.EphemeralBackupKey != nil {
go func() {
if s.Client.Store.MasterKey != nil {
s.Client.SyncStorage(ctx)
} else {
s.UserLogin.Log.Warn().Msg("No master key for storage sync before backup sync")
}
s.syncChats(ctx)
}()
} else if s.Client.Store.MasterKey != nil {
go s.Client.SyncStorage(ctx)
}
}
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSync bool) {
if ctx.Err() != nil {
zerolog.Ctx(ctx).Debug().
Int("retry_count", retryCount).
AnErr("ctx_err", ctx.Err()).
Msg("Context is canceled, not trying to connect")
return
}
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, doSync bool) {
if retryCount == 0 {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
}
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: "signal-connect-failed", Info: map[string]any{"go_error": err.Error()}})
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
retryInSeconds := 2 << retryCount
if retryInSeconds > 150 {
retryInSeconds = 150
@ -328,33 +318,11 @@ func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSy
zerolog.Ctx(ctx).Info().Msg("Context canceled, exit tryConnect")
return
}
s.tryConnect(ctx, retryCount+1, noLoginSync)
return
}
syncCtx, cancel := context.WithCancel(ctx)
if oldCancel := s.cancelChatSync.Swap(&cancel); oldCancel != nil {
(*oldCancel)()
}
go s.bridgeStateLoop(ch)
if noLoginSync {
go s.syncChats(syncCtx, cancel)
s.tryConnect(ctx, retryCount+1, doSync)
} else {
// TODO it would be more proper to only connect after syncing,
// but currently syncing will fetch group info online, so it has to be connected.
if s.Client.Store.EphemeralBackupKey != nil {
go func() {
if s.Client.Store.MasterKey != nil {
s.Client.StorageSync(ctx)
} else {
s.UserLogin.Log.Warn().Msg("No master key for storage sync before backup sync")
}
s.syncChats(syncCtx, cancel)
}()
} else {
cancel()
if s.Client.Store.MasterKey != nil {
s.Client.QueueStorageSync(ctx)
}
go s.bridgeStateLoop(ch)
if doSync {
go s.syncChats(ctx)
}
}
}

View file

@ -41,6 +41,7 @@ type SignalConfig struct {
DeviceName string `yaml:"device_name"`
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"`
DisappearViewOnce bool `yaml:"disappear_view_once"`
ExtEvPolls bool `yaml:"extev_polls"`
displaynameTemplate *template.Template `yaml:"-"`
@ -74,16 +75,13 @@ type DisplaynameParams struct {
AboutEmoji string
}
func (c *SignalConfig) FormatDisplayname(contact *types.Recipient, username string) string {
if username != "" && !strings.HasPrefix(username, "@") {
username = "@" + username
}
func (c *SignalConfig) FormatDisplayname(contact *types.Recipient) string {
var nameBuf strings.Builder
err := c.displaynameTemplate.Execute(&nameBuf, &DisplaynameParams{
ProfileName: contact.Profile.Name,
ContactName: contact.ContactName,
Nickname: contact.Nickname,
Username: username,
Username: "",
PhoneNumber: contact.E164,
UUID: contact.ACI.String(),
ACI: contact.ACI.String(),
@ -105,6 +103,7 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "device_name")
helper.Copy(up.Str, "note_to_self_avatar")
helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "disappear_view_once")
helper.Copy(up.Bool, "extev_polls")
}

View file

@ -20,7 +20,6 @@ import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/google/uuid"
@ -45,9 +44,6 @@ type SignalConnector struct {
Store *store.Container
Bridge *bridgev2.Bridge
Config SignalConfig
lastTxnTS int64
lastTxnTSLock sync.Mutex
}
var _ bridgev2.NetworkConnector = (*SignalConnector)(nil)
@ -70,6 +66,7 @@ func (s *SignalConnector) Init(bridge *bridgev2.Bridge) {
s.Bridge = bridge
s.MsgConv = msgconv.NewMessageConverter(bridge)
s.MsgConv.LocationFormat = s.Config.LocationFormat
s.MsgConv.DisappearViewOnce = s.Config.DisappearViewOnce
s.MsgConv.ExtEvPolls = s.Config.ExtEvPolls
bridge.Commands.(*commands.Processor).AddHandlers(CmdDiscardSenderKey)
}
@ -94,11 +91,8 @@ 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() {
@ -123,8 +117,7 @@ func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.Use
Main: s,
UserLogin: login,
recentMessageRooms: exsync.NewRingBuffer[networkid.MessageID, networkid.PortalKey](32),
queueEmptyWaiter: exsync.NewEvent(),
queueEmptyWaiter: exsync.NewEvent(),
}
if device != nil {
sc.Client = signalmeow.NewClient(
@ -140,8 +133,5 @@ func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.Use
}
func (s *SignalConnector) GenerateTransactionID(userID id.UserID, roomID id.RoomID, eventType event.Type) networkid.RawTransactionID {
s.lastTxnTSLock.Lock()
defer s.lastTxnTSLock.Unlock()
s.lastTxnTS = max(time.Now().UnixMilli(), s.lastTxnTS+1)
return networkid.RawTransactionID(strconv.FormatInt(s.lastTxnTS, 10))
return networkid.RawTransactionID(strconv.FormatInt(time.Now().UnixMilli(), 10))
}

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"maunium.net/go/mautrix/bridgev2"
@ -29,7 +30,6 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to parse direct media id: %w", err)
}
var rawDataResp []byte
switch info := info.(type) {
case *signalid.DirectMediaAttachment:
log.Info().
@ -76,11 +76,18 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to to get group master key: %w", err)
}
rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, wrapDirectMediaError(err)
}
return &mediaproxy.GetMediaResponseCallback{
Callback: func(w io.Writer) (int64, error) {
data, err := client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return 0, err
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
case *signalid.DirectMediaProfileAvatar:
log.Info().
Stringer("user_id", info.UserID).
@ -104,27 +111,19 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("profile key not found")
}
rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, wrapDirectMediaError(err)
}
case *signalid.DirectMediaSticker:
log.Info().
Hex("pack_id", info.PackID).
Uint32("sticker_id", info.StickerID).
Msg("Direct downloading sticker")
return &mediaproxy.GetMediaResponseCallback{
Callback: func(w io.Writer) (int64, error) {
data, err := client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
if err != nil {
log.Err(err).Msg("Direct download failed")
return 0, err
}
rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
if err != nil {
log.Err(err).Msg("Direct download failed")
return nil, wrapDirectMediaError(err)
}
_, err = w.Write(data)
return int64(len(data)), err
},
}, nil
default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
}
if rawDataResp == nil {
return nil, fmt.Errorf("unexpected fallthrough with no data")
}
return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
}

View file

@ -1,183 +0,0 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"errors"
"fmt"
"net/http"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
func init() {
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
"signal-logged-out": "You have been logged out of Signal, please reconnect",
"signal-not-logged-in": "You're not logged into Signal",
"signal-disconnected": "Disconnected from Signal, reconnecting",
"signal-connection-error": "Connection to Signal failed, reconnecting",
"signal-server-error": "Signal server error, reconnecting",
"signal-rate-limited": "Rate limited by Signal, reconnecting",
"signal-connect-failed": "Failed to connect to Signal, retrying",
"signal-connection-failed": "Connection to Signal failed",
})
}
var (
errEditOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only edit your own messages")).
WithErrorAsMessage().
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
errDeleteOthersMessage = bridgev2.WrapErrorInStatus(errors.New("you can only delete your own messages")).
WithErrorAsMessage().
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
errInviteToDM = bridgev2.WrapErrorInStatus(errors.New("can't invite more users to Signal DMs")).
WithErrorAsMessage().
WithErrorReason(event.MessageStatusUnsupported).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
errUnsupportedDisappearingType = bridgev2.WrapErrorInStatus(errors.New("only disappearing timers that start after reading are supported on Signal")).
WithErrorAsMessage().
WithErrorReason(event.MessageStatusUnsupported).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
)
func wrapConversionError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
return bridgev2.WrapErrorInStatus(err).
WithMessage("failed to convert message for Signal").
WithIsCertain(true).
WithSendNotice(true)
}
func wrapSendError(err error) error {
if err == nil || errors.As(err, &bridgev2.MessageStatus{}) {
return err
}
switch {
case errors.Is(err, signalmeow.ErrUnregisteredUser):
return bridgev2.WrapErrorInStatus(err).
WithMessage("recipient isn't registered on Signal").
WithErrorReason(event.MessageStatusNetworkError).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, signalmeow.ErrRateLimited), errors.Is(err, signalmeow.RateLimitError):
return bridgev2.WrapErrorInStatus(err).
WithMessage("rate limited by Signal, try again later").
WithErrorReason(event.MessageStatusNetworkError).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, signalmeow.AuthorizationFailedError):
return bridgev2.WrapErrorInStatus(err).
WithMessage("you don't have permission to do that in this Signal group").
WithErrorReason(event.MessageStatusNoPermission).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
case errors.Is(err, signalmeow.GroupPatchNotAcceptedError):
return bridgev2.WrapErrorInStatus(err).
WithMessage("Signal rejected the group change").
WithErrorReason(event.MessageStatusNetworkError).
WithStatus(event.MessageStatusFail).
WithIsCertain(true).
WithSendNotice(true)
default:
return bridgev2.WrapErrorInStatus(err).
WithMessage("failed to send to Signal").
WithSendNotice(true)
}
}
func wrapRespError(err error) error {
var mapped bridgev2.RespError
var cdsRateLimit signalmeow.ContactDiscoveryRateLimitError
switch {
case errors.As(err, &cdsRateLimit), errors.Is(err, signalmeow.RateLimitError), errors.Is(err, signalmeow.ErrRateLimited),
grpcstatus.Code(err) == codes.ResourceExhausted:
mapped = bridgev2.RespError(mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithStatus(http.StatusBadRequest))
case errors.Is(err, signalmeow.AuthorizationFailedError):
mapped = bridgev2.RespError(mautrix.MForbidden.WithMessage("Signal denied the request"))
default:
mapped = bridgev2.RespError(mautrix.MUnknown.WithMessage("Request to Signal failed"))
}
return fmt.Errorf("%w: %w", mapped, err)
}
func wrapDirectMediaError(err error) error {
var statusErr web.HTTPStatusError
if !errors.As(err, &statusErr) {
return err
}
switch statusErr.StatusCode {
case http.StatusNotFound, http.StatusGone:
return mautrix.MNotFound.WithMessage("Media not found on Signal")
case http.StatusUnauthorized, http.StatusForbidden:
return mautrix.MForbidden.WithMessage("Signal denied access to the media")
case http.StatusTooManyRequests:
return mautrix.MLimitExceeded.WithMessage("Rate limited by Signal, please try again later").WithCanRetry(true)
default:
return mautrix.MUnknown.WithMessage("Unexpected response from Signal").WithStatus(http.StatusBadGateway).WithCanRetry(true)
}
}
func connectionErrorState(err error) status.BridgeState {
state := status.BridgeState{
StateEvent: status.StateTransientDisconnect,
Error: "signal-connection-error",
Info: map[string]any{"go_error": err.Error()},
}
var statusErr web.HTTPStatusError
if errors.As(err, &statusErr) {
if statusErr.StatusCode == http.StatusTooManyRequests {
state.Error = "signal-rate-limited"
} else if statusErr.StatusCode >= 500 {
state.Error = "signal-server-error"
}
}
return state
}
func (s *SignalClient) sendLoggedOutState(err error) {
state := status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: "signal-logged-out",
UserAction: status.UserActionRelogin,
}
if err != nil {
state.Info = map[string]any{"go_error": err.Error()}
}
s.UserLogin.BridgeState.Send(state)
}

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