1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-14 21:26:54 -04:00

Compare commits

..

3 commits

Author SHA1 Message Date
Tulir Asokan
7ffa38469f groupinfo: add exclude from timeline flag for group resyncs 2025-10-01 15:30:21 +03:00
Tulir Asokan
606d904cac dependencies: update mautrix-go 2025-10-01 15:30:18 +03:00
Tulir Asokan
c795cea7a1 signalmeow/groups: update to v2 api 2025-10-01 15:30:15 +03:00
123 changed files with 4700 additions and 11335 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 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
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. --> ignored or closed immediately.
-->
* [ ] 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: ``

View file

@ -11,14 +11,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: ["1.25", "1.26"] go-version: ["1.24", "1.25"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} name: Lint ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: true cache: true
@ -40,14 +40,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: ["1.25", "1.26"] go-version: ["1.24", "1.25"]
name: Test ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} name: Test ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: true cache: true

View file

@ -17,7 +17,7 @@ jobs:
lock-stale: lock-stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v6 - uses: dessant/lock-threads@v5
id: lock id: lock
with: with:
issue-inactive-days: 90 issue-inactive-days: 90

View file

@ -9,7 +9,7 @@ repos:
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang - repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.4 rev: v1.0.0-rc.2
hooks: hooks:
- id: go-imports - id: go-imports
exclude: "pb\\.go$" exclude: "pb\\.go$"

View file

@ -1,75 +1,3 @@
# 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.
* Added support for roundtripping large attachments via disk to avoid keeping
the entire file in memory during en/decryption.
# v26.02.2
* Added support for more new binary service ID fields.
# v26.02.1
* Updated libsignal to v0.87.5.
* Added support for new binary service ID fields that Signal 8.0 switched to.
# v26.02
* Bumped minimum Go version to 1.25.
* Updated libsignal to v0.87.1.
* Added automatic recovery for the session not found error from libsignal.
* Fixed sender key state not being cleared on logout properly.
# v26.01
* Updated libsignal to v0.86.12.
* Changed automatic contact list sync option to only sync every 3 days rather
than on every restart.
* Fixed sending messages to groups with no other registered members.
* Fixed sender key sends failing if some users had changed devices.
* Fixed timestamps of outgoing typing notifications in DMs.
# v25.12
* Updated libsignal to v0.86.8.
* Updated Docker image to Alpine 3.23.
* Added support for dropping incoming DMs from blocked contacts on Signal.
* Added support for sender key encryption when sending to groups, which makes
sending much faster and enables sending typing notifications.
* Added support for encryption retry receipts.
* Fixed bugs with handling poll votes.
* Fixed history transfer option not showing up when pairing with Signal Android.
* Fixed nicknames being cleared not being bridged
(thanks to [@Enzime] in [#623]).
[#623]: https://github.com/mautrix/signal/pull/623
[@Enzime]: https://github.com/Enzime
# v25.11
* Updated libsignal to v0.86.4.
* Added support for bridging invite state in groups for phone number invites.
* Added support for polls.
* Fixed PNI signature not being sent when replying to message requests.
* Fixed unnecessary repeating error notices when Signal is down.
* Fixed sticker size metadata on Matrix not matching how native Signal Desktop
renders them.
# v25.10
* Switched to calendar versioning.
* Updated libsignal to v0.84.0.
* Fixed backfill creating incorrect disappearing timer change notices.
# v0.8.7 (2025-09-16) # v0.8.7 (2025-09-16)
* Removed legacy provisioning API and database legacy migration. * Removed legacy provisioning API and database legacy migration.

View file

@ -1,17 +1,18 @@
# -- Build libsignal (with Rust) -- # -- Build libsignal (with Rust) --
FROM rust:1-alpine AS rust-builder 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 WORKDIR /build
# Copy all files needed for Rust build, and no Go files # Copy all files needed for Rust build, and no Go files
COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/. COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
COPY build-rust.sh . COPY build-rust.sh .
ARG DBG=0
RUN ./build-rust.sh RUN ./build-rust.sh
# -- Build mautrix-signal (with Go) -- # -- Build mautrix-signal (with Go) --
FROM golang:1-alpine3.23 AS go-builder FROM golang:1-alpine3.22 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev zlib-dev RUN apk add --no-cache git ca-certificates build-base olm-dev
WORKDIR /build WORKDIR /build
# Copy all files needed for Go build, and no Rust files # Copy all files needed for Go build, and no Rust files
@ -25,14 +26,20 @@ COPY pkg/connector/. pkg/connector/.
COPY cmd/. cmd/. COPY cmd/. cmd/.
COPY .git .git COPY .git .git
ARG DBG=0
ENV LIBRARY_PATH=. ENV LIBRARY_PATH=.
COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./ COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./
RUN <<EOF RUN <<EOF
if [ "$DBG" = 1 ]; then
go install github.com/go-delve/delve/cmd/dlv@latest
else
touch /go/bin/dlv
fi
EOF EOF
RUN ./build-go.sh RUN ./build-go.sh
# -- Run mautrix-signal -- # -- Run mautrix-signal --
FROM alpine:3.23 FROM alpine:3.22
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337
@ -41,6 +48,11 @@ RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go olm
COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal
COPY --from=go-builder /build/docker-run.sh /docker-run.sh COPY --from=go-builder /build/docker-run.sh /docker-run.sh
COPY --from=go-builder /go/bin/dlv /usr/bin/dlv
VOLUME /data VOLUME /data
ARG DBG
ARG DBGWAIT=0
ENV DBG=${DBG} DBGWAIT=${DBGWAIT}
RUN echo "Debug mode: DBG=${DBG} DBGWAIT=${DBGWAIT}"
CMD ["/docker-run.sh"] CMD ["/docker-run.sh"]

View file

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

View file

@ -5,7 +5,6 @@
* [x] Text * [x] Text
* [x] Formatting * [x] Formatting
* [x] Mentions * [x] Mentions
* [x] Polls
* [x] Media * [x] Media
* [x] Images * [x] Images
* [x] Audio files * [x] Audio files
@ -35,7 +34,6 @@
* [x] Text * [x] Text
* [x] Formatting * [x] Formatting
* [x] Mentions * [x] Mentions
* [x] Polls
* [ ] Media * [ ] Media
* [x] Images * [x] Images
* [x] Voice notes * [x] Voice notes
@ -67,8 +65,8 @@
* [ ] Delivery receipts (there's no good way to bridge these) * [ ] Delivery receipts (there's no good way to bridge these)
* [x] Disappearing messages * [x] Disappearing messages
* Misc * Misc
* [x] Automatic portal creation * [ ] Automatic portal creation
* [x] After login * [ ] After login
* [x] When receiving message * [x] When receiving message
* [x] Linking as secondary device * [x] Linking as secondary device
* [ ] Registering as primary device * [ ] Registering as primary device

View file

@ -1,2 +1,9 @@
#!/bin/sh #!/bin/sh
BINARY_NAME=mautrix-signal go tool maubuild "$@" MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
GO_LDFLAGS="-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
if [ "$DBG" = 1 ]; then
GO_GCFLAGS='all=-N -l'
else
GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
fi
go build -gcflags="$GO_GCFLAGS" -ldflags="$GO_LDFLAGS" -o mautrix-signal "$@" ./cmd/mautrix-signal

View file

@ -1,3 +1,10 @@
#!/bin/sh #!/bin/sh
# TODO fix linking with debug library
#if [ "$DBG" != 1 ]; then
# RUST_PROFILE=release
#else
# RUST_PROFILE=dev
#fi
RUST_PROFILE=release
git submodule update --init git submodule update --init
cd pkg/libsignalgo/libsignal && RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" cargo build -p libsignal-ffi --profile=release cd pkg/libsignalgo/libsignal && RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" cargo build -p libsignal-ffi --profile=$RUST_PROFILE

View file

@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
set -e
./build-rust.sh ./build-rust.sh
cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a . cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh

View file

@ -17,12 +17,9 @@
package main package main
import ( import (
"fmt"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain" "maunium.net/go/mautrix/bridgev2/matrix/mxmain"
"go.mau.fi/mautrix-signal/pkg/connector" "go.mau.fi/mautrix-signal/pkg/connector"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
// Information to find out exactly which commit the bridge was built from. // Information to find out exactly which commit the bridge was built from.
@ -37,14 +34,12 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal", Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal", URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.", Description: "A Matrix-Signal puppeting bridge.",
Version: "26.04", Version: "0.8.7",
SemCalVer: true,
Connector: &connector.SignalConnector{}, Connector: &connector.SignalConnector{},
} }
func main() { func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.PostStart = func() { m.PostStart = func() {
if m.Matrix.Provisioning != nil { if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier) m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)

41
go.mod
View file

@ -1,51 +1,48 @@
module go.mau.fi/mautrix-signal module go.mau.fi/mautrix-signal
go 1.25.0 go 1.24.0
toolchain go1.26.2 toolchain go1.25.1
tool go.mau.fi/util/cmd/maubuild
require ( require (
github.com/coder/websocket v1.8.14 github.com/coder/websocket v1.8.14
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mattn/go-pointer v0.0.1 github.com/mattn/go-pointer v0.0.1
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10
golang.org/x/crypto v0.50.0 golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/exp v0.0.0-20250911091902-df9299821621
golang.org/x/net v0.53.0 golang.org/x/net v0.44.0
golang.org/x/sync v0.20.0 google.golang.org/protobuf v1.36.9
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf
) )
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.12.3 // indirect github.com/lib/pq v1.10.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark v1.7.13 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.35.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.43.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

80
go.sum
View file

@ -1,18 +1,19 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= 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/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.14/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.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -22,19 +23,23 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@ -42,8 +47,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 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.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= 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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@ -51,37 +56,36 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 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=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10 h1:EvX/di02gOriKN0xGDJuQ5mgiNdAF4LJc8moffI7Svo=
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -91,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo= maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf h1:prmIYgiziW4A8H2v/TliQ7fis8uTWblabxyPIeLFlNg=
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM= maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf/go.mod h1:eWXuX2UAGye4AU7i/8Fv2L2Nh7L9kZtuv3R0O0n1KaM=

View file

@ -151,7 +151,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
if dm == nil { if dm == nil {
continue continue
} }
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, senderACI, s.Main.Bridge.Bot, dm, attMap) cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, s.Main.Bridge.Bot, dm, attMap)
convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions)) convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions))
for _, reaction := range reactions { for _, reaction := range reactions {
reactionSenderACI, err := getRecipientACI(reaction.AuthorId) reactionSenderACI, err := getRecipientACI(reaction.AuthorId)
@ -187,7 +187,7 @@ func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchM
CompleteCallback: func() { CompleteCallback: func() {
// When reaching the last backwards backfill batch, delete the chat from the backup store. // When reaching the last backwards backfill batch, delete the chat from the backup store.
// If backwards backfilling isn't enabled, delete immediately after the first backfill request. // 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) err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store") zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")

View file

@ -25,7 +25,6 @@ import (
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
) )
@ -38,7 +37,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.signal.capabilities.2026_05_12" base := "fi.mau.signal.capabilities.2025_08_25"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -111,8 +110,7 @@ var signalCaps = &event.RoomFeatures{
}, },
event.CapMsgSticker: { event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
// Signal clients will only render static webp, so apng is preferred "image/webp": event.CapLevelFullySupported,
"image/webp": event.CapLevelPartialSupport,
"image/png": event.CapLevelFullySupported, "image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported, "image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(), "image/gif": supportedIfFFmpeg(),
@ -138,19 +136,6 @@ var signalCaps = &event.RoomFeatures{
MaxSize: MaxFileSize, MaxSize: MaxFileSize,
}, },
}, },
State: event.StateFeatureMap{
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
},
MemberActions: event.MemberFeatureMap{
event.MemberActionInvite: event.CapLevelFullySupported,
event.MemberActionRevokeInvite: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported,
event.MemberActionBan: event.CapLevelFullySupported,
event.MemberActionKick: event.CapLevelFullySupported,
},
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
LocationMessage: event.CapLevelPartialSupport, LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelRejected, Poll: event.CapLevelRejected,
@ -170,12 +155,6 @@ var signalCaps = &event.RoomFeatures{
CustomEmojiReactions: false, CustomEmojiReactions: false,
ReadReceipts: true, ReadReceipts: true,
TypingNotifications: true, TypingNotifications: true,
DeleteChat: true,
MessageRequest: &event.MessageRequestFeatures{
AcceptWithMessage: event.CapLevelPartialSupport,
AcceptWithButton: event.CapLevelFullySupported,
},
} }
var signalDisappearingCap = &event.DisappearingTimerCapability{ var signalDisappearingCap = &event.DisappearingTimerCapability{
@ -183,16 +162,9 @@ var signalDisappearingCap = &event.DisappearingTimerCapability{
} }
var signalCapsNoteToSelf *event.RoomFeatures var signalCapsNoteToSelf *event.RoomFeatures
var signalCapsDM *event.RoomFeatures
func init() { func init() {
signalCapsDM = ptr.Clone(signalCaps) signalCapsNoteToSelf = ptr.Clone(signalCaps)
signalCapsDM.ID = capID() + "+dm"
signalCapsDM.MemberActions = nil
signalCapsDM.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
}
signalCapsNoteToSelf = ptr.Clone(signalCapsDM)
signalCapsNoteToSelf.EditMaxAge = nil signalCapsNoteToSelf.EditMaxAge = nil
signalCapsNoteToSelf.DeleteMaxAge = nil signalCapsNoteToSelf.DeleteMaxAge = nil
signalCapsNoteToSelf.ID = capID() + "+note_to_self" signalCapsNoteToSelf.ID = capID() + "+note_to_self"
@ -201,8 +173,6 @@ func init() {
func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures { func (s *SignalClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) { if portal.Receiver == s.UserLogin.ID && portal.ID == networkid.PortalID(s.UserLogin.ID) {
return signalCapsNoteToSelf return signalCapsNoteToSelf
} else if portal.RoomType == database.RoomTypeDM {
return signalCapsDM
} }
return signalCaps return signalCaps
} }
@ -212,7 +182,6 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true, AggressiveUpdateInfo: true,
ImplicitReadReceipts: true, ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{ Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true, CreateDM: true,
LookupPhone: true, LookupPhone: true,
@ -237,5 +206,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
} }
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) { func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 8 return 1, 5
} }

View file

@ -45,16 +45,13 @@ var (
_ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil) _ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil) _ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.GhostDMCreatingNetworkAPI = (*SignalClient)(nil)
) )
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
const PrivateChatTopic = "Signal private chat" const PrivateChatTopic = "Signal private chat"
const NoteToSelfName = "Signal Note to Self" const NoteToSelfName = "Signal Note to Self"
func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *bridgev2.Ghost, refreshAfter time.Duration) (*bridgev2.UserInfo, error) { func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *bridgev2.Ghost, refreshAfter time.Duration) (*bridgev2.UserInfo, error) {
userID, err := signalid.ParseUserIDAsServiceID(ghost.ID) userID, err := signalid.ParseUserID(ghost.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -62,17 +59,12 @@ func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *b
// Don't do unnecessary fetches in background mode // Don't do unnecessary fetches in background mode
return nil, nil return nil, nil
} }
var contact *types.Recipient contact, err := s.Client.ContactByACIWithRefreshAfter(ctx, userID, refreshAfter)
if userID.Type == libsignalgo.ServiceIDTypePNI {
contact, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, uuid.Nil, userID.UUID, nil)
} else {
contact, err = s.Client.ContactByACIWithRefreshAfter(ctx, userID.UUID, refreshAfter)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
meta := ghost.Metadata.(*signalid.GhostMetadata) meta := ghost.Metadata.(*signalid.GhostMetadata)
if userID.Type != libsignalgo.ServiceIDTypePNI && (!s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt)) { if !s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt) {
return nil, nil return nil, nil
} }
return s.contactToUserInfo(ctx, contact) return s.contactToUserInfo(ctx, contact)
@ -165,41 +157,18 @@ func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Rec
return ui, nil return ui, nil
} }
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool { func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
_, err := signalid.ParseUserIDAsServiceID(id) _, err := signalid.ParseUserIDAsServiceID(id)
return err == nil return err == nil
} }
func (s *SignalClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, createChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
parsedID, err := signalid.ParseUserIDAsServiceID(ghost.ID)
if err != nil {
return nil, err
}
resp, err := s.ResolveIdentifier(ctx, parsedID.String(), true)
if err != nil {
return nil, err
} else if resp == nil {
return nil, nil
}
resultID, err := signalid.ParseUserIDAsServiceID(resp.UserID)
if err != nil {
return nil, fmt.Errorf("failed to parse result user ID: %w", err)
}
if parsedID.Type == libsignalgo.ServiceIDTypePNI {
if resultID.Type == libsignalgo.ServiceIDTypeACI && !resultID.IsEmpty() {
resp.Chat.DMRedirectedTo = resp.UserID
} else {
resp.Chat.DMRedirectedTo = bridgev2.SpecialValueDMRedirectedToBot
}
}
return resp.Chat, nil
}
func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ bool) (*bridgev2.ResolveIdentifierResponse, error) {
var aci, pni uuid.UUID var aci, pni uuid.UUID
var e164Number uint64 var e164Number uint64
var recipient *types.Recipient var recipient *types.Recipient
serviceID, err := signalid.ParseUserIDAsServiceID(networkid.UserID(number)) serviceID, err := libsignalgo.ServiceIDFromString(number)
if err != nil { if err != nil {
number, err = bridgev2.CleanPhoneNumber(number) number, err = bridgev2.CleanPhoneNumber(number)
if err != nil { if err != nil {
@ -212,7 +181,7 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
e164String := fmt.Sprintf("+%d", e164Number) e164String := fmt.Sprintf("+%d", e164Number)
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil { if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
return nil, fmt.Errorf("error looking up number in local contact list: %w", err) 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))) { } else if recipient != nil {
aci = recipient.ACI aci = recipient.ACI
pni = recipient.PNI pni = recipient.PNI
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil { } else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
@ -228,9 +197,6 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone") zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
} }
aci, pni = recipient.ACI, recipient.PNI aci, pni = recipient.ACI, recipient.PNI
if aci != uuid.Nil {
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
}
} }
} else { } else {
aci, pni = serviceID.ToACIAndPNI() aci, pni = serviceID.ToACIAndPNI()
@ -250,29 +216,31 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, _ b
return nil, fmt.Errorf("failed to convert contact: %w", err) return nil, fmt.Errorf("failed to convert contact: %w", err)
} }
var userID networkid.UserID
if aci != uuid.Nil {
userID = signalid.MakeUserID(aci)
} else {
userID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni))
}
// createChat is a no-op: chats don't need to be created, and we always return chat info // createChat is a no-op: chats don't need to be created, and we always return chat info
resp := &bridgev2.ResolveIdentifierResponse{ if aci != uuid.Nil {
UserID: userID, ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(aci))
UserInfo: userInfo,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}
resp.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, resp.UserID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err) return nil, fmt.Errorf("failed to get ghost: %w", err)
} }
return resp, nil return &bridgev2.ResolveIdentifierResponse{
UserID: signalid.MakeUserID(aci),
UserInfo: userInfo,
Ghost: ghost,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}, nil
} else {
return &bridgev2.ResolveIdentifierResponse{
UserID: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
UserInfo: userInfo,
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
}, nil
}
} }
func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
group := &signalmeow.Group{ group := &signalmeow.Group{
Title: ptr.Val(params.Name).Name, Title: ptr.Val(params.Name).Name,
Members: make([]*signalmeow.GroupMember, 1, len(params.Participants)+1), Members: make([]*signalmeow.GroupMember, len(params.Participants)+1),
Description: ptr.Val(params.Topic).Topic, Description: ptr.Val(params.Topic).Topic,
AnnouncementsOnly: false, AnnouncementsOnly: false,
DisappearingMessagesDuration: uint32(ptr.Val(params.Disappear).Timer.Seconds()), DisappearingMessagesDuration: uint32(ptr.Val(params.Disappear).Timer.Seconds()),
@ -299,25 +267,14 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
ACI: s.Client.Store.ACI, ACI: s.Client.Store.ACI,
Role: signalmeow.GroupMember_ADMINISTRATOR, Role: signalmeow.GroupMember_ADMINISTRATOR,
} }
currentTS := uint64(time.Now().UnixMilli()) for i, member := range params.Participants {
for _, member := range params.Participants { userID, err := signalid.ParseUserID(member)
userID, err := signalid.ParseUserIDAsServiceID(member)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid user ID %q: %w", member, err) return nil, fmt.Errorf("invalid user ID %q: %w", member, err)
} }
if userID.Type == libsignalgo.ServiceIDTypeACI { group.Members[i+1] = &signalmeow.GroupMember{
group.Members = append(group.Members, &signalmeow.GroupMember{ ACI: userID,
ACI: userID.UUID,
Role: signalmeow.GroupMember_DEFAULT, // TODO set proper role from power levels Role: signalmeow.GroupMember_DEFAULT, // TODO set proper role from power levels
})
} else if userID.Type == libsignalgo.ServiceIDTypePNI {
// TODO check if this is correct
group.PendingMembers = append(group.PendingMembers, &signalmeow.PendingMember{
ServiceID: userID,
Role: signalmeow.GroupMember_DEFAULT,
AddedByUserID: s.Client.Store.ACI,
Timestamp: currentTS,
})
} }
} }
_, err := signalmeow.PrepareGroupCreation(group) _, err := signalmeow.PrepareGroupCreation(group)
@ -326,13 +283,13 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
} }
var avatarBytes []byte var avatarBytes []byte
var avatarMXC id.ContentURIString var avatarMXC id.ContentURIString
if params.Avatar != nil && params.Avatar.URL != "" { if params.Avatar != nil {
avatarMXC = params.Avatar.URL avatarMXC = params.Avatar.URL
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil) avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, params.Avatar.MSC3414File)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to download avatar: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to upload avatar: %w", err) return nil, fmt.Errorf("failed to upload avatar: %w", err)
} }
@ -362,7 +319,7 @@ func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCr
return nil, fmt.Errorf("failed to set portal room ID: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to create group: %w", err) return nil, fmt.Errorf("failed to create group: %w", err)
} }
@ -414,7 +371,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 { func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
namePtr := bridgev2.DefaultChatName name := ""
topic := PrivateChatTopic topic := PrivateChatTopic
selfUser := s.makeEventSender(s.Client.Store.ACI) selfUser := s.makeEventSender(s.Client.Store.ACI)
members := &bridgev2.ChatMemberList{ members := &bridgev2.ChatMemberList{
@ -441,7 +398,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
var serviceID libsignalgo.ServiceID var serviceID libsignalgo.ServiceID
var avatar *bridgev2.Avatar var avatar *bridgev2.Avatar
if recipient.ACI == uuid.Nil { if recipient.ACI == uuid.Nil {
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient)) name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI) serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else { } else {
if backupChat == nil { if backupChat == nil {
@ -453,7 +410,7 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
} }
members.OtherUserID = signalid.MakeUserID(recipient.ACI) members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI { if recipient.ACI == s.Client.Store.ACI {
namePtr = ptr.Ptr(NoteToSelfName) name = NoteToSelfName
avatar = &bridgev2.Avatar{ avatar = &bridgev2.Avatar{
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar), ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0, Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
@ -474,14 +431,14 @@ func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *type
return &bridgev2.CreateChatResponse{ return &bridgev2.CreateChatResponse{
PortalKey: s.makeDMPortalKey(serviceID), PortalKey: s.makeDMPortalKey(serviceID),
PortalInfo: &bridgev2.ChatInfo{ PortalInfo: &bridgev2.ChatInfo{
Name: namePtr, Name: &name,
Avatar: avatar, Avatar: avatar,
Topic: &topic, Topic: &topic,
Members: members, Members: members,
Type: ptr.Ptr(database.RoomTypeDM), Type: ptr.Ptr(database.RoomTypeDM),
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
CanBackfill: backupChat != nil, CanBackfill: backupChat != nil,
ExtraUpdates: updatePortalSyncMeta, ExtraUpdates: updatePortalSyncMeta,
}, },
} }

View file

@ -32,19 +32,10 @@ import (
"go.mau.fi/mautrix-signal/pkg/signalmeow/types" "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
) )
func (s *SignalClient) stopChatSync() { func (s *SignalClient) syncChats(ctx context.Context) {
if cancel := s.cancelChatSync.Swap(nil); cancel != nil {
(*cancel)()
}
}
func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) {
defer cancel()
if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced { if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced {
return return
} }
if s.Client.Store.EphemeralBackupKey != nil { if s.Client.Store.EphemeralBackupKey != nil {
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats") zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
meta, err := s.Client.WaitForTransfer(ctx) meta, err := s.Client.WaitForTransfer(ctx)
@ -74,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") zerolog.Ctx(ctx).Info().Int("chat_count", len(chats)).Msg("Fetched chats to sync from database")
for _, chat := range chats { 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) recipient, err := s.Client.Store.BackupStore.GetBackupRecipient(ctx, chat.RecipientId)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat") zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat")
continue 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{ resyncEvt := &simplevent.ChatResync{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{

View file

@ -19,7 +19,6 @@ package connector
import ( import (
"context" "context"
"fmt" "fmt"
"sync/atomic"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -27,7 +26,6 @@ import (
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow" "go.mau.fi/mautrix-signal/pkg/signalmeow"
@ -41,13 +39,11 @@ type SignalClient struct {
Ghost *bridgev2.Ghost Ghost *bridgev2.Ghost
queueEmptyWaiter *exsync.Event queueEmptyWaiter *exsync.Event
cancelChatSync atomic.Pointer[context.CancelFunc]
} }
var ( var (
_ bridgev2.NetworkAPI = (*SignalClient)(nil) _ bridgev2.NetworkAPI = (*SignalClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
) )
var pushCfg = &bridgev2.PushConfig{ var pushCfg = &bridgev2.PushConfig{
@ -78,27 +74,18 @@ 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) { func (s *SignalClient) LogoutRemote(ctx context.Context) {
if s.Client == nil { if s.Client == nil {
return return
} }
s.stopChatSync() err := s.Client.StopReceiveLoops()
err := s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
}
err = s.Client.StopReceiveLoops()
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout") zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout")
} }
err = s.Client.Unlink(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
}
err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData) err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store") zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store")
@ -189,7 +176,6 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
} }
case signalmeow.SignalConnectionEventLoggedOut: case signalmeow.SignalConnectionEventLoggedOut:
s.stopChatSync()
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState") s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
if err == nil { if err == nil {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
@ -202,10 +188,6 @@ func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnec
} }
case signalmeow.SignalConnectionEventError: 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()})
case signalmeow.SignalConnectionEventFatalError:
s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState") s.UserLogin.Log.Debug().Msg("Sending UnknownError BridgeState")
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()}) s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
@ -251,7 +233,7 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
case web.SignalWebsocketConnectionEventLoggedOut: case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Authed websocket logged out") log.Err(status.Err).Msg("Authed websocket logged out")
return fmt.Errorf("authed websocket logged out: %w", status.Err) return fmt.Errorf("authed websocket logged out: %w", status.Err)
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Authed websocket error") log.Err(status.Err).Msg("Authed websocket error")
return fmt.Errorf("authed websocket errored: %w", status.Err) return fmt.Errorf("authed websocket errored: %w", status.Err)
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
@ -265,7 +247,7 @@ func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.Connec
log.Err(status.Err).Msg("Unauthed websocket disconnected") log.Err(status.Err).Msg("Unauthed websocket disconnected")
case web.SignalWebsocketConnectionEventLoggedOut: case web.SignalWebsocketConnectionEventLoggedOut:
log.Err(status.Err).Msg("Unauthed websocket logged out") log.Err(status.Err).Msg("Unauthed websocket logged out")
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Unauthed websocket error") log.Err(status.Err).Msg("Unauthed websocket error")
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
log.Info().Msg("Unauthed websocket clean shutdown") log.Info().Msg("Unauthed websocket clean shutdown")
@ -288,7 +270,6 @@ func (s *SignalClient) Disconnect() {
if s.Client == nil { if s.Client == nil {
return return
} }
s.stopChatSync()
err := s.Client.StopReceiveLoops() err := s.Client.StopReceiveLoops()
if err != nil { if err != nil {
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops") s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
@ -296,63 +277,45 @@ func (s *SignalClient) Disconnect() {
} }
func (s *SignalClient) postLoginConnect() { 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) s.tryConnect(ctx, 0, false)
if s.Client.Store.EphemeralBackupKey != nil {
go func() {
s.syncChats(ctx)
if s.Client.Store.MasterKey != nil {
s.Client.SyncStorage(ctx)
}
}()
} else if s.Client.Store.MasterKey != nil {
go s.Client.SyncStorage(ctx)
}
} }
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSync bool) { func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, doSync bool) {
if ctx.Err() != nil { err := s.Client.RegisterCapabilities(ctx)
zerolog.Ctx(ctx).Debug(). if err != nil {
Int("retry_count", retryCount). zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
AnErr("ctx_err", ctx.Err()). } else {
Msg("Context is canceled, not trying to connect") zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
return
}
if retryCount == 0 {
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
} }
ch, err := s.Client.StartReceiveLoops(ctx) ch, err := s.Client.StartReceiveLoops(ctx)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops") zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops")
if retryCount < 6 {
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: "unknown-websocket-error", Message: err.Error()})
retryInSeconds := 2 << retryCount retryInSeconds := 2 << retryCount
if retryInSeconds > 150 {
retryInSeconds = 150
}
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection") zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
select { time.Sleep(time.Duration(retryInSeconds) * time.Second)
case <-time.After(time.Duration(retryInSeconds) * time.Second): s.tryConnect(ctx, retryCount+1, doSync)
case <-ctx.Done(): } else {
zerolog.Ctx(ctx).Info().Msg("Context canceled, exit tryConnect") s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
return
}
s.tryConnect(ctx, retryCount+1, noLoginSync)
return
}
syncCtx, cancel := context.WithCancel(ctx)
if oldCancel := s.cancelChatSync.Swap(&cancel); oldCancel != nil {
(*oldCancel)()
} }
} else {
go s.bridgeStateLoop(ch) go s.bridgeStateLoop(ch)
if noLoginSync { if doSync {
go s.syncChats(syncCtx, cancel) go s.syncChats(ctx)
} 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.SyncStorage(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 {
go s.Client.SyncStorage(ctx)
}
} }
} }
} }

View file

@ -1,73 +0,0 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2025 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"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/networkid"
"go.mau.fi/mautrix-signal/pkg/signalid"
)
var CmdDiscardSenderKey = &commands.FullHandler{
Func: fnDiscardSenderKey,
Name: "discard-sender-key",
Help: commands.HelpMeta{
Section: commands.HelpSectionChats,
Description: "Discard the Signal-side sender key in the current group",
Args: "[_login ID_]",
},
RequiresPortal: true,
RequiresLogin: true,
}
func fnDiscardSenderKey(ce *commands.Event) {
_, groupID, _ := signalid.ParsePortalID(ce.Portal.ID)
if groupID == "" {
ce.Reply("This command can only be used in group chat portals")
return
}
var login *bridgev2.UserLogin
if len(ce.Args) > 0 {
login = ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if login == nil || login.UserMXID != ce.User.MXID {
ce.Reply("Login not found")
return
}
} else {
var err error
login, _, err = ce.Portal.FindPreferredLogin(ce.Ctx, ce.User, false)
if errors.Is(err, bridgev2.ErrNotLoggedIn) {
ce.Reply("You're not logged in in this portal")
return
} else if err != nil {
ce.Log.Err(err).Msg("Failed to find preferred login for portal")
ce.Reply("Failed to find preferred login for portal")
return
}
}
distributionID, err := login.Client.(*SignalClient).Client.ResetSenderKey(ce.Ctx, groupID)
if err != nil {
ce.Log.Err(err).Msg("Failed to reset sender key")
ce.Reply("Failed to reset sender key")
} else {
ce.Reply("Reset sender key with distribution ID %s", distributionID)
}
}

View file

@ -42,7 +42,6 @@ type SignalConfig struct {
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"` NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
LocationFormat string `yaml:"location_format"` LocationFormat string `yaml:"location_format"`
DisappearViewOnce bool `yaml:"disappear_view_once"` DisappearViewOnce bool `yaml:"disappear_view_once"`
ExtEvPolls bool `yaml:"extev_polls"`
displaynameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"`
} }
@ -104,7 +103,6 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "note_to_self_avatar") helper.Copy(up.Str, "note_to_self_avatar")
helper.Copy(up.Str, "location_format") helper.Copy(up.Str, "location_format")
helper.Copy(up.Bool, "disappear_view_once") helper.Copy(up.Bool, "disappear_view_once")
helper.Copy(up.Bool, "extev_polls")
} }
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) { func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {

View file

@ -24,19 +24,15 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/util/exhttp"
"go.mau.fi/util/exsync" "go.mau.fi/util/exsync"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-signal/pkg/msgconv" "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"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store" "go.mau.fi/mautrix-signal/pkg/signalmeow/store"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
type SignalConnector struct { type SignalConnector struct {
@ -67,8 +63,6 @@ func (s *SignalConnector) Init(bridge *bridgev2.Bridge) {
s.MsgConv = msgconv.NewMessageConverter(bridge) s.MsgConv = msgconv.NewMessageConverter(bridge)
s.MsgConv.LocationFormat = s.Config.LocationFormat s.MsgConv.LocationFormat = s.Config.LocationFormat
s.MsgConv.DisappearViewOnce = s.Config.DisappearViewOnce s.MsgConv.DisappearViewOnce = s.Config.DisappearViewOnce
s.MsgConv.ExtEvPolls = s.Config.ExtEvPolls
bridge.Commands.(*commands.Processor).AddHandlers(CmdDiscardSenderKey)
} }
func (s *SignalConnector) SetMaxFileSize(maxSize int64) { func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
@ -76,7 +70,6 @@ func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
} }
func (s *SignalConnector) Start(ctx context.Context) error { func (s *SignalConnector) Start(ctx context.Context) error {
s.ResetHTTPTransport()
err := s.Store.Upgrade(ctx) err := s.Store.Upgrade(ctx)
if err != nil { if err != nil {
return bridgev2.DBUpgradeError{Err: err, Section: "signalmeow"} return bridgev2.DBUpgradeError{Err: err, Section: "signalmeow"}
@ -84,26 +77,6 @@ func (s *SignalConnector) Start(ctx context.Context) error {
return nil return nil
} }
func (s *SignalConnector) ResetHTTPTransport() {
settings := exhttp.SensibleClientSettings
hs, ok := s.Bridge.Matrix.(bridgev2.MatrixConnectorWithHTTPSettings)
if ok {
settings = hs.GetHTTPClientSettings()
}
oldClient := web.SignalHTTPClient
web.SignalHTTPClient = settings.WithTLSConfig(web.SignalTLSConfig).Compile()
oldClient.CloseIdleConnections()
}
func (s *SignalConnector) ResetNetworkConnections() {
for _, login := range s.Bridge.GetAllCachedUserLogins() {
c := login.Client.(*SignalClient)
if c.Client != nil {
c.Client.ForceReconnect()
}
}
}
func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error {
aci, err := uuid.Parse(string(login.ID)) aci, err := uuid.Parse(string(login.ID))
if err != nil { if err != nil {
@ -120,13 +93,13 @@ func (s *SignalConnector) LoadUserLogin(ctx context.Context, login *bridgev2.Use
queueEmptyWaiter: exsync.NewEvent(), queueEmptyWaiter: exsync.NewEvent(),
} }
if device != nil { if device != nil {
sc.Client = signalmeow.NewClient( sc.Client = &signalmeow.Client{
device, Store: device,
sc.UserLogin.Log.With().Str("component", "signalmeow").Logger(), Log: sc.UserLogin.Log.With().Str("component", "signalmeow").Logger(),
sc.handleSignalEvent, EventHandler: sc.handleSignalEvent,
)
sc.Client.SyncContactsOnConnect = s.Config.SyncContactsOnStartup && SyncContactsOnConnect: s.Config.SyncContactsOnStartup,
time.Since(login.Metadata.(*signalid.UserLoginMetadata).LastContactSync.Time) > 3*24*time.Hour }
} }
login.Client = sc login.Client = sc
return nil return nil

View file

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

View file

@ -24,5 +24,3 @@ note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s' location_format: 'https://www.google.com/maps/place/%[1]s,%[2]s'
# Should view-once messages disappear shortly after sending a read receipt on Matrix? # Should view-once messages disappear shortly after sending a read receipt on Matrix?
disappear_view_once: false disappear_view_once: false
# Should polls be sent using unstable MSC3381 event types?
extev_polls: false

View file

@ -98,7 +98,7 @@ func inviteLinkToJoinRule(inviteLinkAccess signalmeow.AccessControl) event.JoinR
} }
func (s *SignalClient) getGroupInfo(ctx context.Context, groupID types.GroupIdentifier, minRevision uint32, backupChat *store.BackupChat) (*bridgev2.ChatInfo, error) { func (s *SignalClient) getGroupInfo(ctx context.Context, groupID types.GroupIdentifier, minRevision uint32, backupChat *store.BackupChat) (*bridgev2.ChatInfo, error) {
groupInfo, _, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision) groupInfo, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve group by id: %w", err) return nil, fmt.Errorf("failed to retrieve group by id: %w", err)
} }
@ -123,30 +123,43 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members) applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink) joinRule = inviteLinkToJoinRule(groupInfo.AccessControl.AddFromInviteLink)
} }
for _, member := range groupInfo.RequestingMembers {
members.MemberMap.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, member := range groupInfo.PendingMembers {
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
MemberSender: s.makeEventSender(member.AddedByUserID),
})
}
for _, member := range groupInfo.Members { for _, member := range groupInfo.Members {
members.MemberMap.Set(bridgev2.ChatMember{ evtSender := s.makeEventSender(member.ACI)
EventSender: s.makeEventSender(member.ACI), members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) }
}
for _, member := range groupInfo.PendingMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
evtSender := s.makeEventSender(*aci)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
}
}
for _, member := range groupInfo.RequestingMembers {
evtSender := s.makeEventSender(member.ACI)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
Membership: event.MembershipKnock,
}
} }
for _, member := range groupInfo.BannedMembers { for _, member := range groupInfo.BannedMembers {
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{ aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
evtSender := s.makeEventSender(*aci)
members.MemberMap[evtSender.Sender] = bridgev2.ChatMember{
EventSender: evtSender,
Membership: event.MembershipBan, Membership: event.MembershipBan,
}) }
} }
if backupChat == nil { if backupChat == nil {
var err error var err error
@ -178,10 +191,6 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
}, nil }, nil
} }
func addMemberToMap(mc map[networkid.UserID]bridgev2.ChatMember, member bridgev2.ChatMember) {
mc[member.EventSender.Sender] = member
}
func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool { func updatePortalSyncMeta(ctx context.Context, portal *bridgev2.Portal) bool {
meta := portal.Metadata.(*signalid.PortalMetadata) meta := portal.Metadata.(*signalid.PortalMetadata)
meta.LastSync = jsontime.UnixNow() meta.LastSync = jsontime.UnixNow()
@ -277,128 +286,132 @@ func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID
JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess), JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess),
} }
} }
mc := make(bridgev2.ChatMemberMap) var mc []bridgev2.ChatMember
for _, member := range groupChange.AddPendingMembers {
s.addChatMemberWithACIQuery(ctx, mc, member.ServiceID, bridgev2.ChatMember{
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
PrevMembership: event.MembershipLeave,
MemberSender: s.makeEventSender(member.AddedByUserID),
})
}
for _, member := range groupChange.AddRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, memberServiceID := range groupChange.DeletePendingMembers {
s.addChatMemberWithACIQuery(ctx, mc, *memberServiceID, bridgev2.ChatMember{
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
})
}
for _, memberACI := range groupChange.DeleteRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipKnock,
})
}
for _, memberACI := range groupChange.DeleteMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
})
}
for _, memberServiceID := range groupChange.DeleteBannedMembers {
s.addChatMemberWithACIQuery(ctx, mc, *memberServiceID, bridgev2.ChatMember{
Membership: event.MembershipLeave,
PrevMembership: event.MembershipBan,
})
}
for _, member := range groupChange.AddBannedMembers {
s.addChatMemberWithACIQuery(ctx, mc, member.ServiceID, bridgev2.ChatMember{
Membership: event.MembershipBan,
})
}
for _, member := range groupChange.PromotePendingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromotePendingPniAciMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
})
mc.Set(bridgev2.ChatMember{
EventSender: s.makePNIEventSender(member.PNI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true,
},
})
}
for _, member := range groupChange.PromoteRequestingMembers {
mc.Set(bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipKnock,
})
}
for _, member := range groupChange.AddMembers { for _, member := range groupChange.AddMembers {
mc.Set(bridgev2.ChatMember{ mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) })
} }
for _, member := range groupChange.ModifyMemberRoles { for _, member := range groupChange.ModifyMemberRoles {
mc.Set(bridgev2.ChatMember{ mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI), EventSender: s.makeEventSender(member.ACI),
PowerLevel: roleToPL(member.Role), PowerLevel: roleToPL(member.Role),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
}) })
} }
bannedMembers := make(map[libsignalgo.ServiceID]bool)
for _, member := range groupChange.AddBannedMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
bannedMembers[member.ServiceID] = true
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipBan,
})
}
for _, memberACI := range groupChange.DeleteMembers {
if bannedMembers[libsignalgo.NewACIServiceID(*memberACI)] {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
})
}
for _, member := range groupChange.AddPendingMembers {
aci := s.maybeResolvePNItoACI(ctx, &member.ServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
PowerLevel: roleToPL(member.Role),
Membership: event.MembershipInvite,
})
}
for _, memberServiceID := range groupChange.DeletePendingMembers {
if bannedMembers[*memberServiceID] {
continue
}
aci := s.maybeResolvePNItoACI(ctx, memberServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.AddRequestingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipKnock,
})
}
for _, memberACI := range groupChange.DeleteRequestingMembers {
if bannedMembers[libsignalgo.NewACIServiceID(*memberACI)] {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*memberACI),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipKnock,
})
}
for _, memberServiceID := range groupChange.DeleteBannedMembers {
aci := s.maybeResolvePNItoACI(ctx, memberServiceID)
if aci == nil {
continue
}
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(*aci),
Membership: event.MembershipLeave,
PrevMembership: event.MembershipBan,
})
}
for _, member := range groupChange.PromotePendingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromotePendingPniAciMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipInvite,
})
}
for _, member := range groupChange.PromoteRequestingMembers {
mc = append(mc, bridgev2.ChatMember{
EventSender: s.makeEventSender(member.ACI),
Membership: event.MembershipJoin,
PrevMembership: event.MembershipKnock,
})
}
if len(mc) > 0 || pls != nil { if len(mc) > 0 || pls != nil {
ic.MemberChanges = &bridgev2.ChatMemberList{MemberMap: mc, PowerLevels: pls} ic.MemberChanges = &bridgev2.ChatMemberList{Members: mc, PowerLevels: pls}
} }
return ic, nil return ic, nil
} }
func (s *SignalClient) addChatMemberWithACIQuery( func (s *SignalClient) maybeResolvePNItoACI(ctx context.Context, serviceID *libsignalgo.ServiceID) *uuid.UUID {
ctx context.Context, mc bridgev2.ChatMemberMap, serviceID libsignalgo.ServiceID, member bridgev2.ChatMember, if serviceID.Type == libsignalgo.ServiceIDTypeACI {
) { return &serviceID.UUID
member.EventSender = s.makeEventSenderFromServiceID(serviceID)
mc.Set(member)
if aci := s.tryResolvePNItoLoggedInACI(ctx, serviceID); aci != nil {
member.EventSender = s.makeEventSender(*aci)
mc.Add(member)
} }
device, err := s.Client.Store.DeviceStore.DeviceByPNI(ctx, serviceID.UUID)
if err != nil || device == nil {
return nil
} }
func (s *SignalClient) tryResolvePNItoLoggedInACI(ctx context.Context, serviceID libsignalgo.ServiceID) *uuid.UUID {
if serviceID.Type != libsignalgo.ServiceIDTypePNI {
return nil
} else if serviceID.UUID == s.Client.Store.PNI {
return &s.Client.Store.ACI
} else if s.Main.Bridge.Config.SplitPortals {
// When split portals is enabled, we don't care about anyone else's logins
return nil
} else if device, err := s.Client.Store.DeviceStore.DeviceByPNI(ctx, serviceID.UUID); err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ACI for PNI")
return nil
} else if device == nil {
return nil
} else {
return &device.ACI return &device.ACI
} }
}
func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) { func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) {
if fromRevision >= toRevision { if fromRevision >= toRevision {
@ -430,8 +443,8 @@ func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal
chatInfoChange, err := s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(portal.ID), gc.GroupChange.Revision, gc.GroupChange) chatInfoChange, err := s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(portal.ID), gc.GroupChange.Revision, gc.GroupChange)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to convert group info") log.Err(err).Msg("Failed to convert group info")
} else { } else if gc.GroupChange.SourceServiceID.Type == libsignalgo.ServiceIDTypeACI {
portal.ProcessChatInfoChange(ctx, s.makeEventSenderFromServiceID(gc.GroupChange.SourceServiceID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts))) portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceServiceID.UUID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
} }
if gc.GroupChange.Revision == toRevision { if gc.GroupChange.Revision == toRevision {
break break

View file

@ -21,7 +21,6 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"slices"
"strconv" "strconv"
"time" "time"
@ -53,9 +52,6 @@ var (
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil) _ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.DeleteChatHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.PollHandlingNetworkAPI = (*SignalClient)(nil)
_ bridgev2.MessageRequestAcceptingNetworkAPI = (*SignalClient)(nil)
) )
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error { func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
@ -74,17 +70,17 @@ func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.Porta
Int("failed_to_send_to_count", len(result.FailedToSendTo)). Int("failed_to_send_to_count", len(result.FailedToSendTo)).
Int("successfully_sent_to_count", len(result.SuccessfullySentTo)). Int("successfully_sent_to_count", len(result.SuccessfullySentTo)).
Logger() Logger()
if len(result.FailedToSendTo) > 0 {
log.Error().Msg("Failed to send event to some members of Signal group")
}
if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 { if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 {
log.Debug().Msg("No successes or failures - Probably sent to myself") log.Debug().Msg("No successes or failures - Probably sent to myself")
} else if len(result.SuccessfullySentTo) == 0 { } else if len(result.SuccessfullySentTo) == 0 {
log.Error().Msg("Failed to send event to all members of Signal group") log.Error().Msg("Failed to send event to all members of Signal group")
return errors.New("failed to send to any members of Signal group") return errors.New("failed to send to any members of Signal group")
} else if len(result.SuccessfullySentTo) < totalRecipients { } else if len(result.SuccessfullySentTo) < totalRecipients {
if len(result.FailedToSendTo) > 0 {
log.Warn().Msg("Failed to send event to some members of Signal group")
} else {
log.Warn().Msg("Only sent event to some members of Signal group") log.Warn().Msg("Only sent event to some members of Signal group")
}
} else { } else {
log.Debug().Msg("Sent event to all members of Signal group") log.Debug().Msg("Sent event to all members of Signal group")
} }
@ -113,31 +109,16 @@ func getTimestampForEvent(txnID networkid.RawTransactionID, evt *event.Event, or
} }
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) { func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted, err := s.Main.MsgConv.ToSignal( converted, err := s.Main.MsgConv.ToSignal(
ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo, ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, msg.ReplyTo,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
})
}
func (s *SignalClient) doSendMessage(
ctx context.Context,
msg *bridgev2.MatrixMessage,
converted *signalpb.DataMessage,
meta *signalid.MessageMetadata,
) (*bridgev2.MatrixMessageResponse, error) {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts
if meta == nil {
meta = &signalid.MessageMetadata{}
}
msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts) msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.AddPendingToIgnore(networkid.TransactionID(msgID)) msg.AddPendingToIgnore(networkid.TransactionID(msgID))
err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted)) err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{DataMessage: converted})
if err != nil { if err != nil {
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
} }
@ -145,7 +126,9 @@ func (s *SignalClient) doSendMessage(
ID: msgID, ID: msgID,
SenderID: signalid.MakeUserID(s.Client.Store.ACI), SenderID: signalid.MakeUserID(s.Client.Store.ACI),
Timestamp: time.UnixMilli(int64(ts)), Timestamp: time.UnixMilli(int64(ts)),
Metadata: meta, Metadata: &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
},
} }
return &bridgev2.MatrixMessageResponse{ return &bridgev2.MatrixMessageResponse{
DB: dbMsg, DB: dbMsg,
@ -167,16 +150,15 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
return fmt.Errorf("failed to get message reply target: %w", err) return fmt.Errorf("failed to get message reply target: %w", err)
} }
} }
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, ts, msg.OrigSender != nil, replyTo)
if err != nil { if err != nil {
return err return err
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{EditMessage: &signalpb.EditMessage{
converted.Timestamp = &ts
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapEditMessage(&signalpb.EditMessage{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
DataMessage: converted, DataMessage: converted,
})) }})
if err != nil { if err != nil {
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true) return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
} }
@ -200,16 +182,19 @@ func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.M
return nil, fmt.Errorf("failed to parse target message ID: %w", err) return nil, fmt.Errorf("failed to parse target message ID: %w", err)
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts), Timestamp: proto.Uint64(ts),
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Reaction: &signalpb.DataMessage_Reaction{ Reaction: &signalpb.DataMessage_Reaction{
Emoji: proto.String(msg.PreHandleResp.Emoji), Emoji: proto.String(msg.PreHandleResp.Emoji),
Remove: proto.Bool(false), Remove: proto.Bool(false),
TargetAuthorAciBinary: targetAuthorACI[:], TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}, },
})) },
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -222,16 +207,19 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
return fmt.Errorf("failed to parse target message ID: %w", err) return fmt.Errorf("failed to parse target message ID: %w", err)
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts), Timestamp: proto.Uint64(ts),
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
Reaction: &signalpb.DataMessage_Reaction{ Reaction: &signalpb.DataMessage_Reaction{
Emoji: proto.String(msg.TargetReaction.Emoji), Emoji: proto.String(msg.TargetReaction.Emoji),
Remove: proto.Bool(true), Remove: proto.Bool(true),
TargetAuthorAciBinary: targetAuthorACI[:], TargetAuthorAci: proto.String(targetAuthorACI.String()),
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}, },
})) },
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return err return err
} }
@ -246,12 +234,15 @@ func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridg
return fmt.Errorf("cannot delete other people's messages") return fmt.Errorf("cannot delete other people's messages")
} }
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ wrappedContent := &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: proto.Uint64(ts), Timestamp: proto.Uint64(ts),
Delete: &signalpb.DataMessage_Delete{ Delete: &signalpb.DataMessage_Delete{
TargetSentTimestamp: proto.Uint64(targetSentTimestamp), TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
}, },
})) },
}
err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent)
if err != nil { if err != nil {
return err return err
} }
@ -311,21 +302,18 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
} }
func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error { func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error {
userID, groupID, err := signalid.ParsePortalID(typing.Portal.ID) userID, _, err := signalid.ParsePortalID(typing.Portal.ID)
if err != nil { if err != nil {
return err return err
} }
typingMessage := signalmeow.TypingMessage(typing.IsTyping) // Only send typing notifications in DMs for now
// Sending efficiently to groups requires implementing the proper SenderKey stuff first
if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI { if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI {
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
result := s.Client.SendMessage(ctx, userID, typingMessage) result := s.Client.SendMessage(ctx, userID, typingMessage)
if !result.WasSuccessful { if !result.WasSuccessful {
return result.Error return result.Error
} }
} else if groupID != "" {
_, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage)
if err != nil {
return err
}
} }
return nil return nil
} }
@ -378,7 +366,7 @@ func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2
return false, fmt.Errorf("failed to download avatar: %w", err) return false, fmt.Errorf("failed to download avatar: %w", err)
} }
avatarHash = sha256.Sum256(data) avatarHash = sha256.Sum256(data)
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "") avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to reupload avatar: %w", err) return false, fmt.Errorf("failed to reupload avatar: %w", err)
} }
@ -397,24 +385,22 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.
}, nil) }, nil)
} }
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
var targetIntent bridgev2.MatrixAPI var targetIntent bridgev2.MatrixAPI
var targetSignalID libsignalgo.ServiceID var targetSignalID uuid.UUID
var err error var err error
if msg.Portal.RoomType == database.RoomTypeDM { if msg.Portal.RoomType == database.RoomTypeDM {
//TODO: this probably needs to revert some changes and clean up the portal on leaves
switch msg.Type { switch msg.Type {
case bridgev2.Invite: case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm") return false, fmt.Errorf("cannot invite additional user to dm")
default: default:
return nil, nil return false, nil
} }
} }
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target) targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse target signal id: %w", err) return false, fmt.Errorf("failed to parse target signal id: %w", err)
} }
switch target := msg.Target.(type) { switch target := msg.Target.(type) {
case *bridgev2.Ghost: case *bridgev2.Ghost:
@ -424,12 +410,12 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
if targetIntent == nil { if targetIntent == nil {
ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID)) ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ghost for user: %w", err) return false, fmt.Errorf("failed to get ghost for user: %w", err)
} }
targetIntent = ghost.Intent targetIntent = ghost.Intent
} }
default: default:
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target) return false, fmt.Errorf("cannot get target intent: unknown type: %T", target)
} }
log := zerolog.Ctx(ctx).With(). log := zerolog.Ctx(ctx).With().
Str("From Membership", string(msg.Type.From)). Str("From Membership", string(msg.Type.From)).
@ -448,35 +434,21 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
} }
switch msg.Type { switch msg.Type {
case bridgev2.AcceptInvite: case bridgev2.AcceptInvite:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept invite for non-ACI service ID")
}
gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{ gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{
ACI: targetSignalID.UUID, ACI: targetSignalID,
}} }}
case bridgev2.RevokeInvite, bridgev2.RejectInvite: case bridgev2.RevokeInvite, bridgev2.RejectInvite:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember}
case bridgev2.Leave, bridgev2.Kick: case bridgev2.Leave, bridgev2.Kick:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't kick non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.Invite: case bridgev2.Invite:
if targetSignalID.Type == libsignalgo.ServiceIDTypeACI {
gc.AddMembers = []*signalmeow.AddMember{{ gc.AddMembers = []*signalmeow.AddMember{{
GroupMember: signalmeow.GroupMember{ GroupMember: signalmeow.GroupMember{
ACI: targetSignalID.UUID, ACI: targetSignalID,
Role: role, Role: role,
}, },
}} }}
} else {
gc.AddPendingMembers = []*signalmeow.PendingMember{{
ServiceID: targetSignalID,
Role: role,
AddedByUserID: s.Client.Store.ACI,
Timestamp: uint64(msg.Event.Timestamp),
}}
}
// TODO: joining and knocking requires a way to obtain the invite link // TODO: joining and knocking requires a way to obtain the invite link
// because the joining/knocking member doesn't have the GroupMasterKey yet // because the joining/knocking member doesn't have the GroupMasterKey yet
// case bridgev2.Join: // case bridgev2.Join:
@ -493,59 +465,50 @@ func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2
// Timestamp: uint64(time.Now().UnixMilli()), // Timestamp: uint64(time.Now().UnixMilli()),
// }} // }}
case bridgev2.AcceptKnock: case bridgev2.AcceptKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("can't accept knock from non-ACI service ID")
}
gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{ gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{
ACI: targetSignalID.UUID, ACI: targetSignalID,
Role: role, Role: role,
}} }}
case bridgev2.RetractKnock, bridgev2.RejectKnock: case bridgev2.RetractKnock, bridgev2.RejectKnock:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't reject knock from non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft: case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft:
gc.AddBannedMembers = []*signalmeow.BannedMember{{ gc.AddBannedMembers = []*signalmeow.BannedMember{{
ServiceID: targetSignalID, ServiceID: libsignalgo.NewACIServiceID(targetSignalID),
Timestamp: uint64(time.Now().UnixMilli()), Timestamp: uint64(time.Now().UnixMilli()),
}} }}
switch msg.Type { switch msg.Type {
case bridgev2.BanJoined: case bridgev2.BanJoined:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
}
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
case bridgev2.BanInvited: case bridgev2.BanInvited:
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember}
case bridgev2.BanKnocked: case bridgev2.BanKnocked:
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID}
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
}
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
} }
case bridgev2.Unban: case bridgev2.Unban:
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID} unbanUser := libsignalgo.NewACIServiceID(targetSignalID)
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&unbanUser}
default: default:
return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To) log.Debug().Msg("unsupported membership change")
return false, nil
} }
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID) _, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil || groupID == "" { if err != nil || groupID == "" {
return nil, err return false, err
} }
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1 gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID) revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil { if err != nil {
return nil, err return false, err
} }
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI { if msg.Type == bridgev2.Invite {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
if err != nil { if err != nil {
return nil, err return false, err
} }
} }
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return nil, nil return true, nil
} }
func plToRole(pl int) signalmeow.GroupMemberRole { func plToRole(pl int) signalmeow.GroupMemberRole {
@ -577,17 +540,18 @@ func (s *SignalClient) HandleMatrixPowerLevels(ctx context.Context, msg *bridgev
if msg.Portal.RoomType == database.RoomTypeDM { if msg.Portal.RoomType == database.RoomTypeDM {
return false, nil return false, nil
} }
log := zerolog.Ctx(ctx)
gc := &signalmeow.GroupChange{} gc := &signalmeow.GroupChange{}
for _, plc := range msg.Users { for _, plc := range msg.Users {
if !hasAdminChanged(&plc.SinglePowerLevelChange) { if !hasAdminChanged(&plc.SinglePowerLevelChange) {
continue continue
} }
serviceID, err := signalid.ParseGhostOrUserLoginID(plc.Target) aci, err := signalid.ParseGhostOrUserLoginID(plc.Target)
if err != nil || serviceID.Type != libsignalgo.ServiceIDTypeACI { if err != nil {
continue log.Err(err).Msg("Couldn't parse user id")
} }
gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{ gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{
ACI: serviceID.UUID, ACI: aci,
Role: plToRole(plc.NewLevel), Role: plToRole(plc.NewLevel),
}) })
} }
@ -679,11 +643,13 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
}) })
} else { } else {
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
res := s.Client.SendMessage(ctx, userID, signalmeow.WrapDataMessage(&signalpb.DataMessage{ res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
DataMessage: &signalpb.DataMessage{
Timestamp: ptr.Ptr(ts), Timestamp: ptr.Ptr(ts),
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)), Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
})) },
})
if !res.WasSuccessful { if !res.WasSuccessful {
return false, res.Error return false, res.Error
} }
@ -691,222 +657,3 @@ func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *b
return true, nil return true, nil
} }
} }
func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
if msg.Content.FromMessageRequest {
// 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)
}
}
// Build ConversationIdentifier based on portal type
var conversationID *signalpb.ConversationIdentifier
if groupID == "" {
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadServiceIdBinary{
ThreadServiceIdBinary: userID.Bytes(),
},
}
} else {
gid, err := groupID.Bytes()
if err != nil {
return fmt.Errorf("failed to parse group ID: %w", err)
}
conversationID = &signalpb.ConversationIdentifier{
Identifier: &signalpb.ConversationIdentifier_ThreadGroupId{
ThreadGroupId: gid[:],
},
}
}
// Retrieve most recent messages from the portal
var mostRecentMessages []*signalpb.AddressableMessage
dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(
ctx,
msg.Portal.PortalKey,
time.Now().Add(-30*24*time.Hour), // Last 30 days
time.Now(),
)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get recent messages for conversation delete")
} else if len(dbMessages) > 0 {
// Limit to the 5 most recent messages overall
limit := 5
startIdx := 0
if len(dbMessages) > limit {
startIdx = len(dbMessages) - limit
}
// Create AddressableMessage for most recent messages
for _, dbMsg := range dbMessages[startIdx:] {
senderACI, timestamp, err := signalid.ParseMessageID(dbMsg.ID)
if err != nil {
continue
}
mostRecentMessages = append(mostRecentMessages, &signalpb.AddressableMessage{
Author: &signalpb.AddressableMessage_AuthorServiceIdBinary{
AuthorServiceIdBinary: senderACI[:],
},
SentTimestamp: proto.Uint64(timestamp),
})
}
}
recipientID := s.Client.Store.ACIServiceID()
// Send DeleteForMe sync message to self
result := s.Client.SendMessage(ctx, recipientID, signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
Content: &signalpb.SyncMessage_DeleteForMe_{
DeleteForMe: &signalpb.SyncMessage_DeleteForMe{
ConversationDeletes: []*signalpb.SyncMessage_DeleteForMe_ConversationDelete{{
Conversation: conversationID,
MostRecentMessages: mostRecentMessages,
IsFullDelete: proto.Bool(true),
}},
},
},
}))
zerolog.Ctx(ctx).Debug().
Str("portal_id", string(msg.Portal.ID)).
Int("recent_messages_count", len(mostRecentMessages)).
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 nil
}
func (s *SignalClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
optionNames := make([]string, len(msg.Content.PollStart.Answers))
optionIDs := make([]string, len(msg.Content.PollStart.Answers))
for i, option := range msg.Content.PollStart.Answers {
optionNames[i] = option.Text
optionIDs[i] = option.ID
}
converted := &signalpb.DataMessage{
PollCreate: &signalpb.DataMessage_PollCreate{
Question: ptr.Ptr(msg.Content.PollStart.Question.Text),
AllowMultiple: ptr.Ptr(msg.Content.PollStart.MaxSelections != 1),
Options: optionNames,
},
RequiredProtocolVersion: ptr.Ptr(uint32(signalpb.DataMessage_POLLS)),
}
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{
MatrixPollOptionIDs: optionIDs,
})
}
func (s *SignalClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
senderACI, msgTS, err := signalid.ParseMessageID(msg.VoteTo.ID)
if err != nil {
return nil, err
}
mxOptions := msg.VoteTo.Metadata.(*signalid.MessageMetadata).MatrixPollOptionIDs
optionIndexes := make([]uint32, len(msg.Content.Response.Answers))
for i, answer := range msg.Content.Response.Answers {
if idx := slices.Index(mxOptions, answer); idx >= 0 {
optionIndexes[i] = uint32(idx)
} else if idx, err = strconv.Atoi(answer); err == nil && idx >= 0 {
optionIndexes[i] = uint32(idx)
} else {
return nil, fmt.Errorf("unknown poll answer ID: %s", answer)
}
}
converted := &signalpb.DataMessage{
PollVote: &signalpb.DataMessage_PollVote{
TargetAuthorAciBinary: senderACI[:],
TargetSentTimestamp: &msgTS,
OptionIndexes: optionIndexes,
VoteCount: proto.Uint32(1), // TODO
},
RequiredProtocolVersion: proto.Uint32(0),
}
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, nil)
}
func (s *SignalClient) syncMessageRequestResponse(
ctx context.Context,
portal *bridgev2.Portal,
respType signalpb.SyncMessage_MessageRequestResponse_Type,
) error {
userID, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil {
return err
}
accept := &signalpb.SyncMessage_MessageRequestResponse{
Type: respType.Enum(),
}
if groupID != "" {
gidBytes, err := groupID.Bytes()
if err != nil {
return fmt.Errorf("failed to parse group ID: %w", err)
}
accept.GroupId = gidBytes[:]
} else if userID.Type == libsignalgo.ServiceIDTypeACI {
accept.ThreadAciBinary = userID.UUID[:]
} else {
return fmt.Errorf("invalid portal ID for message request response: %s", portal.ID)
}
res := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(s.Client.Store.ACI), signalmeow.WrapSyncMessage(&signalpb.SyncMessage{
Content: &signalpb.SyncMessage_MessageRequestResponse_{
MessageRequestResponse: accept,
},
}))
if !res.WasSuccessful {
return res.Error
}
return nil
}
func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg *bridgev2.MatrixAcceptMessageRequest) error {
userID, _, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT)
if err != nil {
return 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)
if err != nil {
return fmt.Errorf("failed to get own profile key: %w", err)
}
var pniSig *signalpb.PniSignatureMessage
if s.Client.Store.AccountRecord.GetPhoneNumberSharingMode() == signalpb.AccountRecord_EVERYBODY {
sig, err := s.Client.Store.PNIIdentityKeyPair.SignAlternateIdentity(s.Client.Store.ACIIdentityKeyPair.GetIdentityKey())
if err != nil {
return fmt.Errorf("failed to generate PNI signature: %w", err)
}
pniSig = &signalpb.PniSignatureMessage{
Pni: s.Client.Store.PNI[:],
Signature: sig,
}
}
res := s.Client.SendMessage(ctx, userID, &signalpb.Content{
Content: &signalpb.Content_DataMessage{DataMessage: &signalpb.DataMessage{
Flags: proto.Uint32(uint32(signalpb.DataMessage_PROFILE_KEY_UPDATE)),
ProfileKey: profileKey.Slice(),
Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)),
RequiredProtocolVersion: proto.Uint32(0),
}},
PniSignatureMessage: pniSig,
})
if !res.WasSuccessful {
return fmt.Errorf("failed to share profile key to accept message request: %w", res.Error)
}
// TODO send read receipts too?
}
return nil
}

View file

@ -26,8 +26,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exzerolog" "go.mau.fi/util/exzerolog"
"go.mau.fi/util/jsontime"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
@ -37,7 +35,6 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events" "go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types" "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
@ -55,8 +52,6 @@ func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
return s.handleSignalReadSelf(evt) return s.handleSignalReadSelf(evt)
case *events.DeleteForMe: case *events.DeleteForMe:
return s.handleSignalDeleteForMe(evt) return s.handleSignalDeleteForMe(evt)
case *events.MessageRequestResponse:
return s.handleSignalMessageRequestResponse(evt)
case *events.Call: case *events.Call:
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
case *events.ContactList: case *events.ContactList:
@ -103,9 +98,6 @@ func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridg
if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() { if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
content.MsgType = event.MsgText content.MsgType = event.MsgText
} }
content.BeeperActionMessage = &event.BeeperActionMessage{
Type: event.BeeperActionMessageCall,
}
} else { } else {
content.Body = "Call ended" content.Body = "Call ended"
} }
@ -175,7 +167,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
case *signalpb.DataMessage: case *signalpb.DataMessage:
switch { switch {
case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil, case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil,
innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil, innerEvt.Payment != nil, innerEvt.GiftBadge != nil,
innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT), innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0: innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
return bridgev2.RemoteEventMessage return bridgev2.RemoteEventMessage
@ -184,7 +176,7 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
return bridgev2.RemoteEventReactionRemove return bridgev2.RemoteEventReactionRemove
} }
return bridgev2.RemoteEventReaction return bridgev2.RemoteEventReaction
case innerEvt.Delete != nil, innerEvt.AdminDelete != nil: case innerEvt.Delete != nil:
return bridgev2.RemoteEventMessageRemove return bridgev2.RemoteEventMessageRemove
case innerEvt.GetGroupV2().GetGroupChange() != nil: case innerEvt.GetGroupV2().GetGroupChange() != nil:
return bridgev2.RemoteEventChatInfoChange return bridgev2.RemoteEventChatInfoChange
@ -293,21 +285,16 @@ func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
} }
func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID { func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
var targetAuthorACI uuid.UUID var targetAuthorACI string
var targetSentTS uint64 var targetSentTS uint64
switch innerEvt := evt.Event.(type) { switch innerEvt := evt.Event.(type) {
case *signalpb.DataMessage: case *signalpb.DataMessage:
switch { switch {
case innerEvt.Reaction != nil: case innerEvt.Reaction != nil:
targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary()) targetAuthorACI = innerEvt.Reaction.GetTargetAuthorAci()
targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp() targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
case innerEvt.Delete != nil: case innerEvt.Delete != nil:
targetSentTS = innerEvt.Delete.GetTargetSentTimestamp() targetSentTS = innerEvt.Delete.GetTargetSentTimestamp()
case innerEvt.AdminDelete != nil:
if len(innerEvt.AdminDelete.GetTargetAuthorAciBinary()) == 16 {
targetAuthorACI = uuid.UUID(innerEvt.AdminDelete.GetTargetAuthorAciBinary())
}
targetSentTS = innerEvt.AdminDelete.GetTargetSentTimestamp()
default: default:
return "" return ""
} }
@ -316,10 +303,11 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
default: default:
return "" return ""
} }
if targetAuthorACI == uuid.Nil { targetAuthorUUID := evt.Info.Sender
targetAuthorACI = evt.Info.Sender if targetAuthorACI != "" {
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
} }
return signalid.MakeMessageID(targetAuthorACI, targetSentTS) return signalid.MakeMessageID(targetAuthorUUID, targetSentTS)
} }
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) { func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
@ -339,7 +327,7 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
if !ok { if !ok {
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event") return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
} }
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, dataMsg, nil) converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, dataMsg, nil)
if converted.Disappear.Type != "" { if converted.Disappear.Type != "" {
evtTS := evt.GetTimestamp() evtTS := evt.GetTimestamp()
if !dataMsg.GetIsViewOnce() { if !dataMsg.GetIsViewOnce() {
@ -364,7 +352,7 @@ func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Porta
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event") return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
} }
// TODO tell converter about existing parts to avoid reupload? // TODO tell converter about existing parts to avoid reupload?
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, editMsg.GetDataMessage(), nil) converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, editMsg.GetDataMessage(), nil)
// TODO can anything other than the text be edited? // TODO can anything other than the text be edited?
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1]) editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
editPart.Part.EditCount++ editPart.Part.EditCount++
@ -426,7 +414,7 @@ func (b *Bv2Receipt) GetReadUpTo() time.Time {
return time.Time{} return time.Time{}
} }
var _ bridgev2.RemoteReadReceipt = (*Bv2Receipt)(nil) var _ bridgev2.RemoteReceipt = (*Bv2Receipt)(nil)
func convertReceipts[T any](ctx context.Context, input []T, getMessageFunc func(ctx context.Context, msgID T) (*database.Message, error)) map[networkid.PortalKey]*Bv2Receipt { func convertReceipts[T any](ctx context.Context, input []T, getMessageFunc func(ctx context.Context, msgID T) (*database.Message, error)) map[networkid.PortalKey]*Bv2Receipt {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
@ -472,7 +460,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
Stringer("sender_id", evt.Sender). Stringer("sender_id", evt.Sender).
Stringer("receipt_type", evt.Content.GetType()). Stringer("receipt_type", evt.Content.GetType()).
Logger() Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx) ctx := log.WithContext(context.TODO())
receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) { receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS)) return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS))
}) })
@ -483,9 +471,9 @@ func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) bool {
log := s.UserLogin.Log.With(). log := s.UserLogin.Log.With().
Str("action", "handle signal read self"). Str("action", "handle signal read self").
Logger() Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx) ctx := log.WithContext(context.TODO())
receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) { receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) {
aciUUID, err := signalmeow.ParseStringOrBinaryUUID(msgInfo.GetSenderAci(), msgInfo.GetSenderAciBinary()) aciUUID, err := uuid.Parse(msgInfo.GetSenderAci())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -504,13 +492,6 @@ func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signa
return networkid.PortalKey{}, false return networkid.PortalKey{}, false
} }
return s.makeDMPortalKey(serviceID), true return s.makeDMPortalKey(serviceID), true
case *signalpb.ConversationIdentifier_ThreadServiceIdBinary:
serviceID, err := libsignalgo.ServiceIDFromBytes(ident.ThreadServiceIdBinary)
if err != nil {
log.Err(err).Hex("chat_id", ident.ThreadServiceIdBinary).Msg("Failed to parse delete for me conversation ID")
return networkid.PortalKey{}, false
}
return s.makeDMPortalKey(serviceID), true
case *signalpb.ConversationIdentifier_ThreadGroupId: case *signalpb.ConversationIdentifier_ThreadGroupId:
if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength { if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
log.Error(). log.Error().
@ -549,22 +530,6 @@ func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey net
return "" return ""
} }
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp()) return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
case *signalpb.AddressableMessage_AuthorServiceIdBinary:
serviceID, err := libsignalgo.ServiceIDFromBytes(typedAuthor.AuthorServiceIdBinary)
if err != nil {
log.Err(err).
Object("portal_key", portalKey).
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
Msg("Failed to parse delete for me message author service ID")
return ""
} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().
Object("portal_key", portalKey).
Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
Msg("Dropping delete for me message with unsupported service ID type")
return ""
}
return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
case *signalpb.AddressableMessage_AuthorE164: case *signalpb.AddressableMessage_AuthorE164:
log.Warn(). log.Warn().
Object("portal_key", portalKey). Object("portal_key", portalKey).
@ -655,45 +620,13 @@ func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
return true return true
} }
func (s *SignalClient) handleSignalMessageRequestResponse(evt *events.MessageRequestResponse) bool {
if evt.Type != signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
// TODO do we need to do anything with blocks/deletes here or are they sent as normal delete events?
return true
}
var portalKey networkid.PortalKey
if evt.GroupID != nil {
portalKey = s.makePortalKey(evt.GroupID.String())
} else if evt.ThreadACI != uuid.Nil {
portalKey = s.makeDMPortalKey(libsignalgo.NewACIServiceID(evt.ThreadACI))
} else {
return true
}
res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
PortalKey: portalKey,
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
StreamOrder: int64(evt.Timestamp),
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("action", "unmark message request").Str("source", "sync message")
},
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &bridgev2.ChatInfo{
MessageRequest: ptr.Ptr(false),
},
},
})
return res.Success
}
func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) { func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
log := s.UserLogin.Log.With(). log := s.UserLogin.Log.With().
Str("action", "handle aci found"). Str("action", "handle aci found").
Stringer("aci", evt.ACI). Stringer("aci", evt.ACI).
Stringer("pni", evt.PNI). Stringer("pni", evt.PNI).
Logger() Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx) ctx := log.WithContext(context.TODO())
pniPortalKey := s.makeDMPortalKey(evt.PNI) pniPortalKey := s.makeDMPortalKey(evt.PNI)
aciPortalKey := s.makeDMPortalKey(evt.ACI) aciPortalKey := s.makeDMPortalKey(evt.ACI)
result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey) result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey)
@ -713,7 +646,7 @@ func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
func (s *SignalClient) handleSignalContactList(evt *events.ContactList) { func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger() log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger()
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx) ctx := log.WithContext(context.TODO())
for _, contact := range evt.Contacts { for _, contact := range evt.Contacts {
if contact.ACI == uuid.Nil { if contact.ACI == uuid.Nil {
continue continue
@ -741,33 +674,6 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
if contact.ACI == s.Client.Store.ACI { if contact.ACI == s.Client.Store.ACI {
s.updateRemoteProfile(ctx, true) s.updateRemoteProfile(ctx, true)
} }
if ptr.Val(contact.Whitelisted) {
portal, err := s.Main.Bridge.GetExistingPortalByKey(ctx, s.makeDMPortalKey(libsignalgo.NewACIServiceID(contact.ACI)))
if err != nil {
log.Err(err).Msg("Failed to get existing portal to update contact info")
continue
} else if portal != nil && portal.MessageRequest {
s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatInfoChange,
LogContext: func(c zerolog.Context) zerolog.Context {
return c.Str("action", "unmark message request").Str("source", "contact list")
},
PortalKey: portal.PortalKey,
},
ChatInfoChange: &bridgev2.ChatInfoChange{
ChatInfo: &bridgev2.ChatInfo{
MessageRequest: ptr.Ptr(false),
},
},
})
}
}
}
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).LastContactSync = jsontime.UnixMilliNow()
err := s.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to update last contact sync time")
} }
} }

View file

@ -17,8 +17,6 @@
package connector package connector
import ( import (
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
@ -50,20 +48,3 @@ func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
Sender: signalid.MakeUserID(sender), Sender: signalid.MakeUserID(sender),
} }
} }
func (s *SignalClient) makePNIEventSender(sender uuid.UUID) bridgev2.EventSender {
return bridgev2.EventSender{
Sender: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(sender)),
}
}
func (s *SignalClient) makeEventSenderFromServiceID(serviceID libsignalgo.ServiceID) bridgev2.EventSender {
switch serviceID.Type {
case libsignalgo.ServiceIDTypeACI:
return s.makeEventSender(serviceID.UUID)
case libsignalgo.ServiceIDTypePNI:
return s.makePNIEventSender(serviceID.UUID)
default:
panic(fmt.Errorf("invalid service ID type %d", serviceID.Type))
}
}

View file

@ -52,6 +52,8 @@ type QRLogin struct {
cancelChan context.CancelFunc cancelChan context.CancelFunc
ProvChan chan signalmeow.ProvisioningResponse ProvChan chan signalmeow.ProvisioningResponse
newQRCount int newQRCount int
ProvData *store.DeviceData
} }
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil) var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
@ -75,7 +77,7 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
Str("action", "login"). Str("action", "login").
Stringer("user_id", qr.User.MXID). Stringer("user_id", qr.User.MXID).
Logger() Logger()
provCtx, cancel := context.WithCancel(log.WithContext(qr.Main.Bridge.BackgroundCtx)) provCtx, cancel := context.WithCancel(log.WithContext(context.Background()))
qr.cancelChan = cancel qr.cancelChan = cancel
// Don't use the start context here: the channel will outlive the start request. // Don't use the start context here: the channel will outlive the start request.
qr.ProvChan = signalmeow.PerformProvisioning( qr.ProvChan = signalmeow.PerformProvisioning(
@ -110,6 +112,14 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
return nil, fmt.Errorf("login not started") return nil, fmt.Errorf("login not started")
} }
if qr.ProvData == nil {
return qr.qrWait(ctx)
} else {
return qr.processingWait(ctx)
}
}
func (qr *QRLogin) qrWait(ctx context.Context) (*bridgev2.LoginStep, error) {
select { select {
case resp := <-qr.ProvChan: case resp := <-qr.ProvChan:
if resp.Err != nil { if resp.Err != nil {
@ -122,7 +132,15 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
qr.cancelChan() qr.cancelChan()
return nil, fmt.Errorf("no signal account ID received") return nil, fmt.Errorf("no signal account ID received")
} }
return qr.loginComplete(ctx, resp.ProvisioningData) qr.ProvData = resp.ProvisioningData
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait,
StepID: LoginStepProcess,
Instructions: fmt.Sprintf("Processing login as %s...", resp.ProvisioningData.Number),
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
Type: bridgev2.LoginDisplayTypeNothing,
},
}, nil
// Server will timeout the request after 60 seconds, but Signal Desktop opens // Server will timeout the request after 60 seconds, but Signal Desktop opens
// a new socket and gets a new QR code after 45 seconds. We should do the same. // a new socket and gets a new QR code after 45 seconds. We should do the same.
@ -140,13 +158,26 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
} }
} }
func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData) (*bridgev2.LoginStep, error) { func (qr *QRLogin) processingWait(ctx context.Context) (*bridgev2.LoginStep, error) {
defer qr.cancelChan() defer qr.cancelChan()
newLoginID := signalid.MakeUserLoginID(qr.ProvData.ACI)
select {
case resp := <-qr.ProvChan:
if resp.Err != nil {
return nil, resp.Err
} else if resp.State != signalmeow.StateProvisioningPreKeysRegistered {
return nil, fmt.Errorf("unexpected state %v", resp.State)
}
case <-ctx.Done():
return nil, ctx.Err()
}
ul, err := qr.User.NewLogin(ctx, &database.UserLogin{ ul, err := qr.User.NewLogin(ctx, &database.UserLogin{
ID: signalid.MakeUserLoginID(provData.ACI), ID: newLoginID,
RemoteName: provData.Number, RemoteName: qr.ProvData.Number,
RemoteProfile: status.RemoteProfile{ RemoteProfile: status.RemoteProfile{
Phone: provData.Number, Phone: qr.ProvData.Number,
}, },
Metadata: &signalid.UserLoginMetadata{}, Metadata: &signalid.UserLoginMetadata{},
}, &bridgev2.NewLoginParams{ }, &bridgev2.NewLoginParams{
@ -159,7 +190,7 @@ func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData
return &bridgev2.LoginStep{ return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeComplete, Type: bridgev2.LoginStepTypeComplete,
StepID: LoginStepComplete, StepID: LoginStepComplete,
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", provData.Number, provData.ACI), Instructions: fmt.Sprintf("Successfully logged in as %s / %s", qr.ProvData.Number, qr.ProvData.ACI),
CompleteParams: &bridgev2.LoginCompleteParams{ CompleteParams: &bridgev2.LoginCompleteParams{
UserLoginID: ul.ID, UserLoginID: ul.ID,
UserLogin: ul, UserLogin: ul,

View file

@ -21,8 +21,6 @@ package libsignalgo
*/ */
import "C" import "C"
import ( import (
"fmt"
"runtime"
"unsafe" "unsafe"
) )
@ -44,22 +42,6 @@ func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
return buf return buf
} }
func ManyBytesToBuffer[T ~[]byte](datas []T) (C.SignalBorrowedSliceOfBuffers, func()) {
buffers := make([]C.SignalBorrowedBuffer, len(datas))
var pinner runtime.Pinner
for i, data := range datas {
if len(data) == 0 {
panic(fmt.Errorf("empty slice passed to ManyBytesToBuffer at index %d", i))
}
pinner.Pin(&data[0])
buffers[i] = BytesToBuffer(data)
}
return C.SignalBorrowedSliceOfBuffers{
base: unsafe.SliceData(buffers),
length: C.size_t(len(buffers)),
}, pinner.Unpin
}
func EmptyBorrowedBuffer() C.SignalBorrowedBuffer { func EmptyBorrowedBuffer() C.SignalBorrowedBuffer {
return C.SignalBorrowedBuffer{} return C.SignalBorrowedBuffer{}
} }

View file

@ -1,6 +1,6 @@
package libsignalgo package libsignalgo
/* /*
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz -lstdc++ #cgo LDFLAGS: -lsignal_ffi -ldl -lm
*/ */
import "C" import "C"

View file

@ -39,17 +39,3 @@ func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
C.signal_free_buffer(buffer.base, buffer.length) C.signal_free_buffer(buffer.base, buffer.length)
return return
} }
func CopySignalBytestringArray[T ~[]byte](buffer C.SignalBytestringArray) (b []T) {
concatted := C.GoBytes(unsafe.Pointer(buffer.bytes.base), C.int(buffer.bytes.length))
b = make([]T, int(buffer.lengths.length))
sizeTSize := unsafe.Sizeof(C.size_t(0))
offset := 0
for i := 0; i < int(buffer.lengths.length); i++ {
length := int(*(*C.size_t)(unsafe.Add(unsafe.Pointer(buffer.lengths.base), uintptr(i)*sizeTSize)))
b[i] = concatted[offset : offset+length]
offset += length
}
C.signal_free_bytestring_array(buffer)
return
}

View file

@ -23,6 +23,7 @@ package libsignalgo
import "C" import "C"
import ( import (
"runtime" "runtime"
"time"
) )
type DecryptionErrorMessage struct { type DecryptionErrorMessage struct {
@ -48,7 +49,7 @@ func DeserializeDecryptionErrorMessage(messageBytes []byte) (*DecryptionErrorMes
return wrapDecryptionErrorMessage(dem.raw), nil return wrapDecryptionErrorMessage(dem.raw), nil
} }
func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType CiphertextMessageType, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) { func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType uint8, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) {
var dem C.SignalMutPointerDecryptionErrorMessage var dem C.SignalMutPointerDecryptionErrorMessage
signalFfiError := C.signal_decryption_error_message_for_original_message( signalFfiError := C.signal_decryption_error_message_for_original_message(
&dem, &dem,
@ -111,14 +112,14 @@ func (dem *DecryptionErrorMessage) Serialize() ([]byte, error) {
return CopySignalOwnedBufferToBytes(serialized), nil return CopySignalOwnedBufferToBytes(serialized), nil
} }
func (dem *DecryptionErrorMessage) GetTimestamp() (uint64, error) { func (dem *DecryptionErrorMessage) GetTimestamp() (time.Time, error) {
var ts C.uint64_t var ts C.uint64_t
signalFfiError := C.signal_decryption_error_message_get_timestamp(&ts, dem.constPtr()) signalFfiError := C.signal_decryption_error_message_get_timestamp(&ts, dem.constPtr())
runtime.KeepAlive(dem) runtime.KeepAlive(dem)
if signalFfiError != nil { if signalFfiError != nil {
return 0, wrapError(signalFfiError) return time.Time{}, wrapError(signalFfiError)
} }
return uint64(ts), nil return time.UnixMilli(int64(ts)), nil
} }
func (dem *DecryptionErrorMessage) GetDeviceID() (uint32, error) { func (dem *DecryptionErrorMessage) GetDeviceID() (uint32, error) {

View file

@ -26,10 +26,6 @@ import (
type ErrorCode int type ErrorCode int
func (e ErrorCode) Error() string {
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
}
const ( const (
ErrorCodeUnknownError ErrorCode = 1 ErrorCodeUnknownError ErrorCode = 1
ErrorCodeInvalidState ErrorCode = 2 ErrorCodeInvalidState ErrorCode = 2
@ -38,7 +34,6 @@ const (
ErrorCodeInvalidArgument ErrorCode = 5 ErrorCodeInvalidArgument ErrorCode = 5
ErrorCodeInvalidType ErrorCode = 6 ErrorCodeInvalidType ErrorCode = 6
ErrorCodeInvalidUtf8String ErrorCode = 7 ErrorCodeInvalidUtf8String ErrorCode = 7
ErrorCodeCancelled ErrorCode = 8
ErrorCodeProtobufError ErrorCode = 10 ErrorCodeProtobufError ErrorCode = 10
ErrorCodeLegacyCiphertextVersion ErrorCode = 21 ErrorCodeLegacyCiphertextVersion ErrorCode = 21
ErrorCodeUnknownCiphertextVersion ErrorCode = 22 ErrorCodeUnknownCiphertextVersion ErrorCode = 22
@ -56,61 +51,9 @@ const (
ErrorCodeInvalidRegistrationId ErrorCode = 81 ErrorCodeInvalidRegistrationId ErrorCode = 81
ErrorCodeInvalidSession ErrorCode = 82 ErrorCodeInvalidSession ErrorCode = 82
ErrorCodeInvalidSenderKeySession ErrorCode = 83 ErrorCodeInvalidSenderKeySession ErrorCode = 83
ErrorCodeInvalidProtocolAddress ErrorCode = 84
ErrorCodeDuplicatedMessage ErrorCode = 90 ErrorCodeDuplicatedMessage ErrorCode = 90
ErrorCodeCallbackError ErrorCode = 100 ErrorCodeCallbackError ErrorCode = 100
ErrorCodeVerificationFailure ErrorCode = 110 ErrorCodeVerificationFailure ErrorCode = 110
ErrorCodeUsernameCannotBeEmpty ErrorCode = 120
ErrorCodeUsernameCannotStartWithDigit ErrorCode = 121
ErrorCodeUsernameMissingSeparator ErrorCode = 122
ErrorCodeUsernameBadDiscriminatorCharacter ErrorCode = 123
ErrorCodeUsernameBadNicknameCharacter ErrorCode = 124
ErrorCodeUsernameTooShort ErrorCode = 125
ErrorCodeUsernameTooLong ErrorCode = 126
ErrorCodeUsernameLinkInvalidEntropyDataLength ErrorCode = 127
ErrorCodeUsernameLinkInvalid ErrorCode = 128
ErrorCodeUsernameDiscriminatorCannotBeEmpty ErrorCode = 130
ErrorCodeUsernameDiscriminatorCannotBeZero ErrorCode = 131
ErrorCodeUsernameDiscriminatorCannotBeSingleDigit ErrorCode = 132
ErrorCodeUsernameDiscriminatorCannotHaveLeadingZeros ErrorCode = 133
ErrorCodeUsernameDiscriminatorTooLarge ErrorCode = 134
ErrorCodeIoError ErrorCode = 140
ErrorCodeInvalidMediaInput ErrorCode = 141
ErrorCodeUnsupportedMediaInput ErrorCode = 142
ErrorCodeConnectionTimedOut ErrorCode = 143
ErrorCodeNetworkProtocol ErrorCode = 144
ErrorCodeRateLimited ErrorCode = 145
ErrorCodeWebSocket ErrorCode = 146
ErrorCodeCdsiInvalidToken ErrorCode = 147
ErrorCodeConnectionFailed ErrorCode = 148
ErrorCodeChatServiceInactive ErrorCode = 149
ErrorCodeRequestTimedOut ErrorCode = 150
ErrorCodeRateLimitChallenge ErrorCode = 151
ErrorCodePossibleCaptiveNetwork ErrorCode = 152
ErrorCodeSvrDataMissing ErrorCode = 160
ErrorCodeSvrRestoreFailed ErrorCode = 161
ErrorCodeSvrRotationMachineTooManySteps ErrorCode = 162
ErrorCodeSvrRequestFailed ErrorCode = 163
ErrorCodeAppExpired ErrorCode = 170
ErrorCodeDeviceDeregistered ErrorCode = 171
ErrorCodeConnectionInvalidated ErrorCode = 172
ErrorCodeConnectedElsewhere ErrorCode = 173
ErrorCodeBackupValidation ErrorCode = 180
ErrorCodeRegistrationInvalidSessionId ErrorCode = 190
ErrorCodeRegistrationUnknown ErrorCode = 192
ErrorCodeRegistrationSessionNotFound ErrorCode = 193
ErrorCodeRegistrationNotReadyForVerification ErrorCode = 194
ErrorCodeRegistrationSendVerificationCodeFailed ErrorCode = 195
ErrorCodeRegistrationCodeNotDeliverable ErrorCode = 196
ErrorCodeRegistrationSessionUpdateRejected ErrorCode = 197
ErrorCodeRegistrationCredentialsCouldNotBeParsed ErrorCode = 198
ErrorCodeRegistrationDeviceTransferPossible ErrorCode = 199
ErrorCodeRegistrationRecoveryVerificationFailed ErrorCode = 200
ErrorCodeRegistrationLock ErrorCode = 201
ErrorCodeKeyTransparencyError ErrorCode = 210
ErrorCodeKeyTransparencyVerificationFailed ErrorCode = 211
ErrorCodeRequestUnauthorized ErrorCode = 220
ErrorCodeMismatchedDevices ErrorCode = 221
) )
type SignalError struct { type SignalError struct {
@ -122,10 +65,6 @@ func (e *SignalError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message) return fmt.Sprintf("%d: %s", e.Code, e.Message)
} }
func (e *SignalError) Unwrap() error {
return e.Code
}
func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error { func (ctx *CallbackContext) wrapError(signalError *C.SignalFfiError) error {
if signalError == nil { if signalError == nil {
return nil return nil

View file

@ -36,7 +36,7 @@ func GroupEncrypt(ctx context.Context, ptext []byte, sender *Address, distributi
signalFfiError := C.signal_group_encrypt_message( signalFfiError := C.signal_group_encrypt_message(
&ciphertextMessage, &ciphertextMessage,
sender.constPtr(), sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)), (*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
BytesToBuffer(ptext), BytesToBuffer(ptext),
callbackCtx.wrapSenderKeyStore(store)) callbackCtx.wrapSenderKeyStore(store))
runtime.KeepAlive(ptext) runtime.KeepAlive(ptext)

View file

@ -76,10 +76,6 @@ func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
} }
} }
func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
return DeriveGroupSecretParamsFromMasterKey(gmk)
}
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) { func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness))) signalFfiError := C.signal_group_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))

View file

@ -1,215 +0,0 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2025 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 libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
import (
"encoding/base64"
"runtime"
"time"
"unsafe"
)
type GroupSendFullToken []byte
func (gsft GroupSendFullToken) String() string {
return base64.StdEncoding.EncodeToString(gsft)
}
func (gsft GroupSendFullToken) CheckValidContents() error {
signalFfiError := C.signal_group_send_full_token_check_valid_contents(
BytesToBuffer(gsft),
)
runtime.KeepAlive(gsft)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gsft GroupSendFullToken) GetExpiration() (time.Time, error) {
var expiration C.uint64_t
signalFfiError := C.signal_group_send_full_token_get_expiration(
&expiration,
BytesToBuffer(gsft),
)
runtime.KeepAlive(gsft)
if signalFfiError != nil {
return time.Time{}, wrapError(signalFfiError)
}
return time.Unix(int64(expiration), 0), nil
}
type GroupSendToken []byte
func (gst GroupSendToken) CheckValidContents() error {
signalFfiError := C.signal_group_send_token_check_valid_contents(
BytesToBuffer(gst),
)
runtime.KeepAlive(gst)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gst GroupSendToken) ToFullToken(expiration time.Time) (GroupSendFullToken, error) {
var fullToken C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_token_to_full_token(
&fullToken,
BytesToBuffer(gst),
C.uint64_t(expiration.Unix()),
)
runtime.KeepAlive(gst)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(fullToken), nil
}
type GroupSendEndorsement []byte
func (gse GroupSendEndorsement) ToToken(groupSecretParams *GroupSecretParams) (GroupSendToken, error) {
var token C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_endorsement_to_token(
&token,
BytesToBuffer(gse),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(groupSecretParams)),
)
runtime.KeepAlive(gse)
runtime.KeepAlive(groupSecretParams)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(token), nil
}
func (gse GroupSendEndorsement) ToFullToken(params *GroupSecretParams, expiration time.Time) (GroupSendFullToken, error) {
token, err := gse.ToToken(params)
if err != nil {
return nil, err
}
return token.ToFullToken(expiration)
}
func (gse GroupSendEndorsement) CheckValidContents() error {
signalFfiError := C.signal_group_send_endorsement_check_valid_contents(
BytesToBuffer(gse),
)
runtime.KeepAlive(gse)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gse GroupSendEndorsement) Remove(other GroupSendEndorsement) (GroupSendEndorsement, error) {
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
signalFfiError := C.signal_group_send_endorsement_remove(
&result,
BytesToBuffer(gse),
BytesToBuffer(other),
)
runtime.KeepAlive(gse)
runtime.KeepAlive(other)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(result), nil
}
func GroupSendEndorsementCombine(endorsements ...GroupSendEndorsement) (GroupSendEndorsement, error) {
var result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
cEndorsements, unpin := ManyBytesToBuffer(endorsements)
defer unpin()
signalFfiError := C.signal_group_send_endorsement_combine(
&result,
cEndorsements,
)
runtime.KeepAlive(endorsements)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(result), nil
}
type GroupSendEndorsementsResponse []byte
func (gser GroupSendEndorsementsResponse) GetExpiration() (time.Time, error) {
var expiration C.uint64_t
signalFfiError := C.signal_group_send_endorsements_response_get_expiration(
&expiration,
BytesToBuffer(gser),
)
runtime.KeepAlive(gser)
if signalFfiError != nil {
return time.Time{}, wrapError(signalFfiError)
}
return time.Unix(int64(expiration), 0), nil
}
func (gser GroupSendEndorsementsResponse) CheckValidContents() error {
signalFfiError := C.signal_group_send_endorsements_response_check_valid_contents(
BytesToBuffer(gser),
)
runtime.KeepAlive(gser)
if signalFfiError != nil {
return wrapError(signalFfiError)
}
return nil
}
func (gser GroupSendEndorsementsResponse) ReceiveWithServiceIDs(
groupMembers []ServiceID, localUser ServiceID, params *GroupSecretParams, spp *ServerPublicParams,
) (GroupSendEndorsement, map[ServiceID]GroupSendEndorsement, error) {
var out C.SignalBytestringArray = C.SignalBytestringArray{}
concatenatedMembers := make([]byte, len(groupMembers)*17)
for i, member := range groupMembers {
copy(concatenatedMembers[i*17:(i+1)*17], member.FixedBytes()[:])
}
signalFfiError := C.signal_group_send_endorsements_response_receive_and_combine_with_service_ids(
&out,
BytesToBuffer(gser),
BytesToBuffer(concatenatedMembers),
localUser.CFixedBytes(),
C.uint64_t(time.Now().Unix()),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(params)),
C.SignalConstPointerServerPublicParams{spp},
)
runtime.KeepAlive(gser)
runtime.KeepAlive(concatenatedMembers)
runtime.KeepAlive(params)
runtime.KeepAlive(spp)
if signalFfiError != nil {
return nil, nil, wrapError(signalFfiError)
}
endorsements := CopySignalBytestringArray[GroupSendEndorsement](out)
memberEndorsements := make(map[ServiceID]GroupSendEndorsement, len(groupMembers))
for i, member := range groupMembers {
if len(endorsements) > i && len(endorsements[i]) > 0 {
memberEndorsements[member] = endorsements[i]
}
}
combined, err := GroupSendEndorsementCombine(endorsements...)
if err != nil {
return nil, memberEndorsements, err
}
return combined, memberEndorsements, nil
}

View file

@ -84,7 +84,8 @@ func (i *IdentityKey) VerifyAlternateIdentity(other *IdentityKey, signature []by
} }
func (i *IdentityKey) Equal(other *IdentityKey) (bool, error) { func (i *IdentityKey) Equal(other *IdentityKey) (bool, error) {
return i.publicKey.Equal(other.publicKey) result, err := i.publicKey.Compare(other.publicKey)
return result == 0, err
} }
type IdentityKeyPair struct { type IdentityKeyPair struct {
@ -113,13 +114,14 @@ func GenerateIdentityKeyPair() (*IdentityKeyPair, error) {
} }
func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) { func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) {
var keys C.SignalPairOfMutPointerPublicKeyMutPointerPrivateKey var privateKey C.SignalMutPointerPrivateKey
signalFfiError := C.signal_identitykeypair_deserialize(&keys, BytesToBuffer(bytes)) var publicKey C.SignalMutPointerPublicKey
signalFfiError := C.signal_identitykeypair_deserialize(&privateKey, &publicKey, BytesToBuffer(bytes))
runtime.KeepAlive(bytes) runtime.KeepAlive(bytes)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) return nil, wrapError(signalFfiError)
} }
return &IdentityKeyPair{publicKey: wrapPublicKey(keys.first.raw), privateKey: wrapPrivateKey(keys.second.raw)}, nil return &IdentityKeyPair{publicKey: wrapPublicKey(publicKey.raw), privateKey: wrapPrivateKey(privateKey.raw)}, nil
} }
func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*IdentityKeyPair, error) { func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*IdentityKeyPair, error) {

View file

@ -20,12 +20,14 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *keyp); typedef const SignalProtocolAddress const_address;
typedef const SignalPublicKey const_public_key;
extern int signal_get_identity_key_pair_callback(void *store_ctx, SignalPrivateKey **keyp);
extern int signal_get_local_registration_id_callback(void *store_ctx, uint32_t *idp); extern int signal_get_local_registration_id_callback(void *store_ctx, uint32_t *idp);
extern int signal_save_identity_key_callback(void *store_ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key); extern int signal_save_identity_key_callback(void *store_ctx, const_address *address, const_public_key *public_key);
extern int signal_get_identity_key_callback(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address); extern int signal_get_identity_key_callback(void *store_ctx, SignalPublicKey **public_keyp, const_address *address);
extern int signal_is_trusted_identity_callback(void *store_ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction); extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
*/ */
import "C" import "C"
import ( import (
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
} }
//export signal_get_identity_key_pair_callback //export signal_get_identity_key_pair_callback
func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp *C.SignalPairOfMutPointerPrivateKeyMutPointerPublicKey) C.int { func signal_get_identity_key_pair_callback(storeCtx unsafe.Pointer, keyp **C.SignalPrivateKey) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
key, err := store.GetIdentityKeyPair(ctx) key, err := store.GetIdentityKeyPair(ctx)
if err != nil { if err != nil {
return err return err
} }
if key == nil { if key == nil {
keyp.first.raw = nil *keyp = nil
keyp.second.raw = nil } else {
return nil clone, err := key.privateKey.Clone()
}
privClone, err := key.privateKey.Clone()
if err != nil { if err != nil {
return err return err
} }
pubClone, err := key.publicKey.Clone() clone.CancelFinalizer()
if err != nil { *keyp = clone.ptr
return err
} }
privClone.CancelFinalizer()
pubClone.CancelFinalizer()
keyp.first.raw = privClone.ptr
keyp.second.raw = pubClone.ptr
return err return err
}) })
} }
@ -88,17 +83,17 @@ func signal_get_local_registration_id_callback(storeCtx unsafe.Pointer, idp *C.u
} }
//export signal_save_identity_key_callback //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 { func signal_save_identity_key_callback(storeCtx unsafe.Pointer, address *C.const_address, publicKey *C.const_public_key) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
publicKeyStruct := PublicKey{ptr: publicKey.raw} publicKeyStruct := PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(publicKey))}
cloned, err := publicKeyStruct.Clone() cloned, err := publicKeyStruct.Clone()
if err != nil { if err != nil {
return err return -1, err
} }
addr := &Address{ptr: address.raw} addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID() theirServiceID, err := addr.NameServiceID()
if err != nil { if err != nil {
return err return -1, err
} }
replaced, err := store.SaveIdentityKey( replaced, err := store.SaveIdentityKey(
ctx, ctx,
@ -106,21 +101,20 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t,
&IdentityKey{cloned}, &IdentityKey{cloned},
) )
if err != nil { if err != nil {
return err return -1, err
} }
if replaced { if replaced {
*out = 1 return 1, nil
} else { } else {
*out = 0 return 0, nil
} }
return nil
}) })
} }
//export signal_get_identity_key_callback //export signal_get_identity_key_callback
func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.SignalMutPointerPublicKey, address C.SignalMutPointerProtocolAddress) C.int { func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp **C.SignalPublicKey, address *C.const_address) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
addr := &Address{ptr: address.raw} addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID() theirServiceID, err := addr.NameServiceID()
if err != nil { if err != nil {
return err return err
@ -128,42 +122,39 @@ func signal_get_identity_key_callback(storeCtx unsafe.Pointer, public_keyp *C.Si
key, err := store.GetIdentityKey(ctx, theirServiceID) key, err := store.GetIdentityKey(ctx, theirServiceID)
if err == nil && key != nil { if err == nil && key != nil {
key.publicKey.CancelFinalizer() key.publicKey.CancelFinalizer()
public_keyp.raw = key.publicKey.ptr *public_keyp = key.publicKey.ptr
} }
return err return err
}) })
} }
//export signal_is_trusted_identity_callback //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 { func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, address *C.const_address, public_key *C.const_public_key, direction C.uint) C.int {
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error { return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
addr := &Address{ptr: address.raw} addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
theirServiceID, err := addr.NameServiceID() theirServiceID, err := addr.NameServiceID()
if err != nil { if err != nil {
return err return -1, err
} }
trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: public_key.raw}}, SignalDirection(direction)) trusted, err := store.IsTrustedIdentity(ctx, theirServiceID, &IdentityKey{&PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(public_key))}}, SignalDirection(direction))
if err != nil { if err != nil {
return err return -1, err
}
if trusted {
return 1, nil
} else {
return 0, nil
} }
*out = C.bool(trusted)
return nil
}) })
} }
//export signal_destroy_identity_key_store_callback
func signal_destroy_identity_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct { func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct {
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{ return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback), get_identity_key_pair: C.SignalGetIdentityKeyPair(C.signal_get_identity_key_pair_callback),
get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback), get_local_registration_id: C.SignalGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback), save_identity: C.SignalSaveIdentityKey(C.signal_save_identity_key_callback),
save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback), get_identity: C.SignalGetIdentityKey(C.signal_get_identity_key_callback),
is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback), is_trusted_identity: C.SignalIsTrustedIdentity(C.signal_is_trusted_identity_callback),
destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
}} }}
} }

View file

@ -20,10 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalMutPointerKyberPreKeyRecord *recordp, uint32_t id); typedef const SignalKyberPreKeyRecord const_kyber_pre_key_record;
extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key); extern int signal_load_kyber_pre_key_callback(void *store_ctx, SignalKyberPreKeyRecord **recordp, uint32_t id);
extern void signal_destroy_kyber_pre_key_store_callback(void *store_ctx); extern int signal_store_kyber_pre_key_callback(void *store_ctx, uint32_t id, const_kyber_pre_key_record *record);
extern int signal_mark_kyber_pre_key_used_callback(void *store_ctx, uint32_t id);
*/ */
import "C" import "C"
import ( import (
@ -38,21 +39,21 @@ type KyberPreKeyStore interface {
} }
//export signal_load_kyber_pre_key_callback //export signal_load_kyber_pre_key_callback
func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerKyberPreKeyRecord, id C.uint32_t) C.int { func signal_load_kyber_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalKyberPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
key, err := store.LoadKyberPreKey(ctx, uint32(id)) key, err := store.LoadKyberPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_kyber_pre_key_callback //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 { func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_kyber_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
record := KyberPreKeyRecord{ptr: preKeyRecord.raw} record := KyberPreKeyRecord{ptr: (*C.SignalKyberPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone() cloned, err := record.Clone()
if err != nil { if err != nil {
return err return err
@ -62,24 +63,18 @@ func signal_store_kyber_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t,
} }
//export signal_mark_kyber_pre_key_used_callback //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 { func signal_mark_kyber_pre_key_used_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store KyberPreKeyStore, ctx context.Context) error {
// TODO use ecPrekeyID and baseKey? err := store.MarkKyberPreKeyUsed(ctx, uint32(id))
return store.MarkKyberPreKeyUsed(ctx, uint32(id)) return err
}) })
} }
//export signal_destroy_kyber_pre_key_store_callback
func signal_destroy_kyber_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct { func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{ return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_kyber_pre_key: C.SignalFfiKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback), load_kyber_pre_key: C.SignalLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
store_kyber_pre_key: C.SignalFfiKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback), store_kyber_pre_key: C.SignalStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
mark_kyber_pre_key_used: C.SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback), mark_kyber_pre_key_used: C.SignalMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
destroy: C.SignalFfiKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
}} }}
} }

@ -1 +1 @@
Subproject commit bbc16886cae2feab1cd1fe271ccc651e8860ce96 Subproject commit 43a23efa1118ac32a1434ab317025adfa2b91e4a

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,6 @@ package libsignalgo
extern void signal_log_callback(void *ctx, SignalLogLevel level, char *file, uint32_t line, char *message); extern void signal_log_callback(void *ctx, SignalLogLevel level, char *file, uint32_t line, char *message);
extern void signal_log_flush_callback(void *ctx); extern void signal_log_flush_callback(void *ctx);
extern void signal_log_destroy_callback(void *ctx);
*/ */
import "C" import "C"
import ( import (
@ -41,11 +40,6 @@ func signal_log_flush_callback(ctx unsafe.Pointer) {
ffiLogger.Flush() ffiLogger.Flush()
} }
//export signal_log_destroy_callback
func signal_log_destroy_callback(ctx unsafe.Pointer) {
ffiLogger.Destroy()
}
type LogLevel int type LogLevel int
const ( const (
@ -59,14 +53,12 @@ const (
type Logger interface { type Logger interface {
Log(level LogLevel, file string, line uint, message string) Log(level LogLevel, file string, line uint, message string)
Flush() Flush()
Destroy()
} }
func InitLogger(level LogLevel, logger Logger) { func InitLogger(level LogLevel, logger Logger) {
ffiLogger = logger ffiLogger = logger
C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLoggerStruct{ C.signal_init_logger(C.SignalLogLevel(level), C.SignalFfiLogger{
log: C.SignalFfiLoggerLog(C.signal_log_callback), log: C.SignalLogCallback(C.signal_log_callback),
flush: C.SignalFfiLoggerFlush(C.signal_log_flush_callback), flush: C.SignalLogFlushCallback(C.signal_log_flush_callback),
destroy: C.SignalFfiLoggerDestroy(C.signal_log_destroy_callback),
}) })
} }

View file

@ -27,7 +27,7 @@ import (
"time" "time"
) )
func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) { func Encrypt(ctx context.Context, plaintext []byte, forAddress *Address, sessionStore SessionStore, identityKeyStore IdentityKeyStore) (*CiphertextMessage, error) {
var ciphertextMessage C.SignalMutPointerCiphertextMessage var ciphertextMessage C.SignalMutPointerCiphertextMessage
var now C.uint64_t = C.uint64_t(time.Now().Unix()) var now C.uint64_t = C.uint64_t(time.Now().Unix())
callbackCtx := NewCallbackContext(ctx) callbackCtx := NewCallbackContext(ctx)
@ -36,7 +36,6 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
&ciphertextMessage, &ciphertextMessage,
BytesToBuffer(plaintext), BytesToBuffer(plaintext),
forAddress.constPtr(), forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityKeyStore), callbackCtx.wrapIdentityKeyStore(identityKeyStore),
now, now,
@ -49,7 +48,7 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
return wrapCiphertextMessage(ciphertextMessage.raw), nil return wrapCiphertextMessage(ciphertextMessage.raw), nil
} }
func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) { func Decrypt(ctx context.Context, message *Message, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
callbackCtx := NewCallbackContext(ctx) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{} var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -57,7 +56,6 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
&decrypted, &decrypted,
message.constPtr(), message.constPtr(),
fromAddress.constPtr(), fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
) )

View file

@ -26,7 +26,7 @@ import (
"runtime" "runtime"
) )
func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore, preKeyStore PreKeyStore, signedPreKeyStore SignedPreKeyStore, kyberPreKeyStore KyberPreKeyStore) ([]byte, error) { func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore, preKeyStore PreKeyStore, signedPreKeyStore SignedPreKeyStore, kyberPreKeyStore KyberPreKeyStore) ([]byte, error) {
callbackCtx := NewCallbackContext(ctx) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{} var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@ -34,12 +34,12 @@ func DecryptPreKey(ctx context.Context, preKeyMessage *PreKeyMessage, fromAddres
&decrypted, &decrypted,
preKeyMessage.constPtr(), preKeyMessage.constPtr(),
fromAddress.constPtr(), fromAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
callbackCtx.wrapPreKeyStore(preKeyStore), callbackCtx.wrapPreKeyStore(preKeyStore),
callbackCtx.wrapSignedPreKeyStore(signedPreKeyStore), callbackCtx.wrapSignedPreKeyStore(signedPreKeyStore),
callbackCtx.wrapKyberPreKeyStore(kyberPreKeyStore), callbackCtx.wrapKyberPreKeyStore(kyberPreKeyStore),
false, // no pq ratchets yet
) )
runtime.KeepAlive(preKeyMessage) runtime.KeepAlive(preKeyMessage)
runtime.KeepAlive(fromAddress) runtime.KeepAlive(fromAddress)

View file

@ -27,17 +27,17 @@ import (
"time" "time"
) )
func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error { func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error {
callbackCtx := NewCallbackContext(ctx) callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref() defer callbackCtx.Unref()
var now C.uint64_t = C.uint64_t(time.Now().Unix()) var now C.uint64_t = C.uint64_t(time.Now().Unix())
signalFfiError := C.signal_process_prekey_bundle( signalFfiError := C.signal_process_prekey_bundle(
bundle.constPtr(), bundle.constPtr(),
forAddress.constPtr(), forAddress.constPtr(),
localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore), callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore), callbackCtx.wrapIdentityKeyStore(identityStore),
now, now,
false, // no pq ratchets yet
) )
runtime.KeepAlive(bundle) runtime.KeepAlive(bundle)
runtime.KeepAlive(forAddress) runtime.KeepAlive(forAddress)

View file

@ -20,10 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_pre_key_callback(void *store_ctx, SignalMutPointerPreKeyRecord *recordp, uint32_t id); typedef const SignalPreKeyRecord const_pre_key_record;
extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
extern int signal_load_pre_key_callback(void *store_ctx, SignalPreKeyRecord **recordp, uint32_t id);
extern int signal_store_pre_key_callback(void *store_ctx, uint32_t id, const_pre_key_record *record);
extern int signal_remove_pre_key_callback(void *store_ctx, uint32_t id); extern int signal_remove_pre_key_callback(void *store_ctx, uint32_t id);
extern void signal_destroy_pre_key_store_callback(void *store_ctx);
*/ */
import "C" import "C"
import ( import (
@ -38,21 +39,21 @@ type PreKeyStore interface {
} }
//export signal_load_pre_key_callback //export signal_load_pre_key_callback
func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerPreKeyRecord, id C.uint32_t) C.int { func signal_load_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
key, err := store.LoadPreKey(ctx, uint32(id)) key, err := store.LoadPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_pre_key_callback //export signal_store_pre_key_callback
func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord C.SignalMutPointerPreKeyRecord) C.int { func signal_store_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store PreKeyStore, ctx context.Context) error {
record := PreKeyRecord{ptr: preKeyRecord.raw} record := PreKeyRecord{ptr: (*C.SignalPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone() cloned, err := record.Clone()
if err != nil { if err != nil {
return err return err
@ -68,17 +69,11 @@ func signal_remove_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t) C.in
}) })
} }
//export signal_destroy_pre_key_store_callback
func signal_destroy_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct { func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct {
return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{ return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_pre_key: C.SignalFfiPreKeyStoreLoadPreKey(C.signal_load_pre_key_callback), load_pre_key: C.SignalLoadPreKey(C.signal_load_pre_key_callback),
store_pre_key: C.SignalFfiPreKeyStoreStorePreKey(C.signal_store_pre_key_callback), store_pre_key: C.SignalStorePreKey(C.signal_store_pre_key_callback),
remove_pre_key: C.SignalFfiPreKeyStoreRemovePreKey(C.signal_remove_pre_key_callback), remove_pre_key: C.SignalRemovePreKey(C.signal_remove_pre_key_callback),
destroy: C.SignalFfiPreKeyStoreDestroy(C.signal_destroy_pre_key_store_callback),
}} }}
} }

View file

@ -23,7 +23,6 @@ package libsignalgo
*/ */
import "C" import "C"
import ( import (
"encoding/base64"
"errors" "errors"
"runtime" "runtime"
"unsafe" "unsafe"
@ -55,6 +54,10 @@ func (pk *ProfileKey) IsEmpty() bool {
return pk == nil || *pk == blankProfileKey return pk == nil || *pk == blankProfileKey
} }
func (ak *AccessKey) String() string {
return string(ak[:])
}
func (pv *ProfileKeyVersion) String() string { func (pv *ProfileKeyVersion) String() string {
return string(pv[:]) return string(pv[:])
} }
@ -66,23 +69,6 @@ func (pk *ProfileKey) Slice() []byte {
return pk[:] return pk[:]
} }
func (ak *AccessKey) Xor(other *AccessKey) *AccessKey {
if ak == nil {
return other
} else if other == nil {
return ak
}
var result AccessKey
for i := 0; i < C.SignalACCESS_KEY_LEN; i++ {
result[i] = ak[i] ^ other[i]
}
return &result
}
func (ak *AccessKey) String() string {
return base64.StdEncoding.EncodeToString(ak[:])
}
func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) { func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) {
c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{} c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk)) c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))

View file

@ -29,9 +29,6 @@ type PublicKey struct {
} }
func wrapPublicKey(ptr *C.SignalPublicKey) *PublicKey { func wrapPublicKey(ptr *C.SignalPublicKey) *PublicKey {
if ptr == nil {
return nil
}
publicKey := &PublicKey{ptr: ptr} publicKey := &PublicKey{ptr: ptr}
runtime.SetFinalizer(publicKey, (*PublicKey).Destroy) runtime.SetFinalizer(publicKey, (*PublicKey).Destroy)
return publicKey return publicKey
@ -84,15 +81,15 @@ func (k *PublicKey) CancelFinalizer() {
runtime.SetFinalizer(k, nil) runtime.SetFinalizer(k, nil)
} }
func (k *PublicKey) Equal(other *PublicKey) (bool, error) { func (k *PublicKey) Compare(other *PublicKey) (int, error) {
var comparison C.bool var comparison C.int
signalFfiError := C.signal_publickey_equals(&comparison, k.constPtr(), other.constPtr()) signalFfiError := C.signal_publickey_compare(&comparison, k.constPtr(), other.constPtr())
runtime.KeepAlive(k) runtime.KeepAlive(k)
runtime.KeepAlive(other) runtime.KeepAlive(other)
if signalFfiError != nil { if signalFfiError != nil {
return false, wrapError(signalFfiError) return 0, wrapError(signalFfiError)
} }
return bool(comparison), nil return int(comparison), nil
} }
func (k *PublicKey) Bytes() ([]byte, error) { func (k *PublicKey) Bytes() ([]byte, error) {

View file

@ -23,9 +23,7 @@ package libsignalgo
import "C" import "C"
import ( import (
"context" "context"
"fmt"
"runtime" "runtime"
"unsafe"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -44,17 +42,8 @@ func NewSealedSenderAddress(e164 string, uuid uuid.UUID, deviceID uint32) *Seale
} }
} }
func SealedSenderEncryptPlaintext( func SealedSenderEncryptPlaintext(ctx context.Context, message []byte, contentHint UnidentifiedSenderMessageContentHint, forAddress *Address, fromSenderCert *SenderCertificate, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
ctx context.Context, ciphertextMessage, err := Encrypt(ctx, message, forAddress, sessionStore, identityStore)
message []byte,
contentHint UnidentifiedSenderMessageContentHint,
forAddress, localAddress *Address,
fromSenderCert *SenderCertificate,
sessionStore SessionStore,
identityStore IdentityKeyStore,
groupID *GroupIdentifier,
) ([]byte, error) {
ciphertextMessage, err := Encrypt(ctx, message, forAddress, localAddress, sessionStore, identityStore)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,7 +52,7 @@ func SealedSenderEncryptPlaintext(
ciphertextMessage, ciphertextMessage,
fromSenderCert, fromSenderCert,
contentHint, contentHint,
groupID, nil,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -89,51 +78,8 @@ func SealedSenderEncrypt(ctx context.Context, usmc *UnidentifiedSenderMessageCon
return CopySignalOwnedBufferToBytes(encrypted), nil return CopySignalOwnedBufferToBytes(encrypted), nil
} }
type SessionAddressTuple struct { func SealedSenderMultiRecipientEncrypt(messageContent *UnidentifiedSenderMessageContent, forRecipients []*Address, identityStore IdentityKeyStore, sessionStore SessionStore, ctx *CallbackContext) ([]byte, error) {
ServiceID ServiceID panic("not implemented")
DeviceID int
Address *Address
Record *SessionRecord
}
func SealedSenderMultiRecipientEncrypt(
ctx context.Context,
usmc *UnidentifiedSenderMessageContent,
recipients []SessionAddressTuple,
identityStore IdentityKeyStore,
) ([]byte, error) {
var encrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
recipientAddresses := make([]C.SignalConstPointerProtocolAddress, len(recipients))
recipientSessions := make([]C.SignalConstPointerSessionRecord, len(recipients))
for i, recipient := range recipients {
recipientAddresses[i] = recipient.Address.constPtr()
recipientSessions[i] = recipient.Record.constPtr()
}
signalFfiError := C.signal_sealed_sender_multi_recipient_encrypt(
&encrypted,
C.SignalBorrowedSliceOfConstPointerProtocolAddress{
base: unsafe.SliceData(recipientAddresses),
length: C.size_t(len(recipientAddresses)),
},
C.SignalBorrowedSliceOfConstPointerSessionRecord{
base: unsafe.SliceData(recipientSessions),
length: C.size_t(len(recipientSessions)),
},
BytesToBuffer(nil),
usmc.constPtr(),
callbackCtx.wrapIdentityKeyStore(identityStore),
)
runtime.KeepAlive(usmc)
runtime.KeepAlive(recipients)
runtime.KeepAlive(recipientAddresses)
runtime.KeepAlive(recipientSessions)
if signalFfiError != nil {
return nil, callbackCtx.wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(encrypted), nil
} }
type SealedSenderResult struct { type SealedSenderResult struct {
@ -180,22 +126,18 @@ func wrapUnidentifiedSenderMessageContent(ptr *C.SignalUnidentifiedSenderMessage
return messageContent return messageContent
} }
func NewUnidentifiedSenderMessageContent(message *CiphertextMessage, senderCertificate *SenderCertificate, contentHint UnidentifiedSenderMessageContentHint, groupID *GroupIdentifier) (*UnidentifiedSenderMessageContent, error) { func NewUnidentifiedSenderMessageContent(message *CiphertextMessage, senderCertificate *SenderCertificate, contentHint UnidentifiedSenderMessageContentHint, groupID []byte) (*UnidentifiedSenderMessageContent, error) {
var usmc C.SignalMutPointerUnidentifiedSenderMessageContent var usmc C.SignalMutPointerUnidentifiedSenderMessageContent
var groupIDBytes []byte
if groupID != nil {
groupIDBytes = groupID[:]
}
signalFfiError := C.signal_unidentified_sender_message_content_new( signalFfiError := C.signal_unidentified_sender_message_content_new(
&usmc, &usmc,
message.constPtr(), message.constPtr(),
senderCertificate.constPtr(), senderCertificate.constPtr(),
C.uint32_t(contentHint), C.uint32_t(contentHint),
BytesToBuffer(groupIDBytes), BytesToBuffer(groupID),
) )
runtime.KeepAlive(message) runtime.KeepAlive(message)
runtime.KeepAlive(senderCertificate) runtime.KeepAlive(senderCertificate)
runtime.KeepAlive(groupIDBytes) runtime.KeepAlive(groupID)
if signalFfiError != nil { if signalFfiError != nil {
return nil, wrapError(signalFfiError) return nil, wrapError(signalFfiError)
} }
@ -267,21 +209,18 @@ func (usmc *UnidentifiedSenderMessageContent) GetContents() ([]byte, error) {
return CopySignalOwnedBufferToBytes(contents), nil return CopySignalOwnedBufferToBytes(contents), nil
} }
func (usmc *UnidentifiedSenderMessageContent) GetGroupID() (*GroupIdentifier, error) { //func (usmc *UnidentifiedSenderMessageContent) GetGroupID() ([]byte, error) {
var contents C.SignalOwnedBuffer = C.SignalOwnedBuffer{} // var groupID *C.uchar
signalFfiError := C.signal_unidentified_sender_message_content_get_group_id_or_empty(&contents, usmc.constPtr()) // var length C.ulong
runtime.KeepAlive(usmc) // signalFfiError := C.signal_unidentified_sender_message_content_get_group_id(&groupID, &length, usmc.ptr)
if signalFfiError != nil { // if signalFfiError != nil {
return nil, wrapError(signalFfiError) // return nil, wrapError(signalFfiError)
} // }
bytes := CopySignalOwnedBufferToBytes(contents) // if groupID == nil {
if len(bytes) == 0 { // return nil, nil
return nil, nil // }
} else if len(bytes) != GroupIdentifierLength { // return CopyBufferToBytes(groupID, length), nil
return nil, fmt.Errorf("unexpected group ID length: %d", len(bytes)) //}
}
return (*GroupIdentifier)(bytes), nil
}
func (usmc *UnidentifiedSenderMessageContent) GetSenderCertificate() (*SenderCertificate, error) { func (usmc *UnidentifiedSenderMessageContent) GetSenderCertificate() (*SenderCertificate, error) {
var senderCertificate C.SignalMutPointerSenderCertificate var senderCertificate C.SignalMutPointerSenderCertificate

View file

@ -60,7 +60,7 @@ func NewSenderKeyDistributionMessage(ctx context.Context, sender *Address, distr
signalFfiError := C.signal_sender_key_distribution_message_create( signalFfiError := C.signal_sender_key_distribution_message_create(
&skdm, &skdm,
sender.constPtr(), sender.constPtr(),
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)), (*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
callbackCtx.wrapSenderKeyStore(store), callbackCtx.wrapSenderKeyStore(store),
) )
runtime.KeepAlive(sender) runtime.KeepAlive(sender)

View file

@ -20,9 +20,13 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_sender_key_callback(void *store_ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id); typedef const SignalProtocolAddress const_address;
extern int signal_store_sender_key_callback(void *store_ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
extern void signal_destroy_sender_key_store_callback(void *store_ctx); typedef const SignalSenderKeyRecord const_sender_key_record;
typedef const uint8_t const_uuid_bytes[16];
extern int signal_load_sender_key_callback(void *store_ctx, SignalSenderKeyRecord**, const_address*, const_uuid_bytes*);
extern int signal_store_sender_key_callback(void *store_ctx, const_address*, const_uuid_bytes*, const_sender_key_record*);
*/ */
import "C" import "C"
import ( import (
@ -38,40 +42,36 @@ type SenderKeyStore interface {
} }
//export signal_load_sender_key_callback //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 { func signal_load_sender_key_callback(storeCtx unsafe.Pointer, recordp **C.SignalSenderKeyRecord, address *C.const_address, distributionIDBytes *C.const_uuid_bytes) C.int {
return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
record, err := store.LoadSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID))) distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
record, err := store.LoadSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID)
if err == nil && record != nil { if err == nil && record != nil {
record.CancelFinalizer() record.CancelFinalizer()
recordp.raw = record.ptr *recordp = record.ptr
} }
return err return err
}) })
} }
//export signal_store_sender_key_callback //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 { func signal_store_sender_key_callback(storeCtx unsafe.Pointer, address *C.const_address, distributionIDBytes *C.const_uuid_bytes, senderKeyRecord *C.const_sender_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SenderKeyStore, ctx context.Context) error {
record := SenderKeyRecord{ptr: senderKeyRecord.raw} distributionID := uuid.UUID(*(*[16]byte)(unsafe.Pointer(distributionIDBytes)))
record := SenderKeyRecord{ptr: (*C.SignalSenderKeyRecord)(unsafe.Pointer(senderKeyRecord))}
cloned, err := record.Clone() cloned, err := record.Clone()
if err != nil { if err != nil {
return err return err
} }
return store.StoreSenderKey(ctx, &Address{ptr: address.raw}, *(*uuid.UUID)(unsafe.Pointer(&distributionID)), cloned) return store.StoreSenderKey(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, distributionID, cloned)
}) })
} }
//export signal_destroy_sender_key_store_callback
func signal_destroy_sender_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct { func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct {
return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{ return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_sender_key: C.SignalFfiSenderKeyStoreLoadSenderKey(C.signal_load_sender_key_callback), load_sender_key: C.SignalLoadSenderKey(C.signal_load_sender_key_callback),
store_sender_key: C.SignalFfiSenderKeyStoreStoreSenderKey(C.signal_store_sender_key_callback), store_sender_key: C.SignalStoreSenderKey(C.signal_store_sender_key_callback),
destroy: C.SignalFfiSenderKeyStoreDestroy(C.signal_destroy_sender_key_store_callback),
}} }}
} }

View file

@ -31,6 +31,12 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
func init() {
if C.SignalUUID_LEN != 16 {
panic("libsignal-ffi uuid type size mismatch")
}
}
type ServiceIDType byte type ServiceIDType byte
const ( const (
@ -87,9 +93,6 @@ func (s ServiceID) IsEmpty() bool {
} }
func (s ServiceID) Address(deviceID uint) (*Address, error) { func (s ServiceID) Address(deviceID uint) (*Address, error) {
if s.IsEmpty() {
return nil, fmt.Errorf("cannot create address from empty ServiceID")
}
return newAddress(s.String(), deviceID) return newAddress(s.String(), deviceID)
} }
@ -115,19 +118,6 @@ func (s ServiceID) GoString() string {
return fmt.Sprintf(`libsignalgo.ServiceID{Type: %#v, UUID: uuid.MustParse("%s")}`, s.Type, s.UUID) return fmt.Sprintf(`libsignalgo.ServiceID{Type: %#v, UUID: uuid.MustParse("%s")}`, s.Type, s.UUID)
} }
func (s ServiceID) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s *ServiceID) UnmarshalText(text []byte) error {
parsed, err := ServiceIDFromString(string(text))
if err != nil {
return err
}
*s = parsed
return nil
}
func (s ServiceID) MarshalZerologObject(e *zerolog.Event) { func (s ServiceID) MarshalZerologObject(e *zerolog.Event) {
e.Stringer("type", s.Type) e.Stringer("type", s.Type)
e.Stringer("uuid", s.UUID) e.Stringer("uuid", s.UUID)
@ -161,18 +151,6 @@ func ServiceIDFromString(val string) (ServiceID, error) {
} }
} }
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) {
if len(bytes) == 16 {
return NewACIServiceID(uuid.UUID(bytes)), nil
} else if len(bytes) == 17 {
return ServiceID{
Type: ServiceIDType(bytes[0]),
UUID: uuid.UUID(bytes[1:]),
}, nil
}
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected 16 or 17)", len(bytes))
}
func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID { func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
var id ServiceID var id ServiceID
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID)) fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))

View file

@ -30,7 +30,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
) )
func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress, aliceAddress *libsignalgo.Address) { func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress *libsignalgo.Address) {
ctx := context.TODO() ctx := context.TODO()
bobPreKey, err := libsignalgo.GeneratePrivateKey() bobPreKey, err := libsignalgo.GeneratePrivateKey()
@ -86,7 +86,7 @@ func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtoc
assert.NoError(t, err) assert.NoError(t, err)
// Alice processes the bundle // Alice processes the bundle
err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceAddress, aliceStore, aliceStore) err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err) assert.NoError(t, err)
record, err := aliceStore.LoadSession(ctx, bobAddress) record, err := aliceStore.LoadSession(ctx, bobAddress)
@ -132,11 +132,11 @@ func TestSessionCipher(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore() aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore() bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress) initializeSessions(t, aliceStore, bobStore, bobAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9} alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceAddress, aliceStore, aliceStore) aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err) assert.NoError(t, err)
aliceCiphertextMessageType, err := aliceCiphertext.MessageType() aliceCiphertextMessageType, err := aliceCiphertext.MessageType()
assert.NoError(t, err) assert.NoError(t, err)
@ -147,13 +147,13 @@ func TestSessionCipher(t *testing.T) {
bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized) bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized)
assert.NoError(t, err) assert.NoError(t, err)
bobPlaintext, err := libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobAddress, bobStore, bobStore, bobStore, bobStore, bobStore) bobPlaintext, err := libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, alicePlaintext, bobPlaintext) assert.Equal(t, alicePlaintext, bobPlaintext)
bobPlaintext2 := []byte{23} bobPlaintext2 := []byte{23}
bobCiphertext2, err := libsignalgo.Encrypt(ctx, bobPlaintext2, aliceAddress, bobAddress, bobStore, bobStore) bobCiphertext2, err := libsignalgo.Encrypt(ctx, bobPlaintext2, aliceAddress, bobStore, bobStore)
assert.NoError(t, err) assert.NoError(t, err)
bobCiphertext2MessageType, err := bobCiphertext2.MessageType() bobCiphertext2MessageType, err := bobCiphertext2.MessageType()
assert.NoError(t, err) assert.NoError(t, err)
@ -163,7 +163,7 @@ func TestSessionCipher(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
aliceCiphertext2, err := libsignalgo.DeserializeMessage(bobCiphertext2Serialized) aliceCiphertext2, err := libsignalgo.DeserializeMessage(bobCiphertext2Serialized)
assert.NoError(t, err) assert.NoError(t, err)
alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceAddress, aliceStore, aliceStore) alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, bobPlaintext2, alicePlaintext2) assert.Equal(t, bobPlaintext2, alicePlaintext2)
} }
@ -183,11 +183,11 @@ func TestSessionCipherWithBadStore(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore() aliceStore := NewInMemorySignalProtocolStore()
bobStore := &BadInMemorySignalProtocolStore{NewInMemorySignalProtocolStore()} bobStore := &BadInMemorySignalProtocolStore{NewInMemorySignalProtocolStore()}
initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress, aliceAddress) initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9} alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceAddress, aliceStore, aliceStore) aliceCiphertext, err := libsignalgo.Encrypt(ctx, alicePlaintext, bobAddress, aliceStore, aliceStore)
assert.NoError(t, err) assert.NoError(t, err)
aliceCiphertextMessageType, err := aliceCiphertext.MessageType() aliceCiphertextMessageType, err := aliceCiphertext.MessageType()
assert.NoError(t, err) assert.NoError(t, err)
@ -198,7 +198,7 @@ func TestSessionCipherWithBadStore(t *testing.T) {
bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized) bobCiphertext, err := libsignalgo.DeserializePreKeyMessage(aliceCiphertextSerialized)
assert.NoError(t, err) assert.NoError(t, err)
t.Skip("This test is broken") // TODO fix t.Skip("This test is broken") // TODO fix
_, err = libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobAddress, bobStore, bobStore, bobStore, bobStore, bobStore) _, err = libsignalgo.DecryptPreKey(ctx, bobCiphertext, aliceAddress, bobStore, bobStore, bobStore, bobStore, bobStore)
require.Error(t, err) require.Error(t, err)
assert.Equal(t, "Test error", err.Error()) assert.Equal(t, "Test error", err.Error())
} }
@ -216,7 +216,7 @@ func TestSealedSenderEncrypt_Repeated(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore() aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore() bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress) initializeSessions(t, aliceStore, bobStore, bobAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair() trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err) assert.NoError(t, err)
@ -241,7 +241,7 @@ func TestSealedSenderEncrypt_Repeated(t *testing.T) {
}() }()
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
message := []byte(fmt.Sprintf("%04d vision", i)) message := []byte(fmt.Sprintf("%04d vision", i))
ciphertext, err := libsignalgo.SealedSenderEncryptPlaintext(ctx, message, libsignalgo.UnidentifiedSenderMessageContentHintDefault, bobAddress, aliceAddress, senderCert, aliceStore, aliceStore, nil) ciphertext, err := libsignalgo.SealedSenderEncryptPlaintext(ctx, message, libsignalgo.UnidentifiedSenderMessageContentHintDefault, bobAddress, senderCert, aliceStore, aliceStore)
require.NoError(t, err) require.NoError(t, err)
assert.NotNil(t, ciphertext) assert.NotNil(t, ciphertext)
} }
@ -252,18 +252,15 @@ func TestArchiveSession(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
setupLogging() setupLogging()
aliceACI := uuid.New()
bobACI := uuid.New() bobACI := uuid.New()
aliceAddress, err := libsignalgo.NewACIServiceID(aliceACI).Address(1)
assert.NoError(t, err)
bobAddress, err := libsignalgo.NewACIServiceID(bobACI).Address(1) bobAddress, err := libsignalgo.NewACIServiceID(bobACI).Address(1)
assert.NoError(t, err) assert.NoError(t, err)
aliceStore := NewInMemorySignalProtocolStore() aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore() bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress) initializeSessions(t, aliceStore, bobStore, bobAddress)
session, err := aliceStore.LoadSession(ctx, bobAddress) session, err := aliceStore.LoadSession(ctx, bobAddress)
assert.NoError(t, err) assert.NoError(t, err)
@ -318,7 +315,7 @@ func TestSealedSenderGroupCipher(t *testing.T) {
bobStore := NewInMemorySignalProtocolStore() bobStore := NewInMemorySignalProtocolStore()
initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress) initializeSessions(t, aliceStore, bobStore, bobAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair() trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -83,9 +83,6 @@ func (sr *SessionRecord) ArchiveCurrentState() error {
} }
func (sr *SessionRecord) CurrentRatchetKeyMatches(key *PublicKey) (bool, error) { func (sr *SessionRecord) CurrentRatchetKeyMatches(key *PublicKey) (bool, error) {
if sr == nil || key == nil {
return false, nil
}
var result C.bool var result C.bool
signalFfiError := C.signal_session_record_current_ratchet_key_matches( signalFfiError := C.signal_session_record_current_ratchet_key_matches(
&result, &result,

View file

@ -20,9 +20,11 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_session_callback(void *store_ctx, SignalMutPointerSessionRecord *recordp, SignalMutPointerProtocolAddress address); typedef const SignalSessionRecord const_session_record;
extern int signal_store_session_callback(void *store_ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record); typedef const SignalProtocolAddress const_address;
extern void signal_destroy_session_store_callback(void *store_ctx);
extern int signal_load_session_callback(void *store_ctx, SignalSessionRecord **recordp, const_address *address);
extern int signal_store_session_callback(void *store_ctx, const_address *address, const_session_record *record);
*/ */
import "C" import "C"
import ( import (
@ -36,39 +38,33 @@ type SessionStore interface {
} }
//export signal_load_session_callback //export signal_load_session_callback
func signal_load_session_callback(storeCtx unsafe.Pointer, recordp *C.SignalMutPointerSessionRecord, address C.SignalMutPointerProtocolAddress) C.int { func signal_load_session_callback(storeCtx unsafe.Pointer, recordp **C.SignalSessionRecord, address *C.const_address) C.int {
return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
record, err := store.LoadSession(ctx, &Address{ptr: address.raw}) record, err := store.LoadSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))})
if err == nil && record != nil { if err == nil && record != nil {
record.CancelFinalizer() record.CancelFinalizer()
recordp.raw = record.ptr *recordp = record.ptr
} }
return err return err
}) })
} }
//export signal_store_session_callback //export signal_store_session_callback
func signal_store_session_callback(storeCtx unsafe.Pointer, address C.SignalMutPointerProtocolAddress, sessionRecord C.SignalMutPointerSessionRecord) C.int { func signal_store_session_callback(storeCtx unsafe.Pointer, address *C.const_address, sessionRecord *C.const_session_record) C.int {
return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SessionStore, ctx context.Context) error {
record := SessionRecord{ptr: sessionRecord.raw} record := SessionRecord{ptr: (*C.SignalSessionRecord)(unsafe.Pointer(sessionRecord))}
cloned, err := record.Clone() cloned, err := record.Clone()
if err != nil { if err != nil {
return err return err
} }
return store.StoreSession(ctx, &Address{ptr: address.raw}, cloned) return store.StoreSession(ctx, &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}, cloned)
}) })
} }
//export signal_destroy_session_store_callback
func signal_destroy_session_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct { func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct {
return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{ return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_session: C.SignalFfiSessionStoreLoadSession(C.signal_load_session_callback), load_session: C.SignalLoadSession(C.signal_load_session_callback),
store_session: C.SignalFfiSessionStoreStoreSession(C.signal_store_session_callback), store_session: C.SignalStoreSession(C.signal_store_session_callback),
destroy: C.SignalFfiSessionStoreDestroy(C.signal_destroy_session_store_callback),
}} }}
} }

View file

@ -54,8 +54,6 @@ func (FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, message
func (FFILogger) Flush() {} func (FFILogger) Flush() {}
func (FFILogger) Destroy() {}
var loggingSetup = false var loggingSetup = false
func setupLogging() { func setupLogging() {

View file

@ -20,9 +20,10 @@ package libsignalgo
/* /*
#include "./libsignal-ffi.h" #include "./libsignal-ffi.h"
extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalMutPointerSignedPreKeyRecord *recordp, uint32_t id); typedef const SignalSignedPreKeyRecord const_signed_pre_key_record;
extern int signal_store_signed_pre_key_callback(void *store_ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
extern void signal_destroy_signed_pre_key_store_callback(void *store_ctx); extern int signal_load_signed_pre_key_callback(void *store_ctx, SignalSignedPreKeyRecord **recordp, uint32_t id);
extern int signal_store_signed_pre_key_callback(void *store_ctx, uint32_t id, const_signed_pre_key_record *record);
*/ */
import "C" import "C"
import ( import (
@ -36,21 +37,21 @@ type SignedPreKeyStore interface {
} }
//export signal_load_signed_pre_key_callback //export signal_load_signed_pre_key_callback
func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp *C.SignalMutPointerSignedPreKeyRecord, id C.uint32_t) C.int { func signal_load_signed_pre_key_callback(storeCtx unsafe.Pointer, keyp **C.SignalSignedPreKeyRecord, id C.uint32_t) C.int {
return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
key, err := store.LoadSignedPreKey(ctx, uint32(id)) key, err := store.LoadSignedPreKey(ctx, uint32(id))
if err == nil && key != nil { if err == nil && key != nil {
key.CancelFinalizer() key.CancelFinalizer()
keyp.raw = key.ptr *keyp = key.ptr
} }
return err return err
}) })
} }
//export signal_store_signed_pre_key_callback //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 { func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t, preKeyRecord *C.const_signed_pre_key_record) C.int {
return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error { return wrapStoreCallback(storeCtx, func(store SignedPreKeyStore, ctx context.Context) error {
record := SignedPreKeyRecord{ptr: preKeyRecord.raw} record := SignedPreKeyRecord{ptr: (*C.SignalSignedPreKeyRecord)(unsafe.Pointer(preKeyRecord))}
cloned, err := record.Clone() cloned, err := record.Clone()
if err != nil { if err != nil {
return err return err
@ -59,16 +60,10 @@ func signal_store_signed_pre_key_callback(storeCtx unsafe.Pointer, id C.uint32_t
}) })
} }
//export signal_destroy_signed_pre_key_store_callback
func signal_destroy_signed_pre_key_store_callback(storeCtx unsafe.Pointer) {
// No-op: Go's garbage collector handles cleanup
}
func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct { func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct {
return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{ return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{
ctx: wrapStore(ctx, store), ctx: wrapStore(ctx, store),
load_signed_pre_key: C.SignalFfiSignedPreKeyStoreLoadSignedPreKey(C.signal_load_signed_pre_key_callback), load_signed_pre_key: C.SignalLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
store_signed_pre_key: C.SignalFfiSignedPreKeyStoreStoreSignedPreKey(C.signal_store_signed_pre_key_callback), store_signed_pre_key: C.SignalStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
destroy: C.SignalFfiSignedPreKeyStoreDestroy(C.signal_destroy_signed_pre_key_store_callback),
}} }}
} }

View file

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
cd /data cd /data
export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER=""
apk add --no-cache git make cmake protobuf-dev musl-dev g++ clang-dev cbindgen apk add --no-cache git make cmake protoc musl-dev g++ clang-dev cbindgen
cd libsignal cd libsignal
cargo build -p libsignal-ffi --release cargo build -p libsignal-ffi --release
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h

View file

@ -2,4 +2,4 @@
package libsignalgo package libsignalgo
const Version = "v0.93.2" const Version = "v0.80.3"

View file

@ -44,6 +44,7 @@ func (mc *MessageConverter) ToSignal(
portal *bridgev2.Portal, portal *bridgev2.Portal,
evt *event.Event, evt *event.Event,
content *event.MessageEventContent, content *event.MessageEventContent,
timestamp uint64,
relaybotFormatted bool, relaybotFormatted bool,
replyTo *database.Message, replyTo *database.Message,
) (*signalpb.DataMessage, error) { ) (*signalpb.DataMessage, error) {
@ -54,6 +55,7 @@ func (mc *MessageConverter) ToSignal(
} }
dm := &signalpb.DataMessage{ dm := &signalpb.DataMessage{
Timestamp: &timestamp,
Preview: mc.convertURLPreviewToSignal(ctx, content), Preview: mc.convertURLPreviewToSignal(ctx, content),
} }
if replyTo != nil { if replyTo != nil {
@ -61,7 +63,7 @@ func (mc *MessageConverter) ToSignal(
if err == nil { if err == nil {
dm.Quote = &signalpb.DataMessage_Quote{ dm.Quote = &signalpb.DataMessage_Quote{
Id: proto.Uint64(messageID), Id: proto.Uint64(messageID),
AuthorAciBinary: authorACI[:], AuthorAci: proto.String(authorACI.String()),
Type: signalpb.DataMessage_Quote_NORMAL.Enum(), Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
} }
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments { if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
@ -110,9 +112,6 @@ func (mc *MessageConverter) ToSignal(
return nil, fmt.Errorf("failed to convert sticker: %w", err) return nil, fmt.Errorf("failed to convert sticker: %w", err)
} }
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS)) att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker)
if dm.Sticker == nil {
var emoji *string var emoji *string
// TODO check for single grapheme cluster? // TODO check for single grapheme cluster?
if len([]rune(content.Body)) == 1 { if len([]rune(content.Body)) == 1 {
@ -124,10 +123,10 @@ func (mc *MessageConverter) ToSignal(
PackId: make([]byte, 16), PackId: make([]byte, 16),
PackKey: make([]byte, 32), PackKey: make([]byte, 32),
StickerId: proto.Uint32(0), StickerId: proto.Uint32(0),
Data: att,
Emoji: emoji, Emoji: emoji,
} }
}
dm.Sticker.Data = att
case event.MsgLocation: case event.MsgLocation:
lat, lon, err := parseGeoURI(content.GeoURI) lat, lon, err := parseGeoURI(content.GeoURI)
if err != nil { if err != nil {

View file

@ -81,16 +81,6 @@ func BackupToDataMessage(ci *backuppb.ChatItem, attMap AttachmentMap) (*signalpb
Emoji: ti.StickerMessage.Sticker.Emoji, Emoji: ti.StickerMessage.Sticker.Emoji,
Data: backupToSignalAttachment(ti.StickerMessage.Sticker.Data, 0, uuid.New(), attMap), Data: backupToSignalAttachment(ti.StickerMessage.Sticker.Data, 0, uuid.New(), attMap),
} }
case *backuppb.ChatItem_Poll:
dm.PollCreate = &signalpb.DataMessage_PollCreate{
Question: &ti.Poll.Question,
AllowMultiple: &ti.Poll.AllowMultiple,
Options: exslices.CastFunc(ti.Poll.Options, func(from *backuppb.Poll_PollOption) string {
return from.Option
}),
}
// TODO handle votes
// TODO handle hasEnded somehow?
case *backuppb.ChatItem_RemoteDeletedMessage: case *backuppb.ChatItem_RemoteDeletedMessage:
// TODO handle some other way? (also disappeared view-once messages) // TODO handle some other way? (also disappeared view-once messages)
return nil, nil return nil, nil
@ -248,7 +238,11 @@ func backupToSignalBodyRange(from *backuppb.BodyRange) *signalpb.BodyRange {
out.Length = &from.Length out.Length = &from.Length
switch av := from.AssociatedValue.(type) { switch av := from.AssociatedValue.(type) {
case *backuppb.BodyRange_MentionAci: case *backuppb.BodyRange_MentionAci:
out.AssociatedValue = &signalpb.BodyRange_MentionAciBinary{MentionAciBinary: av.MentionAci} // TODO confirm this is correct
if len(av.MentionAci) != 16 {
return nil
}
out.AssociatedValue = &signalpb.BodyRange_MentionAci{MentionAci: uuid.UUID(av.MentionAci).String()}
case *backuppb.BodyRange_Style_: case *backuppb.BodyRange_Style_:
out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)} out.AssociatedValue = &signalpb.BodyRange_Style_{Style: signalpb.BodyRange_Style(av.Style)}
} }

View file

@ -22,10 +22,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os"
"strconv"
"strings" "strings"
"time" "time"
@ -54,7 +51,7 @@ func calculateLength(dm *signalpb.DataMessage) int {
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 { if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
return 1 return 1
} }
if dm.Sticker != nil || dm.PollVote != nil || dm.PollCreate != nil || dm.PollTerminate != nil { if dm.Sticker != nil {
return 1 return 1
} }
length := len(dm.Attachments) + len(dm.Contact) length := len(dm.Attachments) + len(dm.Contact)
@ -78,13 +75,11 @@ func CanConvertSignal(dm *signalpb.DataMessage) bool {
} }
const ViewOnceDisappearTimer = 5 * time.Minute const ViewOnceDisappearTimer = 5 * time.Minute
const matrixTextMaxLength = 30000 // approximate value to avoid hitting 64 KiB PDU size limit with HTML duplication
func (mc *MessageConverter) ToMatrix( func (mc *MessageConverter) ToMatrix(
ctx context.Context, ctx context.Context,
client *signalmeow.Client, client *signalmeow.Client,
portal *bridgev2.Portal, portal *bridgev2.Portal,
sender uuid.UUID,
intent bridgev2.MatrixAPI, intent bridgev2.MatrixAPI,
dm *signalpb.DataMessage, dm *signalpb.DataMessage,
attMap AttachmentMap, attMap AttachmentMap,
@ -113,20 +108,8 @@ func (mc *MessageConverter) ToMatrix(
// Don't allow any other parts in a sticker message // Don't allow any other parts in a sticker message
return cm return cm
} }
if dm.PollVote != nil {
cm.Parts = append(cm.Parts, mc.convertPollVoteToMatrix(ctx, dm.PollVote))
return cm
}
if dm.PollCreate != nil {
cm.Parts = append(cm.Parts, mc.convertPollCreateToMatrix(dm.PollCreate))
return cm
}
if dm.PollTerminate != nil {
cm.Parts = append(cm.Parts, mc.convertPollTerminateToMatrix(ctx, sender, dm.PollTerminate))
return cm
}
for i, att := range dm.GetAttachments() { for i, att := range dm.GetAttachments() {
if att.GetContentType() != "text/x-signal-plain" || att.GetSize() > matrixTextMaxLength { if att.GetContentType() != "text/x-signal-plain" {
cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att, attMap)) cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att, attMap))
} else { } else {
longBody, err := mc.downloadSignalLongText(ctx, att, attMap) longBody, err := mc.downloadSignalLongText(ctx, att, attMap)
@ -177,12 +160,9 @@ func (mc *MessageConverter) ToMatrix(
} }
} }
if dm.Quote != nil { if dm.Quote != nil {
authorACI, err := signalmeow.ParseStringOrBinaryUUID(dm.Quote.GetAuthorAci(), dm.Quote.GetAuthorAciBinary()) authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err). zerolog.Ctx(ctx).Err(err).Str("author_aci", dm.Quote.GetAuthorAci()).Msg("Failed to parse quote author ACI")
Str("author_aci", dm.Quote.GetAuthorAci()).
Hex("author_aci_binary", dm.Quote.GetAuthorAciBinary()).
Msg("Failed to parse quote author ACI")
} else { } else {
cm.ReplyTo = &networkid.MessageOptionalPartID{ cm.ReplyTo = &networkid.MessageOptionalPartID{
MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()), MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()),
@ -343,7 +323,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
card.Add(vcard.FieldTelephone, &field) card.Add(vcard.FieldTelephone, &field)
} }
if contact.GetAvatar().GetAvatar() != nil { if contact.GetAvatar().GetAvatar() != nil {
avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap, nil) avatarData, err := mc.downloadAttachment(ctx, contact.GetAvatar().GetAvatar(), attMap)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar") zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar")
} else { } else {
@ -461,28 +441,31 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
}, },
} }
} }
// Signal stickers are 512x512, so tell Matrix clients to render them as 200x200 to match Signal // Signal stickers are 512x512, so tell Matrix clients to render them as 256x256
// https://github.com/signalapp/Signal-Desktop/blob/v7.77.0-beta.1/ts/components/conversation/Message.dom.tsx#L135
if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 { if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 {
converted.Content.Info.Width = 200 converted.Content.Info.Width = 256
converted.Content.Info.Height = 200 converted.Content.Info.Height = 256
} }
converted.Content.Body = sticker.GetEmoji() converted.Content.Body = sticker.GetEmoji()
if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) {
converted.Content.Info.BridgedSticker = &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10),
Emoji: sticker.GetEmoji(),
PackURL: fmt.Sprintf(PackURLFormat, sticker.GetPackId(), sticker.GetPackKey()),
}
}
converted.Type = event.EventSticker converted.Type = event.EventSticker
converted.Content.MsgType = "" converted.Content.MsgType = ""
if converted.Extra == nil {
converted.Extra = map[string]any{}
}
// TODO fetch full pack metadata like the old bridge did?
converted.Extra["fi.mau.signal.sticker"] = map[string]any{
"id": sticker.GetStickerId(),
"emoji": sticker.GetEmoji(),
"pack": map[string]any{
"id": sticker.GetPackId(),
"key": sticker.GetPackKey(),
},
}
return converted return converted
} }
func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) { func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*string, error) {
data, err := mc.downloadAttachment(ctx, att, attMap, nil) data, err := mc.downloadAttachment(ctx, att, attMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -508,9 +491,7 @@ func checkIfAttachmentExists(att *signalpb.AttachmentPointer, attMap AttachmentM
return nil return nil
} }
func (mc *MessageConverter) downloadAttachment( func (mc *MessageConverter) downloadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) ([]byte, error) {
ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap, into *os.File,
) ([]byte, error) {
if err := checkIfAttachmentExists(att, attMap); err != nil { if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err return nil, err
} }
@ -521,19 +502,19 @@ func (mc *MessageConverter) downloadAttachment(
plaintextHash = target.GetPlaintextHash() plaintextHash = target.GetPlaintextHash()
} }
} }
return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash, into) return signalmeow.DownloadAttachmentWithPointer(ctx, att, plaintextHash)
} }
func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*bridgev2.ConvertedMessagePart, error) { func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer, attMap AttachmentMap) (*bridgev2.ConvertedMessagePart, error) {
fileName := att.GetFileName()
content := &event.MessageEventContent{ content := &event.MessageEventContent{
Body: att.GetFileName(),
Info: &event.FileInfo{ Info: &event.FileInfo{
MimeType: att.GetContentType(),
Width: int(att.GetWidth()), Width: int(att.GetWidth()),
Height: int(att.GetHeight()), Height: int(att.GetHeight()),
Size: int(att.GetSize()), Size: int(att.GetSize()),
}, },
} }
mimeType := att.GetContentType()
if err := checkIfAttachmentExists(att, attMap); err != nil { if err := checkIfAttachmentExists(att, attMap); err != nil {
return nil, err return nil, err
} else if mc.DirectMedia { } else if mc.DirectMedia {
@ -560,7 +541,25 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
} }
content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID) content.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
} else { } else {
err = mc.actuallyReuploadAttachment(ctx, content, att, attMap) data, err := mc.downloadAttachment(ctx, att, attMap)
if err != nil {
return nil, err
}
if mimeType == "" {
mimeType = http.DetectContentType(data)
}
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mimeType)
if err != nil {
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
}
fileName += ".ogg"
mimeType = "audio/ogg"
content.MSC3245Voice = &event.MSC3245Voice{}
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
//content.MSC1767Audio = &event.MSC1767Audio{}
}
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, fileName, mimeType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -569,7 +568,7 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
content.Info.Blurhash = att.GetBlurHash() content.Info.Blurhash = att.GetBlurHash()
content.Info.AnoaBlurhash = att.GetBlurHash() content.Info.AnoaBlurhash = att.GetBlurHash()
} }
switch strings.Split(content.Info.MimeType, "/")[0] { switch strings.Split(mimeType, "/")[0] {
case "image": case "image":
content.MsgType = event.MsgImage content.MsgType = event.MsgImage
case "video": case "video":
@ -591,8 +590,10 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
}, },
} }
} }
content.Body = fileName
content.Info.MimeType = mimeType
if content.Body == "" { if content.Body == "" {
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(content.Info.MimeType) content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
} }
return &bridgev2.ConvertedMessagePart{ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage, Type: event.EventMessage,
@ -600,186 +601,3 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
Extra: extra, Extra: extra,
}, nil }, nil
} }
func (mc *MessageConverter) actuallyReuploadAttachment(
ctx context.Context,
content *event.MessageEventContent,
att *signalpb.AttachmentPointer,
attMap AttachmentMap,
) (err error) {
convertVoice := att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported()
requireFile := convertVoice
content.URL, content.File, err = getIntent(ctx).UploadMediaStream(ctx, getPortal(ctx).MXID, int64(att.GetSize()), requireFile, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
osFile, ok := file.(*os.File)
inMemData, err := mc.downloadAttachment(ctx, att, attMap, osFile)
if err != nil {
return nil, err
} else if !ok {
if content.Info.MimeType == "" {
content.Info.MimeType = http.DetectContentType(inMemData)
}
_, err = file.Write(inMemData)
return &bridgev2.FileStreamResult{
FileName: content.Body,
MimeType: content.Info.MimeType,
}, err
}
if content.Info.MimeType == "" {
header := make([]byte, 512)
_, err = osFile.ReadAt(header, 0)
if err != nil {
return nil, fmt.Errorf("failed to read file header for MIME type detection: %w", err)
} else {
content.Info.MimeType = http.DetectContentType(header)
}
}
var replFile string
if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
replFile, err = ffmpeg.ConvertPath(ctx, osFile.Name(), ".ogg", []string{}, []string{"-c:a", "libopus"}, true)
if err != nil {
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
}
if content.Body == "" {
content.Body = "Voice message.ogg"
} else {
content.Body += ".ogg"
}
content.Info.MimeType = "audio/ogg"
content.MSC3245Voice = &event.MSC3245Voice{}
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
//content.MSC1767Audio = &event.MSC1767Audio{}
}
return &bridgev2.FileStreamResult{
ReplacementFile: replFile,
FileName: content.Body,
MimeType: content.Info.MimeType,
}, nil
})
return
}
func (mc *MessageConverter) convertPollCreateToMatrix(create *signalpb.DataMessage_PollCreate) *bridgev2.ConvertedMessagePart {
evtType := event.EventMessage
if mc.ExtEvPolls {
evtType = event.EventUnstablePollStart
}
maxChoices := 1
if create.GetAllowMultiple() {
maxChoices = len(create.GetOptions())
}
msc3381Answers := make([]map[string]any, len(create.GetOptions()))
optionsListText := make([]string, len(create.GetOptions()))
optionsListHTML := make([]string, len(create.GetOptions()))
for i, option := range create.GetOptions() {
msc3381Answers[i] = map[string]any{
"id": strconv.Itoa(i),
"org.matrix.msc1767.text": option,
}
optionsListText[i] = fmt.Sprintf("%d. %s\n", i+1, option)
optionsListHTML[i] = fmt.Sprintf("<li>%s</li>", event.TextToHTML(option))
}
body := fmt.Sprintf("%s\n\n%s\n\n(This message is a poll. Please open Signal to vote.)", create.GetQuestion(), strings.Join(optionsListText, "\n"))
formattedBody := fmt.Sprintf("<p>%s</p><ol>%s</ol><p>(This message is a poll. Please open Signal to vote.)</p>", event.TextToHTML(create.GetQuestion()), strings.Join(optionsListHTML, ""))
return &bridgev2.ConvertedMessagePart{
Type: evtType,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: body,
Format: event.FormatHTML,
FormattedBody: formattedBody,
},
Extra: map[string]any{
"fi.mau.signal.poll": map[string]any{
"question": create.GetQuestion(),
"allow_multiple": create.GetAllowMultiple(),
"options": create.GetOptions(),
},
"org.matrix.msc1767.message": []map[string]any{
{"mimetype": "text/html", "body": formattedBody},
{"mimetype": "text/plain", "body": body},
},
"org.matrix.msc3381.poll.start": map[string]any{
"kind": "org.matrix.msc3381.poll.disclosed",
"max_selections": maxChoices,
"question": map[string]any{
"org.matrix.msc1767.text": create.GetQuestion(),
},
"answers": msc3381Answers,
},
},
DBMetadata: nil,
DontBridge: false,
}
}
func (mc *MessageConverter) convertPollTerminateToMatrix(ctx context.Context, senderACI uuid.UUID, terminate *signalpb.DataMessage_PollTerminate) *bridgev2.ConvertedMessagePart {
pollMessageID := signalid.MakeMessageID(senderACI, terminate.GetTargetSentTimestamp())
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll terminate target message")
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollEnd,
Content: &event.MessageEventContent{},
DontBridge: true,
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollEnd,
Content: &event.MessageEventContent{
RelatesTo: &event.RelatesTo{
Type: event.RelReference,
EventID: pollMessage.MXID,
},
},
Extra: map[string]any{
"org.matrix.msc3381.poll.end": map[string]any{},
},
}
}
var invalidPollVote = &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollResponse,
Content: &event.MessageEventContent{},
DontBridge: true,
}
func (mc *MessageConverter) convertPollVoteToMatrix(ctx context.Context, vote *signalpb.DataMessage_PollVote) *bridgev2.ConvertedMessagePart {
if len(vote.GetTargetAuthorAciBinary()) != 16 {
zerolog.Ctx(ctx).Debug().
Str("author_aci_b64", base64.StdEncoding.EncodeToString(vote.GetTargetAuthorAciBinary())).
Msg("Invalid author ACI in poll vote")
return invalidPollVote
}
pollMessageID := signalid.MakeMessageID(uuid.UUID(vote.GetTargetAuthorAciBinary()), vote.GetTargetSentTimestamp())
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get poll vote target message")
return invalidPollVote
} else if pollMessage == nil {
zerolog.Ctx(ctx).Warn().Msg("Poll vote target message not found")
return invalidPollVote
}
mxOptionIDs := pollMessage.Metadata.(*signalid.MessageMetadata).MatrixPollOptionIDs
optionIDs := make([]string, len(vote.GetOptionIndexes()))
for i, optionIndex := range vote.GetOptionIndexes() {
if int(optionIndex) < len(mxOptionIDs) {
optionIDs[i] = mxOptionIDs[optionIndex]
} else {
optionIDs[i] = strconv.Itoa(int(optionIndex))
}
}
return &bridgev2.ConvertedMessagePart{
Type: event.EventUnstablePollResponse,
Content: &event.MessageEventContent{
RelatesTo: &event.RelatesTo{
Type: event.RelReference,
EventID: pollMessage.MXID,
},
},
Extra: map[string]any{
"org.matrix.msc3381.poll.response": map[string]any{
"answers": optionIDs,
},
},
}
}

View file

@ -1,199 +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 msgconv
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"
"go.mau.fi/util/emojishortcodes"
"google.golang.org/protobuf/proto"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
)
const StickerSourceID = "signal"
const PackURLFormat = "https://signal.art/addstickers/#pack_id=%x&pack_key=%x"
const PackIDLength = 16
const PackKeyLength = 32
const PackURLLength = len(PackURLFormat) - len("%x")*2 + PackIDLength*2 + PackKeyLength*2
var zeroPackID = make([]byte, PackIDLength)
func ParseStickerMeta(info *event.BridgedSticker) *signalpb.DataMessage_Sticker {
if info == nil || info.Network != StickerSourceID || len(info.PackURL) != PackURLLength {
return nil
}
stickerID, err := strconv.ParseUint(info.ID, 10, 32)
if err != nil {
return nil
}
packID, packKey, err := parsePackURL(info.PackURL)
if err != nil || len(packID) != PackIDLength || len(packKey) != PackKeyLength || bytes.Equal(packID, zeroPackID) {
return nil
}
return &signalpb.DataMessage_Sticker{
PackId: packID,
PackKey: packKey,
StickerId: proto.Uint32(uint32(stickerID)),
Emoji: &info.Emoji,
}
}
func parsePackURL(rawURL string) (packID, packKey []byte, err error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, nil, fmt.Errorf("invalid URL: %w", err)
} else if parsed.Host != "signal.art" || !strings.HasPrefix(parsed.Path, "/addstickers") {
return nil, nil, fmt.Errorf("invalid host or path in URL")
}
q, err := url.ParseQuery(parsed.Fragment)
if err != nil {
return nil, nil, fmt.Errorf("invalid URL fragment: %w", err)
}
packID, err = hex.DecodeString(q.Get("pack_id"))
if err != nil {
return nil, nil, fmt.Errorf("invalid pack ID in URL: %w", err)
}
packKey, err = hex.DecodeString(q.Get("pack_key"))
if err != nil {
return nil, nil, fmt.Errorf("invalid pack key in URL: %w", err)
}
return
}
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
packID, packKey, err := parsePackURL(url)
if err != nil {
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
}
manifest, err := signalmeow.DownloadStickerPackManifest(ctx, packID, packKey)
if err != nil {
return nil, fmt.Errorf("failed to download sticker pack manifest: %w", err)
}
topLevelExtra := map[string]any{
"fi.mau.signal.stickerpack": map[string]any{
"pack_id": hex.EncodeToString(packID),
"pack_key": hex.EncodeToString(packKey),
},
}
content := &event.ImagePackEventContent{
Images: make(map[string]*event.ImagePackImage, len(manifest.Stickers)),
Metadata: event.ImagePackMetadata{
DisplayName: manifest.GetTitle(),
AvatarURL: "",
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
Attribution: manifest.GetAuthor(),
BridgedPack: &event.BridgedStickerPack{
Network: StickerSourceID,
URL: fmt.Sprintf(PackURLFormat, packID, packKey),
},
},
}
imagesByID := make(map[uint32]id.ContentURIString, len(manifest.Stickers))
uploadImage := func(sticker *signalpb.Pack_Sticker) (id.ContentURIString, error) {
stickerID := sticker.GetId()
existing, ok := imagesByID[stickerID]
if ok {
return existing, nil
}
var mxc id.ContentURIString
if mc.DirectMedia {
mediaID, err := signalid.DirectMediaSticker{
PackID: packID,
PackKey: packKey,
StickerID: stickerID,
}.AsMediaID()
if err != nil {
return "", fmt.Errorf("failed to create media ID for sticker %d: %w", stickerID, err)
}
mxc, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
if err != nil {
return "", fmt.Errorf("failed to generate content URI for sticker %d: %w", stickerID, err)
}
} else {
dbKey := database.Key(fmt.Sprintf("stickercache:%x:%d", packID, stickerID))
if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
mxc = id.ContentURIString(cached)
imagesByID[stickerID] = mxc
return mxc, nil
}
data, err := signalmeow.DownloadStickerPackItem(ctx, packID, packKey, stickerID)
if err != nil {
return "", fmt.Errorf("failed to download sticker %d: %w", stickerID, err)
}
mxc, _, err = mc.Bridge.Bot.UploadMedia(ctx, "", data, "", sticker.GetContentType())
if err != nil {
return "", fmt.Errorf("failed to upload sticker %d: %w", stickerID, err)
}
mc.Bridge.DB.KV.Set(ctx, dbKey, string(mxc))
}
imagesByID[stickerID] = mxc
return mxc, nil
}
for _, sticker := range manifest.Stickers {
mxc, err := uploadImage(sticker)
if err != nil {
return nil, err
}
shortcode := emojishortcodes.Get(sticker.GetEmoji())
realShortcode := shortcode
i := 2
for _, alreadyExists := content.Images[realShortcode]; alreadyExists; i++ {
realShortcode = fmt.Sprintf("%s_%d", shortcode, i)
}
content.Images[realShortcode] = &event.ImagePackImage{
URL: mxc,
Body: sticker.GetEmoji(),
Info: &event.FileInfo{
MimeType: sticker.GetContentType(),
Width: 200,
Height: 200,
BridgedSticker: &event.BridgedSticker{
Network: StickerSourceID,
ID: strconv.FormatUint(uint64(sticker.GetId()), 10),
Emoji: sticker.GetEmoji(),
PackURL: content.Metadata.BridgedPack.URL,
},
},
}
}
if manifest.Cover != nil {
content.Metadata.AvatarURL, err = uploadImage(manifest.Cover)
if err != nil {
return nil, fmt.Errorf("failed to upload sticker pack cover: %w", err)
}
}
return &bridgev2.ImportedImagePack{
Content: content,
Extra: topLevelExtra,
Shortcode: hex.EncodeToString(packID),
}, nil
}

View file

@ -48,7 +48,6 @@ type MessageConverter struct {
LocationFormat string LocationFormat string
DisappearViewOnce bool DisappearViewOnce bool
DirectMedia bool DirectMedia bool
ExtEvPolls bool
} }
func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter { func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
@ -78,7 +77,7 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID { GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
parsed, ok := br.Matrix.ParseGhostMXID(userID) parsed, ok := br.Matrix.ParseGhostMXID(userID)
if ok { if ok {
u, _ := signalid.ParseUserID(parsed) u, _ := uuid.Parse(string(parsed))
return u return u
} }
user, _ := br.GetExistingUserByMXID(ctx, userID) user, _ := br.GetExistingUserByMXID(ctx, userID)
@ -86,7 +85,7 @@ func NewMessageConverter(br *bridgev2.Bridge) *MessageConverter {
if user != nil { if user != nil {
preferredLogin, _, _ := getPortal(ctx).FindPreferredLogin(ctx, user, true) preferredLogin, _, _ := getPortal(ctx).FindPreferredLogin(ctx, user, true)
if preferredLogin != nil { if preferredLogin != nil {
u, _ := signalid.ParseUserLoginID(preferredLogin.ID) u, _ := uuid.Parse(string(preferredLogin.ID))
return u return u
} }
} }

View file

@ -23,7 +23,6 @@ import (
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@ -86,27 +85,15 @@ func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, pa
Start: int(*r.Start), Start: int(*r.Start),
Length: int(*r.Length), Length: int(*r.Length),
}.TruncateEnd(maxLength) }.TruncateEnd(maxLength)
var mentionACI uuid.UUID
switch rv := r.GetAssociatedValue().(type) { switch rv := r.GetAssociatedValue().(type) {
case *signalpb.BodyRange_Style_: case *signalpb.BodyRange_Style_:
br.Value = Style(rv.Style) br.Value = Style(rv.Style)
case *signalpb.BodyRange_MentionAci: case *signalpb.BodyRange_MentionAci:
var err error parsed, err := uuid.Parse(rv.MentionAci)
mentionACI, err = uuid.Parse(rv.MentionAci)
if err != nil { if err != nil {
continue continue
} }
case *signalpb.BodyRange_MentionAciBinary: userInfo := params.GetUserInfo(ctx, parsed)
if len(rv.MentionAciBinary) != 16 {
continue
}
mentionACI = uuid.UUID(rv.MentionAciBinary)
default:
zerolog.Ctx(ctx).Warn().Type("value_type", rv).Msg("Unsupported body range type")
continue
}
if mentionACI != uuid.Nil {
userInfo := params.GetUserInfo(ctx, mentionACI)
if userInfo.MXID == "" { if userInfo.MXID == "" {
continue continue
} }
@ -115,7 +102,7 @@ func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, pa
// Maybe use NewUTF16String and do index replacements for the plaintext body too, // Maybe use NewUTF16String and do index replacements for the plaintext body too,
// or just replace the plaintext body by parsing the generated HTML. // or just replace the plaintext body by parsing the generated HTML.
content.Body = strings.Replace(content.Body, "\uFFFC", userInfo.Name, 1) content.Body = strings.Replace(content.Body, "\uFFFC", userInfo.Name, 1)
br.Value = Mention{UserInfo: userInfo, UUID: mentionACI} br.Value = Mention{UserInfo: userInfo, UUID: parsed}
} }
lrt.Add(br) lrt.Add(br)
} }

View file

@ -40,8 +40,8 @@ func (m Mention) String() string {
} }
func (m Mention) Proto() signalpb.BodyRangeAssociatedValue { func (m Mention) Proto() signalpb.BodyRangeAssociatedValue {
return &signalpb.BodyRange_MentionAciBinary{ return &signalpb.BodyRange_MentionAci{
MentionAciBinary: m.UUID[:], MentionAci: m.UUID.String(),
} }
} }

View file

@ -29,12 +29,10 @@ type PortalMetadata struct {
type MessageMetadata struct { type MessageMetadata struct {
ContainsAttachments bool `json:"contains_attachments,omitempty"` ContainsAttachments bool `json:"contains_attachments,omitempty"`
MatrixPollOptionIDs []string `json:"matrix_poll_option_ids,omitempty"`
} }
type UserLoginMetadata struct { type UserLoginMetadata struct {
ChatsSynced bool `json:"chats_synced,omitempty"` ChatsSynced bool `json:"chats_synced,omitempty"`
LastContactSync jsontime.UnixMilli `json:"last_contact_sync,omitempty"`
} }
type GhostMetadata struct { type GhostMetadata struct {

View file

@ -48,33 +48,19 @@ func ParseUserLoginID(userLoginID networkid.UserLoginID) (uuid.UUID, error) {
return userID, nil return userID, nil
} }
func toServiceID(id uuid.UUID, err error) (libsignalgo.ServiceID, error) { func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (uuid.UUID, error) {
if err != nil {
return libsignalgo.ServiceID{}, err
}
return libsignalgo.NewACIServiceID(id), nil
}
func ParseGhostOrUserLoginID(ghostOrUserLogin bridgev2.GhostOrUserLogin) (libsignalgo.ServiceID, error) {
switch ghostOrUserLogin := ghostOrUserLogin.(type) { switch ghostOrUserLogin := ghostOrUserLogin.(type) {
case *bridgev2.UserLogin: case *bridgev2.UserLogin:
return toServiceID(ParseUserLoginID(ghostOrUserLogin.ID)) return ParseUserLoginID(ghostOrUserLogin.ID)
case *bridgev2.Ghost: case *bridgev2.Ghost:
return ParseUserIDAsServiceID(ghostOrUserLogin.ID) return ParseUserID(ghostOrUserLogin.ID)
default: default:
return libsignalgo.ServiceID{}, fmt.Errorf("cannot parse ID: unknown type: %T", ghostOrUserLogin) return uuid.Nil, fmt.Errorf("cannot parse ID: unknown type: %T", ghostOrUserLogin)
} }
} }
const pniUserIDPrefix = "pni_"
const pniServiceIDPrefix = "PNI:"
func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) { func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) {
userIDStr := string(userID) return libsignalgo.ServiceIDFromString(string(userID))
if strings.HasPrefix(userIDStr, pniUserIDPrefix) {
userIDStr = pniServiceIDPrefix + userIDStr[len(pniUserIDPrefix):]
}
return libsignalgo.ServiceIDFromString(userIDStr)
} }
func ParsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) { func ParsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) {
@ -117,14 +103,7 @@ func MakeUserID(user uuid.UUID) networkid.UserID {
} }
func MakeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID { func MakeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID {
switch user.Type { return networkid.UserID(user.String())
case libsignalgo.ServiceIDTypeACI:
return MakeUserID(user.UUID)
case libsignalgo.ServiceIDTypePNI:
return networkid.UserID(pniUserIDPrefix + user.UUID.String())
default:
panic(fmt.Errorf("invalid service ID type %d", user.Type))
}
} }
func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID { func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID {

View file

@ -34,7 +34,6 @@ const (
directMediaTypeGroupAvatar directMediaType = 1 directMediaTypeGroupAvatar directMediaType = 1
directMediaTypeProfileAvatar directMediaType = 2 directMediaTypeProfileAvatar directMediaType = 2
directMediaTypePlaintextDigestAttachment directMediaType = 3 directMediaTypePlaintextDigestAttachment directMediaType = 3
directMediaTypeSticker directMediaType = 4
) )
type DirectMediaInfo interface { type DirectMediaInfo interface {
@ -45,7 +44,6 @@ var (
_ DirectMediaInfo = (*DirectMediaAttachment)(nil) _ DirectMediaInfo = (*DirectMediaAttachment)(nil)
_ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil) _ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil) _ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaSticker)(nil)
) )
type DirectMediaAttachment struct { type DirectMediaAttachment struct {
@ -129,30 +127,6 @@ func (m DirectMediaProfileAvatar) AsMediaID() (mediaID networkid.MediaID, err er
return networkid.MediaID(buf.Bytes()), nil return networkid.MediaID(buf.Bytes()), nil
} }
type DirectMediaSticker struct {
PackID []byte
PackKey []byte
StickerID uint32
}
const packIDLen = 16
const packKeyLen = 32
const directMediaStickerLen = 1 + packIDLen + packKeyLen + 4
func (m DirectMediaSticker) AsMediaID() (mediaID networkid.MediaID, err error) {
if len(m.PackID) != packIDLen {
return nil, fmt.Errorf("invalid pack ID length: %d", len(m.PackID))
} else if len(m.PackKey) != packKeyLen {
return nil, fmt.Errorf("invalid pack key length: %d", len(m.PackKey))
}
mediaID = make(networkid.MediaID, directMediaStickerLen)
mediaID[0] = byte(directMediaTypeSticker)
copy(mediaID[1:], m.PackID)
copy(mediaID[1+packIDLen:], m.PackKey)
binary.BigEndian.PutUint32(mediaID[1+packIDLen+packKeyLen:], m.StickerID)
return mediaID, nil
}
func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err error) { func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err error) {
mediaIDLen := len(mediaID) mediaIDLen := len(mediaID)
if mediaIDLen == 0 { if mediaIDLen == 0 {
@ -226,15 +200,6 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err err
info.ProfileAvatarPath = string(profileAvatarPath) info.ProfileAvatarPath = string(profileAvatarPath)
} }
return &info, nil return &info, nil
case directMediaTypeSticker:
var info DirectMediaSticker
if len(mediaID) != directMediaStickerLen {
return info, fmt.Errorf("invalid media ID length for sticker: %d", len(mediaID))
}
info.PackID = mediaID[1 : 1+packIDLen]
info.PackKey = mediaID[1+packIDLen : 1+packIDLen+packKeyLen]
info.StickerID = binary.BigEndian.Uint32(mediaID[1+packIDLen+packKeyLen:])
return &info, nil
} }
return nil, fmt.Errorf("invalid direct media type %d", mediaType) return nil, fmt.Errorf("invalid direct media type %d", mediaType)

View file

@ -31,11 +31,8 @@ import (
"math" "math"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/fallocate"
"go.mau.fi/util/pkcs7"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -62,54 +59,26 @@ var ErrInvalidMACForAttachment = errors.New("invalid MAC for attachment")
var ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment") var ErrInvalidDigestForAttachment = errors.New("invalid digest for attachment")
var ErrAttachmentNotFound = errors.New("attachment not found on server") var ErrAttachmentNotFound = errors.New("attachment not found on server")
func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte, into *os.File) ([]byte, error) { func DownloadAttachmentWithPointer(ctx context.Context, a *signalpb.AttachmentPointer, plaintextHash []byte) ([]byte, error) {
digest := a.GetDigest() digest := a.GetDigest()
plaintextDigest := false plaintextDigest := false
if digest == nil && plaintextHash != nil { if digest == nil && plaintextHash != nil {
digest = plaintextHash digest = plaintextHash
plaintextDigest = true plaintextDigest = true
} }
return DownloadAttachment( return DownloadAttachment(ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize())
ctx, a.GetCdnId(), a.GetCdnKey(), a.GetCdnNumber(), a.Key, digest, plaintextDigest, a.GetSize(), into,
)
} }
func DownloadAttachment( func DownloadAttachment(ctx context.Context, cdnID uint64, cdnKey string, cdnNumber uint32, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
ctx context.Context, path := getAttachmentPath(cdnID, cdnKey)
cdnID uint64, resp, err := web.GetAttachment(ctx, path, cdnNumber, nil)
cdnKey string,
cdnNumber uint32,
key, digest []byte,
plaintextDigest bool,
size uint32,
into *os.File,
) ([]byte, error) {
resp, err := web.GetAttachment(ctx, getAttachmentPath(cdnID, cdnKey), cdnNumber)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { bodyReader := resp.Body
_ = resp.Body.Close() defer bodyReader.Close()
}()
var body []byte body, err := io.ReadAll(bodyReader)
var downloadedSize int64
if resp.StatusCode > 400 {
body, err = io.ReadAll(io.LimitReader(resp.Body, 4096))
} else if into == nil {
if resp.ContentLength > 0 {
body = make([]byte, resp.ContentLength)
_, err = io.ReadFull(resp.Body, body)
} else {
body, err = io.ReadAll(http.MaxBytesReader(nil, resp.Body, max(int64(size), 32*1024)*2))
}
} else {
err = fallocate.Fallocate(into, int(resp.ContentLength))
if err != nil {
return nil, fmt.Errorf("failed to pre-allocate file for attachment: %w", err)
}
downloadedSize, err = io.Copy(into, resp.Body)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,27 +94,12 @@ func DownloadAttachment(
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode) return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
} }
if into != nil {
if _, err = into.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek attachment file after downloading: %w", err)
}
return nil, decryptAttachmentFile(into, downloadedSize, key, digest, plaintextDigest, size)
}
return decryptAttachment(body, key, digest, plaintextDigest, size) return decryptAttachment(body, key, digest, plaintextDigest, size)
} }
const MACLength = 32 const MACLength = 32
const IVLength = 16 const IVLength = 16
func macAndAESDecrypt(body, key []byte) ([]byte, error) {
l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
return aesDecrypt(key[:MACLength], body[:l])
}
func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) { func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
if !plaintextDigest { if !plaintextDigest {
hash := sha256.Sum256(body) hash := sha256.Sum256(body)
@ -153,7 +107,12 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return nil, ErrInvalidDigestForAttachment return nil, ErrInvalidDigestForAttachment
} }
} }
decrypted, err := macAndAESDecrypt(body, key) l := len(body) - MACLength
if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
return nil, ErrInvalidMACForAttachment
}
decrypted, err := aesDecrypt(key[:MACLength], body[:l])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,59 +129,6 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return decrypted, nil return decrypted, nil
} }
func decryptAttachmentFile(file *os.File, downloadedSize int64, key, digest []byte, plaintextDigest bool, size uint32) error {
if !plaintextDigest {
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return ErrInvalidDigestForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing: %w", err)
}
}
mac := make([]byte, MACLength)
n, err := file.ReadAt(mac, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to read MAC from attachment file: %w", err)
} else if n != MACLength {
return fmt.Errorf("unexpected MAC length read from attachment file: %d", n)
}
hasher := hmac.New(sha256.New, key[MACLength:])
_, err = io.CopyN(hasher, file, downloadedSize-MACLength)
if err != nil {
return fmt.Errorf("failed to hash attachment file for MAC verification: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), mac) {
return ErrInvalidMACForAttachment
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after verifying mac: %w", err)
}
decryptedSize, err := aesDecryptFile(key[:MACLength], file, downloadedSize-MACLength)
if err != nil {
return err
} else if decryptedSize < int64(size) {
return fmt.Errorf("decrypted attachment length %d < expected %d", decryptedSize, size)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after decrypting: %w", err)
}
err = file.Truncate(int64(size))
if err != nil {
return fmt.Errorf("failed to truncate attachment file to expected size: %w", err)
}
if plaintextDigest {
hasher = sha256.New()
if _, err = io.Copy(hasher, file); err != nil {
return fmt.Errorf("failed to hash decrypted attachment file: %w", err)
} else if !hmac.Equal(hasher.Sum(nil), digest) {
return fmt.Errorf("%w (plaintext hash)", ErrInvalidDigestForAttachment)
} else if _, err = file.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek attachment file after hashing plaintext: %w", err)
}
}
return nil
}
type attachmentV4UploadAttributes struct { type attachmentV4UploadAttributes struct {
Cdn uint32 `json:"cdn"` Cdn uint32 `json:"cdn"`
Key string `json:"key"` Key string `json:"key"`
@ -245,14 +151,6 @@ func extend(data []byte, paddedLen int) []byte {
} }
} }
func macAndAESEncrypt(keys, plaintext []byte) ([]byte, error) {
encrypted, err := aesEncrypt(keys[:32], plaintext)
if err != nil {
return nil, err
}
return appendMAC(keys[32:], encrypted), nil
}
func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) { func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger() log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
keys := random.Bytes(64) // combined AES and MAC keys keys := random.Bytes(64) // combined AES and MAC keys
@ -268,20 +166,23 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
} }
body = extend(body, paddedLen) body = extend(body, paddedLen)
encryptedWithMAC, err := macAndAESEncrypt(keys, body) encrypted, err := aesEncrypt(keys[:32], body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
encryptedWithMAC := appendMAC(keys[32:], encrypted)
// Get upload attributes from Signal server // Get upload attributes from Signal server
attributesPath := "/v4/attachments/form/upload" attributesPath := "/v4/attachments/form/upload"
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, attributesPath, nil, nil) username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, attributesPath, opts)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to request upload attributes") log.Err(err).Msg("Failed to request upload attributes")
return nil, fmt.Errorf("failed to request upload attributes: %w", err) return nil, fmt.Errorf("failed to request upload attributes: %w", err)
} }
var uploadAttributes attachmentV4UploadAttributes var uploadAttributes attachmentV4UploadAttributes
err = web.DecodeWSResponseBody(ctx, &uploadAttributes, resp) err = web.DecodeHTTPResponseBody(ctx, &uploadAttributes, resp)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to decode upload attributes") log.Err(err).Msg("Failed to decode upload attributes")
return nil, fmt.Errorf("failed to decode upload attributes: %w", err) return nil, fmt.Errorf("failed to decode upload attributes: %w", err)
@ -291,7 +192,7 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC) err = cli.uploadAttachmentTUS(ctx, uploadAttributes, encryptedWithMAC)
} else { } else {
log.Trace().Msg("Using legacy upload") log.Trace().Msg("Using legacy upload")
err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC) err = cli.uploadAttachmentLegacy(ctx, uploadAttributes, encryptedWithMAC, username, password)
} }
if err != nil { if err != nil {
log.Err(err).Msg("Failed to upload attachment") log.Err(err).Msg("Failed to upload attachment")
@ -317,17 +218,17 @@ func (cli *Client) uploadAttachmentLegacy(
ctx context.Context, ctx context.Context,
uploadAttributes attachmentV4UploadAttributes, uploadAttributes attachmentV4UploadAttributes,
encryptedWithMAC []byte, encryptedWithMAC []byte,
username string,
password string,
) error { ) error {
username, password := cli.Store.BasicAuthCreds()
// Allocate attachment on CDN // Allocate attachment on CDN
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{ resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation, OverrideURL: uploadAttributes.SignedUploadLocation,
ContentType: web.ContentTypeOctetStream, ContentType: web.ContentTypeOctetStream,
Headers: uploadAttributes.Headers, Headers: uploadAttributes.Headers,
Username: &username, Username: &username,
Password: &password, Password: &password,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
return fmt.Errorf("failed to send allocate request: %w", err) return fmt.Errorf("failed to send allocate request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 { } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -335,14 +236,13 @@ func (cli *Client) uploadAttachmentLegacy(
} }
// Upload attachment to CDN // Upload attachment to CDN
resp, err = web.SendHTTPRequest(ctx, "", http.MethodPut, "", &web.HTTPReqOpt{ resp, err = web.SendHTTPRequest(ctx, http.MethodPut, "", &web.HTTPReqOpt{
OverrideURL: resp.Header.Get("Location"), OverrideURL: resp.Header.Get("Location"),
Body: encryptedWithMAC, Body: encryptedWithMAC,
ContentType: web.ContentTypeOctetStream, ContentType: web.ContentTypeOctetStream,
Username: &username, Username: &username,
Password: &password, Password: &password,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
return fmt.Errorf("failed to send upload request: %w", err) return fmt.Errorf("failed to send upload request: %w", err)
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 { } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@ -360,13 +260,12 @@ func (cli *Client) uploadAttachmentTUS(
uploadAttributes.Headers["Upload-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC)) uploadAttributes.Headers["Upload-Length"] = fmt.Sprintf("%d", len(encryptedWithMAC))
uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key)) uploadAttributes.Headers["Upload-Metadata"] = "filename " + base64.StdEncoding.EncodeToString([]byte(uploadAttributes.Key))
resp, err := web.SendHTTPRequest(ctx, "", http.MethodPost, "", &web.HTTPReqOpt{ resp, err := web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
OverrideURL: uploadAttributes.SignedUploadLocation, OverrideURL: uploadAttributes.SignedUploadLocation,
Body: encryptedWithMAC, Body: encryptedWithMAC,
ContentType: web.ContentTypeOffsetOctetStream, ContentType: web.ContentTypeOffsetOctetStream,
Headers: uploadAttributes.Headers, Headers: uploadAttributes.Headers,
}) })
web.CloseBody(resp)
// TODO actually support resuming on error // TODO actually support resuming on error
if err != nil { if err != nil {
return fmt.Errorf("failed to send upload request: %w", err) return fmt.Errorf("failed to send upload request: %w", err)
@ -381,17 +280,12 @@ func (cli *Client) uploadAttachmentTUS(
return nil return nil
} }
func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier, groupMasterKey types.SerializedGroupMasterKey) (string, error) { func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier) (string, error) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
if groupMasterKey == "" { groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
var err error
groupMasterKey, err = cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get master key from group id") log.Err(err).Msg("Could not get master key from group id")
return "", err return "", err
} else if groupMasterKey == "" {
return "", fmt.Errorf("no master key found for group %s", gid)
}
} }
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey)) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil { if err != nil {
@ -411,15 +305,14 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
} }
// Get upload form from Signal server // Get upload form from Signal server
formPath := "/v2/groups/avatar/form" formPath := "/v1/groups/avatar/form"
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf} opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf, Host: web.StorageHostname}
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, formPath, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodGet, formPath, opts)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request fetching avatar upload form") log.Err(err).Msg("Error sending request fetching avatar upload form")
return "", err return "", err
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
web.CloseBody(resp)
if err != nil { if err != nil {
log.Err(err).Msg("Error decoding response body fetching upload attributes") log.Err(err).Msg("Error decoding response body fetching upload attributes")
return "", err return "", err
@ -445,11 +338,11 @@ func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gi
w.Close() w.Close()
// Upload avatar to CDN // Upload avatar to CDN
resp, err = web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodPost, "", &web.HTTPReqOpt{ resp, err = web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
Body: requestBody.Bytes(), Body: requestBody.Bytes(),
ContentType: web.ContentType(w.FormDataContentType()), ContentType: web.ContentType(w.FormDataContentType()),
Host: web.CDN1Hostname,
}) })
web.CloseBody(resp)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request uploading attachment") log.Err(err).Msg("Error sending request uploading attachment")
return "", err return "", err
@ -478,56 +371,14 @@ func aesDecrypt(key, ciphertext []byte) ([]byte, error) {
return nil, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", len(ciphertext)%aes.BlockSize) return nil, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", len(ciphertext)%aes.BlockSize)
} }
iv := ciphertext[:IVLength] iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[IVLength:]
mode := cipher.NewCBCDecrypter(block, iv) mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext) mode.CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext) pad := ciphertext[len(ciphertext)-1]
}
func aesDecryptFile(key []byte, file *os.File, downloadedSize int64) (int64, error) {
block, err := aes.NewCipher(key)
if err != nil {
return 0, err
}
fileReader := io.LimitReader(file, downloadedSize)
if downloadedSize%aes.BlockSize != 0 {
return 0, fmt.Errorf("ciphertext not multiple of AES blocksize (%d extra bytes)", downloadedSize%aes.BlockSize)
}
iv := make([]byte, IVLength)
n, err := fileReader.Read(iv)
if err != nil {
return 0, fmt.Errorf("failed to read IV from attachment file: %w", err)
} else if n != IVLength {
return 0, fmt.Errorf("unexpected IV length read from attachment file: %d", n)
}
mode := cipher.NewCBCDecrypter(block, iv)
buf := make([]byte, 4096)
var offset int64
var pad byte
for {
n, err = fileReader.Read(buf)
if err != nil && !errors.Is(err, io.EOF) {
return 0, fmt.Errorf("failed to read from attachment file: %w", err)
}
if n > 0 {
mode.CryptBlocks(buf[:n], buf[:n])
if _, err = file.WriteAt(buf[:n], offset); err != nil {
return 0, fmt.Errorf("failed to write decrypted data to attachment file: %w", err)
}
offset += int64(n)
pad = buf[n-1]
}
if errors.Is(err, io.EOF) {
break
}
}
if pad > aes.BlockSize { if pad > aes.BlockSize {
return 0, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize) return nil, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
} }
return downloadedSize - int64(pad), nil return ciphertext[aes.BlockSize : len(ciphertext)-int(pad)], nil
} }
func appendMAC(key, body []byte) []byte { func appendMAC(key, body []byte) []byte {
@ -542,11 +393,14 @@ func aesEncrypt(key, plaintext []byte) ([]byte, error) {
return nil, err return nil, err
} }
plaintext = pkcs7.Pad(plaintext, aes.BlockSize) pad := aes.BlockSize - len(plaintext)%aes.BlockSize
plaintext = append(plaintext, bytes.Repeat([]byte{byte(pad)}, pad)...)
ciphertext := make([]byte, len(plaintext))
iv := random.Bytes(16) iv := random.Bytes(16)
mode := cipher.NewCBCEncrypter(block, iv) mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(plaintext, plaintext) mode.CryptBlocks(ciphertext, plaintext)
return append(iv, plaintext...), nil return append(iv, ciphertext...), nil
} }

View file

@ -239,7 +239,7 @@ func (cli *Client) deriveTransferKeys() (aesKey, hmacKey [32]byte, err error) {
} }
func downloadTransferArchive(ctx context.Context, meta *TransferArchiveMetadata, writeTo io.Writer) error { func downloadTransferArchive(ctx context.Context, meta *TransferArchiveMetadata, writeTo io.Writer) error {
resp, err := web.GetAttachment(ctx, getAttachmentPath(0, meta.Key), meta.CDN) resp, err := web.GetAttachment(ctx, getAttachmentPath(0, meta.Key), meta.CDN, nil)
if err != nil { if err != nil {
return fmt.Errorf("failed to download transfer archive: %w", err) return fmt.Errorf("failed to download transfer archive: %w", err)
} }
@ -282,11 +282,7 @@ func (cli *Client) WaitForTransfer(ctx context.Context) (*TransferArchiveMetadat
} }
reqDuration := time.Since(reqStart) reqDuration := time.Since(reqStart)
if reqDuration < reqTimeout-10*time.Second { if reqDuration < reqTimeout-10*time.Second {
select { time.Sleep(15 * time.Second)
case <-time.After(15 * time.Second):
case <-ctx.Done():
return nil, ctx.Err()
}
} }
} }
} }
@ -295,14 +291,21 @@ func (cli *Client) tryRequestTransferArchive(ctx context.Context, timeout time.D
reqCtx, cancel := context.WithTimeout(ctx, timeout+15*time.Second) reqCtx, cancel := context.WithTimeout(ctx, timeout+15*time.Second)
defer cancel() defer cancel()
path := "/v1/devices/transfer_archive?timeout=" + strconv.Itoa(int(timeout.Seconds())) path := "/v1/devices/transfer_archive?timeout=" + strconv.Itoa(int(timeout.Seconds()))
resp, err := cli.AuthedWS.SendRequest(reqCtx, http.MethodGet, path, nil, nil) username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(reqCtx, http.MethodGet, path, opts)
defer func() {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}()
if err != nil { if err != nil {
return nil, err return nil, err
} else if resp.GetStatus() == http.StatusNoContent { } else if resp.StatusCode == http.StatusNoContent {
return nil, nil return nil, nil
} else if resp.GetStatus() != http.StatusOK { } else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.GetStatus()) return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
} else if err = json.Unmarshal(resp.Body, &respBody); err != nil { } else if err = json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err) return nil, fmt.Errorf("failed to decode response: %w", err)
} else { } else {
return respBody, nil return respBody, nil

View file

@ -18,21 +18,16 @@ package signalmeow
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exsync"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events" "go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store" "go.mau.fi/mautrix-signal/pkg/signalmeow/store"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web" "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
@ -40,14 +35,12 @@ type Client struct {
Store *store.Device Store *store.Device
Log zerolog.Logger Log zerolog.Logger
senderCertificateWithE164 *libsignalgo.SenderCertificate SenderCertificateWithE164 *libsignalgo.SenderCertificate
senderCertificateNoE164 *libsignalgo.SenderCertificate SenderCertificateNoE164 *libsignalgo.SenderCertificate
senderCertificateCache sync.Mutex GroupCredentials *GroupCredentials
sendCache *exsync.RingBuffer[sendCacheKey, *signalpb.Content]
GroupCache *GroupCache GroupCache *GroupCache
ProfileCache *ProfileCache ProfileCache *ProfileCache
GroupCallCache *map[string]bool
LastContactRequestTime time.Time LastContactRequestTime time.Time
SyncContactsOnConnect bool SyncContactsOnConnect bool
@ -71,26 +64,6 @@ type Client struct {
writeCallbackCounter chan time.Time writeCallbackCounter chan time.Time
} }
// InMemorySendCacheSize specifies how large the cache for sent messages is, which is used to respond to retry receipts.
// The cache is large because every group member will be listed separately.
// 2k entries should hold at least 2 messages in max size groups.
var InMemorySendCacheSize = 2048
func NewClient(device *store.Device, log zerolog.Logger, evtHandler func(events.SignalEvent) bool) *Client {
return &Client{
Store: device,
Log: log,
EventHandler: evtHandler,
GroupCache: NewGroupCache(device.ACIServiceID()),
ProfileCache: &ProfileCache{
profiles: make(map[string]*types.Profile),
errors: make(map[string]*error),
lastFetched: make(map[string]time.Time),
},
sendCache: exsync.NewRingBuffer[sendCacheKey, *signalpb.Content](InMemorySendCacheSize),
}
}
func (cli *Client) handleEvent(evt events.SignalEvent) bool { func (cli *Client) handleEvent(evt events.SignalEvent) bool {
return cli.EventHandler(evt) return cli.EventHandler(evt)
} }
@ -137,11 +110,3 @@ func (cli *Client) connectUnauthedWS(ctx context.Context) (chan web.SignalWebsoc
func (cli *Client) IsLoggedIn() bool { func (cli *Client) IsLoggedIn() bool {
return cli.Store != nil && cli.Store.IsDeviceLoggedIn() return cli.Store != nil && cli.Store.IsDeviceLoggedIn()
} }
func (cli *Client) GetRemoteConfig(ctx context.Context) (json.RawMessage, error) {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, "/v2/config", nil, nil)
if err != nil {
return nil, err
}
return resp.Body, web.DecodeWSResponseBody(ctx, nil, resp)
}

View file

@ -36,7 +36,7 @@ import (
) )
func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDetails *signalpb.ContactDetails, avatar *[]byte) (*types.Recipient, error) { func (cli *Client) StoreContactDetailsAsContact(ctx context.Context, contactDetails *signalpb.ContactDetails, avatar *[]byte) (*types.Recipient, error) {
parsedUUID, err := ParseStringOrBinaryUUID(contactDetails.GetAci(), contactDetails.GetAciBinary()) parsedUUID, err := uuid.Parse(contactDetails.GetAci())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -30,6 +30,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
func hmacSHA256(key, input []byte) []byte { func hmacSHA256(key, input []byte) []byte {
@ -62,12 +63,18 @@ func (cli *Client) updateDeviceName(ctx context.Context, encryptedName []byte) e
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal device name update request: %w", err) return fmt.Errorf("failed to marshal device name update request: %w", err)
} }
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/accounts/name", reqData, nil) username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, "/v1/accounts/name", &web.HTTPReqOpt{
Body: reqData,
Username: &username,
Password: &password,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to send device name update request: %w", err) return fmt.Errorf("failed to send device name update request: %w", err)
} }
if resp.GetStatus() < 200 || resp.GetStatus() >= 300 { defer resp.Body.Close()
return fmt.Errorf("device name update request returned status %d", resp.GetStatus()) if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("device name update request returned status %d", resp.StatusCode)
} }
return nil return nil
} }

View file

@ -36,7 +36,6 @@ func (*Call) isSignalEvent() {}
func (*ContactList) isSignalEvent() {} func (*ContactList) isSignalEvent() {}
func (*ACIFound) isSignalEvent() {} func (*ACIFound) isSignalEvent() {}
func (*DeleteForMe) isSignalEvent() {} func (*DeleteForMe) isSignalEvent() {}
func (*MessageRequestResponse) isSignalEvent() {}
func (*QueueEmpty) isSignalEvent() {} func (*QueueEmpty) isSignalEvent() {}
func (*LoggedOut) isSignalEvent() {} func (*LoggedOut) isSignalEvent() {}
@ -90,14 +89,6 @@ type DeleteForMe struct {
*signalpb.SyncMessage_DeleteForMe *signalpb.SyncMessage_DeleteForMe
} }
type MessageRequestResponse struct {
Timestamp uint64
ThreadACI uuid.UUID
GroupID *libsignalgo.GroupIdentifier
Type signalpb.SyncMessage_MessageRequestResponse_Type
Raw *signalpb.SyncMessage_MessageRequestResponse
}
type QueueEmpty struct{} type QueueEmpty struct{}
type LoggedOut struct{ Error error } type LoggedOut struct{ Error error }

View file

@ -1,346 +0,0 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2025 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 signalmeow
import (
"context"
"fmt"
"slices"
"sync"
"time"
"github.com/google/uuid"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
type SendEndorsementCache struct {
SendEndorsement libsignalgo.GroupSendEndorsement
MemberEndorsements map[libsignalgo.ServiceID]libsignalgo.GroupSendEndorsement
Expiration time.Time
SecretParams *libsignalgo.GroupSecretParams
}
func (sec *SendEndorsementCache) GetToken() (libsignalgo.GroupSendFullToken, error) {
return sec.GetTokenWith(sec.SendEndorsement)
}
func (sec *SendEndorsementCache) GetTokenWith(altToken libsignalgo.GroupSendEndorsement) (libsignalgo.GroupSendFullToken, error) {
return altToken.ToFullToken(sec.SecretParams, sec.Expiration)
}
type cachedGroup struct {
*Group
*SendEndorsementCache
FetchedAt time.Time
UpdatedAt time.Time
}
type GroupCache struct {
serviceID libsignalgo.ServiceID
credentials *GroupCredentials
credentialsLock sync.RWMutex
data map[types.GroupIdentifier]*cachedGroup
lock sync.RWMutex
activeCalls map[types.GroupIdentifier]string
callsLock sync.RWMutex
}
func NewGroupCache(serviceID libsignalgo.ServiceID) *GroupCache {
return &GroupCache{
serviceID: serviceID,
data: make(map[types.GroupIdentifier]*cachedGroup),
activeCalls: make(map[types.GroupIdentifier]string),
}
}
func (gc *GroupCache) GetCredentials(
ctx context.Context,
fetch func(context.Context, time.Time) (*GroupCredentials, error),
) (*GroupCredential, error) {
today := time.Now().Truncate(24 * time.Hour)
gc.credentialsLock.RLock()
cred := gc.getCachedCredentials(today.Unix())
gc.credentialsLock.RUnlock()
if cred != nil {
return cred, nil
}
gc.credentialsLock.Lock()
defer gc.credentialsLock.Unlock()
cred = gc.getCachedCredentials(today.Unix())
if cred != nil {
return cred, nil
}
creds, err := fetch(ctx, today)
if err != nil {
return nil, err
}
gc.credentials = creds
cred = gc.getCachedCredentials(today.Unix())
if cred == nil {
return nil, fmt.Errorf("no credentials for today after fetch")
}
return cred, nil
}
func (gc *GroupCache) getCachedCredentials(today int64) *GroupCredential {
if gc.credentials == nil {
return nil
}
for _, cred := range gc.credentials.Credentials {
if cred.RedemptionTime == today {
return &cred
}
}
return nil
}
func (gc *GroupCache) UpdateActiveCall(id types.GroupIdentifier, callID string) bool {
gc.callsLock.Lock()
defer gc.callsLock.Unlock()
currentCallID, ok := gc.activeCalls[id]
if ok {
// If we do, then this must be ending the call
if currentCallID == callID {
delete(gc.activeCalls, id)
return false
}
}
gc.activeCalls[id] = callID
return true
}
func (gc *GroupCache) Get(id types.GroupIdentifier) (*Group, *SendEndorsementCache, bool) {
gc.lock.RLock()
defer gc.lock.RUnlock()
c, ok := gc.data[id]
if !ok || time.Until(c.Expiration) < 5*time.Minute {
return nil, nil, false
}
return c.Group, c.SendEndorsementCache, true
}
func (gc *GroupCache) Delete(id types.GroupIdentifier) {
gc.lock.Lock()
defer gc.lock.Unlock()
delete(gc.data, id)
}
func (gc *GroupCache) Put(data *Group, endorsementResponse libsignalgo.GroupSendEndorsementsResponse) error {
gsp, err := masterKeyToBytes(data.GroupMasterKey).SecretParams()
if err != nil {
return fmt.Errorf("failed to get secret params: %w", err)
}
expiration, err := endorsementResponse.GetExpiration()
if err != nil {
return fmt.Errorf("failed to get endorsement expiration: %w", err)
}
endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs(data.getMemberServiceIDs(), gc.serviceID, &gsp, prodServerPublicParams)
if err != nil {
return fmt.Errorf("failed to receive endorsements: %w", err)
}
gc.lock.Lock()
defer gc.lock.Unlock()
cached, exists := gc.data[data.GroupIdentifier]
if exists && cached.Revision > data.Revision {
return nil
}
gc.data[data.GroupIdentifier] = &cachedGroup{
Group: data,
FetchedAt: time.Now(),
UpdatedAt: time.Now(),
SendEndorsementCache: &SendEndorsementCache{
Expiration: expiration,
SendEndorsement: endorsement,
MemberEndorsements: memberEndorsements,
SecretParams: &gsp,
},
}
return nil
}
func (gc *GroupCache) ApplyUpdate(change *GroupChange, endorsementResponse libsignalgo.GroupSendEndorsementsResponse) error {
mkBytes := masterKeyToBytes(change.GroupMasterKey)
rawGroupID, err := mkBytes.GroupIdentifier()
if err != nil {
return fmt.Errorf("failed to get group identifier: %w", err)
}
gsp, err := mkBytes.SecretParams()
if err != nil {
return fmt.Errorf("failed to get secret params: %w", err)
}
id := types.GroupIdentifier(rawGroupID.String())
gc.lock.Lock()
defer gc.lock.Unlock()
cached, exists := gc.data[id]
if !exists || cached.Revision >= change.Revision {
return nil
} else if cached.Revision < change.Revision-1 {
// We missed an update, evict
delete(gc.data, id)
return nil
}
// Pending member adds, promotes and removes
cached.PendingMembers = append(cached.PendingMembers, change.AddPendingMembers...)
for _, promo := range change.PromotePendingMembers {
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return p.ServiceID.Type == libsignalgo.ServiceIDTypeACI && p.ServiceID.UUID == promo.ACI
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: promo.ProfileKey,
Role: GroupMember_DEFAULT,
JoinedAtRevision: change.Revision,
})
}
for _, promo := range change.PromotePendingPniAciMembers {
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return (p.ServiceID.Type == libsignalgo.ServiceIDTypePNI && p.ServiceID.UUID == promo.PNI) ||
(p.ServiceID.Type == libsignalgo.ServiceIDTypeACI && p.ServiceID.UUID == promo.ACI)
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: promo.ProfileKey,
Role: GroupMember_DEFAULT,
JoinedAtRevision: change.Revision,
})
}
cached.PendingMembers = slices.DeleteFunc(cached.PendingMembers, func(p *PendingMember) bool {
return slices.ContainsFunc(change.DeletePendingMembers, func(s *libsignalgo.ServiceID) bool {
return s != nil && p.ServiceID == *s
})
})
// Requesting member adds, promotes and removes
cached.RequestingMembers = append(cached.RequestingMembers, change.AddRequestingMembers...)
for _, promo := range change.PromoteRequestingMembers {
var profileKey libsignalgo.ProfileKey
cached.RequestingMembers = slices.DeleteFunc(cached.RequestingMembers, func(r *RequestingMember) bool {
if r.ACI == promo.ACI {
profileKey = r.ProfileKey
return true
}
return false
})
cached.Members = append(cached.Members, &GroupMember{
ACI: promo.ACI,
ProfileKey: profileKey,
Role: promo.Role,
JoinedAtRevision: change.Revision,
})
}
cached.RequestingMembers = slices.DeleteFunc(cached.RequestingMembers, func(r *RequestingMember) bool {
return slices.ContainsFunc(change.DeleteRequestingMembers, func(u *uuid.UUID) bool {
return u != nil && r.ACI == *u
})
})
// Direct member adds, removes and modifications
for _, member := range change.AddMembers {
cached.Members = append(cached.Members, &GroupMember{
ACI: member.ACI,
Role: member.Role,
ProfileKey: member.ProfileKey,
JoinedAtRevision: member.JoinedAtRevision,
})
}
for _, rm := range change.ModifyMemberRoles {
cached.findMemberOrEmpty(rm.ACI).Role = rm.Role
}
for _, pk := range change.ModifyMemberProfileKeys {
cached.findMemberOrEmpty(pk.ACI).ProfileKey = pk.ProfileKey
}
cached.Members = slices.DeleteFunc(cached.Members, func(member *GroupMember) bool {
return slices.ContainsFunc(change.DeleteMembers, func(u *uuid.UUID) bool {
return u != nil && *u == member.ACI
})
})
// Banned members
cached.BannedMembers = append(cached.BannedMembers, change.AddBannedMembers...)
cached.BannedMembers = slices.DeleteFunc(cached.BannedMembers, func(b *BannedMember) bool {
return slices.ContainsFunc(change.DeleteBannedMembers, func(s *libsignalgo.ServiceID) bool {
return s != nil && b.ServiceID == *s
})
})
// Non-member modifications
if change.ModifyInviteLinkPassword != nil {
cached.InviteLinkPassword = change.ModifyInviteLinkPassword
}
if change.ModifyTitle != nil {
cached.Title = *change.ModifyTitle
}
if change.ModifyDescription != nil {
cached.Description = *change.ModifyDescription
}
if change.ModifyAvatar != nil {
cached.AvatarPath = *change.ModifyAvatar
}
if change.ModifyAnnouncementsOnly != nil {
cached.AnnouncementsOnly = *change.ModifyAnnouncementsOnly
}
if change.ModifyDisappearingMessagesDuration != nil {
cached.DisappearingMessagesDuration = *change.ModifyDisappearingMessagesDuration
}
if change.ModifyAttributesAccess != nil {
cached.AccessControl.Attributes = *change.ModifyAttributesAccess
}
if change.ModifyMemberAccess != nil {
cached.AccessControl.Members = *change.ModifyMemberAccess
}
if change.ModifyAddFromInviteLinkAccess != nil {
cached.AccessControl.AddFromInviteLink = *change.ModifyAddFromInviteLinkAccess
}
cached.UpdatedAt = time.Now()
cached.Revision = change.Revision
endorsement, memberEndorsements, err := endorsementResponse.ReceiveWithServiceIDs(
cached.getMemberServiceIDs(),
gc.serviceID,
&gsp,
prodServerPublicParams,
)
if err != nil {
delete(gc.data, id)
return fmt.Errorf("failed to receive endorsements: %w", err)
}
expiration, err := endorsementResponse.GetExpiration()
if err != nil {
delete(gc.data, id)
return fmt.Errorf("failed to get endorsement expiration: %w", err)
}
// TODO do these responses overwrite the entire thing?
cached.SendEndorsementCache = &SendEndorsementCache{
SendEndorsement: endorsement,
MemberEndorsements: memberEndorsements,
Expiration: expiration,
SecretParams: &gsp,
}
return nil
}

View file

@ -31,7 +31,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -91,12 +90,6 @@ type Group struct {
//PublicKey *libsignalgo.PublicKey //PublicKey *libsignalgo.PublicKey
} }
func (group *Group) getMemberServiceIDs() []libsignalgo.ServiceID {
return exslices.CastFunc(group.Members, func(from *GroupMember) libsignalgo.ServiceID {
return libsignalgo.NewACIServiceID(from.ACI)
})
}
func (group *Group) GetInviteLink() (string, error) { func (group *Group) GetInviteLink() (string, error) {
if group.InviteLinkPassword == nil { if group.InviteLinkPassword == nil {
return "", fmt.Errorf("no invite link password set") return "", fmt.Errorf("no invite link password set")
@ -106,8 +99,8 @@ func (group *Group) GetInviteLink() (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("couldn't decode invite link password") return "", fmt.Errorf("couldn't decode invite link password")
} }
inviteLinkContents := signalpb.GroupInviteLink_ContentsV1{ inviteLinkContents := signalpb.GroupInviteLink_V1Contents{
ContentsV1: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{ V1Contents: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
GroupMasterKey: masterKeyBytes[:], GroupMasterKey: masterKeyBytes[:],
InviteLinkPassword: inviteLinkPasswordBytes, InviteLinkPassword: inviteLinkPasswordBytes,
}, },
@ -121,15 +114,6 @@ func (group *Group) GetInviteLink() (string, error) {
return "https://signal.group/#" + inviteLinkPath, nil return "https://signal.group/#" + inviteLinkPath, nil
} }
func (group *Group) findMemberOrEmpty(aci uuid.UUID) *GroupMember {
for _, member := range group.Members {
if member.ACI == aci {
return member
}
}
return &GroupMember{}
}
type GroupAccessControl struct { type GroupAccessControl struct {
Members AccessControl Members AccessControl
AddFromInviteLink AccessControl AddFromInviteLink AccessControl
@ -226,7 +210,8 @@ func (groupChange *GroupChange) isEmpty() bool {
len(groupChange.PromoteRequestingMembers) == 0 && len(groupChange.PromoteRequestingMembers) == 0 &&
groupChange.ModifyDescription == nil && groupChange.ModifyDescription == nil &&
groupChange.ModifyAnnouncementsOnly == nil && groupChange.ModifyAnnouncementsOnly == nil &&
len(groupChange.AddBannedMembers) == 0 len(groupChange.AddBannedMembers) == 0 &&
len(groupChange.DeleteMembers) == 0
} }
func (groupChange *GroupChange) resolveConflict(group *Group) { func (groupChange *GroupChange) resolveConflict(group *Group) {
@ -329,7 +314,8 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
Logger() Logger()
sevenDaysOut := today.Add(7 * 24 * time.Hour) sevenDaysOut := today.Add(7 * 24 * time.Hour)
path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix()) path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix())
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, path, nil, nil) authRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
resp, err := cli.AuthedWS.SendRequest(ctx, authRequest)
if err != nil { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) return nil, fmt.Errorf("SendRequest error: %w", err)
} }
@ -343,22 +329,51 @@ func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*Gr
log.Err(err).Msg("json.Unmarshal error") log.Err(err).Msg("json.Unmarshal error")
return nil, err return nil, err
} }
// make sure pni matches device pni
if creds.PNI != cli.Store.PNI { if creds.PNI != cli.Store.PNI {
return nil, fmt.Errorf("mismatching PNI in group credentials: %s != %s", creds.PNI, cli.Store.PNI) err := fmt.Errorf("creds.PNI != d.PNI")
log.Err(err).Msg("creds.PNI != d.PNI")
return nil, err
} }
return &creds, nil return &creds, nil
} }
func (cli *Client) getCachedAuthorizationForToday(today time.Time) *GroupCredential {
if cli.GroupCredentials == nil {
// No cached credentials
return nil
}
allCreds := cli.GroupCredentials
// Get the credential for today
for _, cred := range allCreds.Credentials {
if cred.RedemptionTime == today.Unix() {
return &cred
}
}
return nil
}
func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) { func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) {
log := zerolog.Ctx(ctx).With(). log := zerolog.Ctx(ctx).With().
Str("action", "get authorization for today"). Str("action", "get authorization for today").
Logger() Logger()
// Timestamps for the start of today, and 7 days later
today := time.Now().Truncate(24 * time.Hour)
todayCred, err := cli.GroupCache.GetCredentials(ctx, cli.fetchNewGroupCreds) todayCred := cli.getCachedAuthorizationForToday(today)
if todayCred == nil {
creds, err := cli.fetchNewGroupCreds(ctx, today)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get group credentials: %w", err) return nil, fmt.Errorf("fetchNewGroupCreds error: %w", err)
}
cli.GroupCredentials = creds
todayCred = cli.getCachedAuthorizationForToday(today)
}
if todayCred == nil {
return nil, fmt.Errorf("couldn't get credential for today")
} }
//TODO: cache cred after unmarshalling
redemptionTime := uint64(todayCred.RedemptionTime) redemptionTime := uint64(todayCred.RedemptionTime)
credential := todayCred.Credential credential := todayCred.Credential
authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(credential) authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(credential)
@ -470,7 +485,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description) descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description)
if err == nil { if err == nil {
// treat a failure in obtaining the description as non-fatal // treat a failure in obtaining the description as non-fatal
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescriptionText()) decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescription())
} }
if encryptedGroup.DisappearingMessagesTimer != nil && len(encryptedGroup.DisappearingMessagesTimer) > 0 { if encryptedGroup.DisappearingMessagesTimer != nil && len(encryptedGroup.DisappearingMessagesTimer) > 0 {
@ -482,8 +497,8 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
} }
// These aren't encrypted // These aren't encrypted
decryptedGroup.AvatarPath = encryptedGroup.AvatarUrl decryptedGroup.AvatarPath = encryptedGroup.Avatar
decryptedGroup.Revision = encryptedGroup.Version decryptedGroup.Revision = encryptedGroup.Revision
// Decrypt members // Decrypt members
for _, member := range encryptedGroup.Members { for _, member := range encryptedGroup.Members {
@ -497,7 +512,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember) decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember)
} }
for _, pendingMember := range encryptedGroup.MembersPendingProfileKey { for _, pendingMember := range encryptedGroup.PendingMembers {
if pendingMember == nil { if pendingMember == nil {
continue continue
} }
@ -509,7 +524,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember) decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember)
} }
for _, requestingMember := range encryptedGroup.MembersPendingAdminApproval { for _, requestingMember := range encryptedGroup.RequestingMembers {
if requestingMember == nil { if requestingMember == nil {
continue continue
} }
@ -520,7 +535,7 @@ func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMast
decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember) decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember)
} }
for _, bannedMember := range encryptedGroup.MembersBanned { for _, bannedMember := range encryptedGroup.BannedMembers {
if bannedMember == nil { if bannedMember == nil {
continue continue
} }
@ -619,7 +634,7 @@ func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier
return nil, fmt.Errorf("failed to get group master key: %w", err) return nil, fmt.Errorf("failed to get group master key: %w", err)
} }
if groupMasterKey == "" { if groupMasterKey == "" {
return nil, fmt.Errorf("%w for %s", ErrGroupMasterKeyNotFound, gid) return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
} }
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey) return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
} }
@ -634,9 +649,9 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
Username: &groupAuth.Username, Username: &groupAuth.Username,
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Host: web.StorageHostname,
} }
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, "/v2/groups", opts) response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v2/groups", opts)
defer web.CloseBody(response)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -661,10 +676,6 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decrypt group: %w", err) return nil, fmt.Errorf("failed to decrypt group: %w", err)
} }
err = cli.GroupCache.Put(group, groupResponse.GroupSendEndorsementsResponse)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to cache group response")
}
// Store the profile keys in case they're new // Store the profile keys in case they're new
for _, member := range group.Members { for _, member := range group.Members {
@ -685,11 +696,11 @@ func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Respon
func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) { func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds() username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{ opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username, Username: &username,
Password: &password, Password: &password,
} }
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err) return nil, fmt.Errorf("failed to send request: %w", err)
} }
@ -708,21 +719,23 @@ func (cli *Client) DownloadGroupAvatar(ctx context.Context, avatarPath string, g
return decrypted, nil return decrypted, nil
} }
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, *SendEndorsementCache, error) { func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, error) {
cached, endorsement, ok := cli.GroupCache.Get(gid) cli.initGroupCache()
if ok && cached.Revision >= revision {
return cached, endorsement, nil lastFetched, ok := cli.GroupCache.lastFetched[gid]
if ok && time.Since(lastFetched) < 1*time.Hour {
group, ok := cli.GroupCache.groups[gid]
if ok && group.Revision >= revision {
return group, nil
}
} }
group, err := cli.fetchGroupByID(ctx, gid) group, err := cli.fetchGroupByID(ctx, gid)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
cached, endorsement, ok = cli.GroupCache.Get(gid) cli.GroupCache.groups[gid] = group
if !ok { cli.GroupCache.lastFetched[gid] = time.Now()
zerolog.Ctx(ctx).Warn().Msg("Group not found in cache after fetching") return group, nil
return group, nil, nil
}
return cached, endorsement, nil
} }
// We should store the group master key in the group store as soon as we see it, // We should store the group master key in the group store as soon as we see it,
@ -740,6 +753,40 @@ func (cli *Client) StoreMasterKey(ctx context.Context, groupMasterKey types.Seri
return groupIdentifier, nil return groupIdentifier, nil
} }
// We need to track active calls so we don't send too many IncomingSignalMessageCalls
// Of course for group calls Signal doesn't tell us *anything* so we're mostly just inferring
// So we just jam a new call ID in, and return true if we *think* this is a new incoming call
func (cli *Client) UpdateActiveCalls(gid types.GroupIdentifier, callID string) (isActive bool) {
cli.initGroupCache()
// Check to see if we currently have an active call for this group
currentCallID, ok := cli.GroupCache.activeCalls[gid]
if ok {
// If we do, then this must be ending the call
if currentCallID == callID {
delete(cli.GroupCache.activeCalls, gid)
return false
}
}
cli.GroupCache.activeCalls[gid] = callID
return true
}
func (cli *Client) initGroupCache() {
if cli.GroupCache == nil {
cli.GroupCache = &GroupCache{
groups: make(map[types.GroupIdentifier]*Group),
lastFetched: make(map[types.GroupIdentifier]time.Time),
activeCalls: make(map[types.GroupIdentifier]string),
}
}
}
type GroupCache struct {
groups map[types.GroupIdentifier]*Group
lastFetched map[types.GroupIdentifier]time.Time
activeCalls map[types.GroupIdentifier]string
}
func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) { func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) {
masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey) masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey)
groupMasterKey := masterKeyFromBytes(masterKeyBytes) groupMasterKey := masterKeyFromBytes(masterKeyBytes)
@ -757,15 +804,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger() log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger()
serverSignature := encryptedGroupChange.ServerSignature serverSignature := encryptedGroupChange.ServerSignature
encryptedActionsBytes := encryptedGroupChange.Actions encryptedActionsBytes := encryptedGroupChange.Actions
var success bool
defer func() {
if !success {
rawGroupID, _ := masterKeyToBytes(groupMasterKey).GroupIdentifier()
if rawGroupID != nil {
cli.GroupCache.Delete(types.GroupIdentifier(rawGroupID.String()))
}
}
}()
var err error var err error
if verifySignature { if verifySignature {
@ -788,14 +826,17 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceUserId)) sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId))
if err != nil { if err != nil {
log.Err(err).Msg("Couldn't decrypt source serviceID") log.Err(err).Msg("Couldn't decrypt source serviceID")
return nil, err return nil, err
} }
if sourceServiceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong serviceid kind: expected aci, got pni")
}
decryptedGroupChange := &GroupChange{ decryptedGroupChange := &GroupChange{
GroupMasterKey: groupMasterKey, GroupMasterKey: groupMasterKey,
Revision: encryptedActions.Version, Revision: encryptedActions.Revision,
SourceServiceID: sourceServiceID, SourceServiceID: sourceServiceID,
} }
@ -815,7 +856,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description) descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description)
if err == nil { if err == nil {
// treat a failure in obtaining the description as non-fatal // treat a failure in obtaining the description as non-fatal
newDescription := cleanupStringProperty(descriptionBlob.GetDescriptionText()) newDescription := cleanupStringProperty(descriptionBlob.GetDescription())
decryptedGroupChange.ModifyDescription = &newDescription decryptedGroupChange.ModifyDescription = &newDescription
} }
} }
@ -850,7 +891,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if serviceID.Type != libsignalgo.ServiceIDTypeACI { if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong ServiceID kind for delete member: expected ACI, got PNI") return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
} }
decryptedGroupChange.DeleteMembers = append(decryptedGroupChange.DeleteMembers, &serviceID.UUID) decryptedGroupChange.DeleteMembers = append(decryptedGroupChange.DeleteMembers, &serviceID.UUID)
} }
@ -863,7 +904,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if serviceID.Type != libsignalgo.ServiceIDTypeACI { if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong ServiceID kind for modify member: expected ACI, got PNI") return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
} }
decryptedGroupChange.ModifyMemberRoles = append(decryptedGroupChange.ModifyMemberRoles, &RoleMember{ decryptedGroupChange.ModifyMemberRoles = append(decryptedGroupChange.ModifyMemberRoles, &RoleMember{
ACI: serviceID.UUID, ACI: serviceID.UUID,
@ -890,7 +931,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, addPendingMember := range encryptedActions.AddMembersPendingProfileKey { for _, addPendingMember := range encryptedActions.AddPendingMembers {
if addPendingMember == nil { if addPendingMember == nil {
continue continue
} }
@ -903,7 +944,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember) decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember)
} }
for _, deletePendingMember := range encryptedActions.DeleteMembersPendingProfileKey { for _, deletePendingMember := range encryptedActions.DeletePendingMembers {
if deletePendingMember == nil { if deletePendingMember == nil {
continue continue
} }
@ -916,7 +957,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID) decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID)
} }
for _, promotePendingMember := range encryptedActions.PromoteMembersPendingProfileKey { for _, promotePendingMember := range encryptedActions.PromotePendingMembers {
if promotePendingMember == nil { if promotePendingMember == nil {
continue continue
} }
@ -935,7 +976,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, promotePendingPniAciMember := range encryptedActions.PromoteMembersPendingPniAciProfileKey { for _, promotePendingPniAciMember := range encryptedActions.PromotePendingPniAciMembers {
// TODO: pretending this is a PendingMember should do for mautrix-signal, but we probably want to treat them separately at some point // TODO: pretending this is a PendingMember should do for mautrix-signal, but we probably want to treat them separately at some point
if promotePendingPniAciMember == nil { if promotePendingPniAciMember == nil {
continue continue
@ -951,7 +992,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
return nil, err return nil, err
} }
if pniServiceID.Type != libsignalgo.ServiceIDTypePNI { if pniServiceID.Type != libsignalgo.ServiceIDTypePNI {
return nil, fmt.Errorf("wrong ServiceID kind for promote pending pni->aci: expected PNI, got ACI") return nil, fmt.Errorf("Wrong ServiceID kind: expected PNI, got ACI")
} }
decryptedGroupChange.PromotePendingPniAciMembers = append(decryptedGroupChange.PromotePendingPniAciMembers, &PromotePendingPniAciMember{ decryptedGroupChange.PromotePendingPniAciMembers = append(decryptedGroupChange.PromotePendingPniAciMembers, &PromotePendingPniAciMember{
ACI: *aci, ACI: *aci,
@ -965,7 +1006,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, addRequestingMember := range encryptedActions.AddMembersPendingAdminApproval { for _, addRequestingMember := range encryptedActions.AddRequestingMembers {
if addRequestingMember == nil { if addRequestingMember == nil {
continue continue
} }
@ -981,7 +1022,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
} }
} }
for _, deleteRequestingMember := range encryptedActions.DeleteMembersPendingAdminApproval { for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers {
if deleteRequestingMember == nil { if deleteRequestingMember == nil {
continue continue
} }
@ -994,7 +1035,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID) decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID)
} }
for _, promoteRequestingMember := range encryptedActions.PromoteMembersPendingAdminApproval { for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers {
if promoteRequestingMember == nil { if promoteRequestingMember == nil {
continue continue
} }
@ -1010,7 +1051,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}) })
} }
for _, addBannedMember := range encryptedActions.AddMembersBanned { for _, addBannedMember := range encryptedActions.AddBannedMembers {
if addBannedMember == nil { if addBannedMember == nil {
continue continue
} }
@ -1027,7 +1068,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
}) })
} }
for _, deleteBannedMember := range encryptedActions.DeleteMembersBanned { for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers {
if deleteBannedMember == nil { if deleteBannedMember == nil {
continue continue
} }
@ -1055,8 +1096,8 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
if encryptedActions.ModifyAnnouncementsOnly != nil { if encryptedActions.ModifyAnnouncementsOnly != nil {
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
} }
if encryptedActions.ModifyDisappearingMessageTimer != nil && len(encryptedActions.ModifyDisappearingMessageTimer.Timer) > 0 { if encryptedActions.ModifyDisappearingMessagesTimer != nil && len(encryptedActions.ModifyDisappearingMessagesTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessageTimer.Timer) timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessagesTimer.Timer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1068,12 +1109,6 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange
decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword
} }
success = true
err = cli.GroupCache.ApplyUpdate(decryptedGroupChange, nil)
if err != nil {
log.Err(err).Msg("Failed to apply group change to cache")
}
return decryptedGroupChange, nil return decryptedGroupChange, nil
} }
@ -1112,7 +1147,7 @@ func decryptPKeyAndIDorPresentation(ctx context.Context, userID []byte, profileK
return nil, nil, err return nil, nil, err
} }
if serviceID.Type == libsignalgo.ServiceIDTypePNI { if serviceID.Type == libsignalgo.ServiceIDTypePNI {
return nil, nil, fmt.Errorf("wrong serviceid kind for profile key: expected ACI, got PNI") return nil, nil, fmt.Errorf("wrong serviceid kind, expected ACI, got PNI")
} }
return &serviceID.UUID, profileKey, nil return &serviceID.UUID, profileKey, nil
@ -1127,11 +1162,11 @@ func decryptMember(ctx context.Context, member *signalpb.Member, groupSecretPara
ACI: *aci, ACI: *aci,
ProfileKey: *profileKey, ProfileKey: *profileKey,
Role: GroupMemberRole(member.Role), Role: GroupMemberRole(member.Role),
JoinedAtRevision: member.JoinedAtVersion, JoinedAtRevision: member.JoinedAtRevision,
}, nil }, nil
} }
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPendingProfileKey, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) { func decryptPendingMember(ctx context.Context, pendingMember *signalpb.PendingMember, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId) encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID) userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
@ -1154,7 +1189,7 @@ func decryptPendingMember(ctx context.Context, pendingMember *signalpb.MemberPen
}, nil }, nil
} }
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.MemberPendingAdminApproval, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) { func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.RequestingMember, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams) aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1175,7 +1210,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get groupSecretParams from master key") log.Err(err).Msg("Could not get groupSecretParams from master key")
return nil, err return nil, err
} }
groupChangeActions := &signalpb.GroupChange_Actions{Version: decryptedGroupChange.Revision} groupChangeActions := &signalpb.GroupChange_Actions{Revision: decryptedGroupChange.Revision}
if decryptedGroupChange.ModifyTitle != nil { if decryptedGroupChange.ModifyTitle != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}} attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}}
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob) encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
@ -1186,7 +1221,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle} groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle}
} }
if decryptedGroupChange.ModifyDescription != nil { if decryptedGroupChange.ModifyDescription != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: *decryptedGroupChange.ModifyDescription}} attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: *decryptedGroupChange.ModifyDescription}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob) encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get encrypt description") log.Err(err).Msg("Could not get encrypt description")
@ -1208,7 +1243,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
JoinFromInviteLink: addMember.JoinFromInviteLink, JoinFromInviteLink: addMember.JoinFromInviteLink,
}) })
} else { } else {
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{ groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember, Added: encryptedPendingMember,
}) })
} }
@ -1240,7 +1275,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Failed to encrypt pendingMember") log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersPendingProfileKey = append(groupChangeActions.AddMembersPendingProfileKey, &signalpb.GroupChange_Actions_AddMemberPendingProfileKeyAction{ groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember, Added: encryptedPendingMember,
}) })
} }
@ -1250,7 +1285,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deletePendingMember") log.Err(err).Msg("Encrypt UserId error for deletePendingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersPendingProfileKey = append(groupChangeActions.DeleteMembersPendingProfileKey, &signalpb.GroupChange_Actions_DeleteMemberPendingProfileKeyAction{ groupChangeActions.DeletePendingMembers = append(groupChangeActions.DeletePendingMembers, &signalpb.GroupChange_Actions_DeletePendingMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1268,7 +1303,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember") log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err return nil, err
} }
groupChangeActions.PromoteMembersPendingProfileKey = append(groupChangeActions.PromoteMembersPendingProfileKey, &signalpb.GroupChange_Actions_PromoteMemberPendingProfileKeyAction{ groupChangeActions.PromotePendingMembers = append(groupChangeActions.PromotePendingMembers, &signalpb.GroupChange_Actions_PromotePendingMemberAction{
Presentation: *presentation, Presentation: *presentation,
}) })
} }
@ -1286,8 +1321,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember") log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersPendingAdminApproval = append(groupChangeActions.AddMembersPendingAdminApproval, &signalpb.GroupChange_Actions_AddMemberPendingAdminApprovalAction{ groupChangeActions.AddRequestingMembers = append(groupChangeActions.AddRequestingMembers, &signalpb.GroupChange_Actions_AddRequestingMemberAction{
Added: &signalpb.MemberPendingAdminApproval{ Added: &signalpb.RequestingMember{
Presentation: *presentation, Presentation: *presentation,
}, },
}) })
@ -1298,7 +1333,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember") log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersPendingAdminApproval = append(groupChangeActions.DeleteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_DeleteMemberPendingAdminApprovalAction{ groupChangeActions.DeleteRequestingMembers = append(groupChangeActions.DeleteRequestingMembers, &signalpb.GroupChange_Actions_DeleteRequestingMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1309,7 +1344,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return nil, err return nil, err
} }
groupChangeActions.PromoteMembersPendingAdminApproval = append(groupChangeActions.PromoteMembersPendingAdminApproval, &signalpb.GroupChange_Actions_PromoteMemberPendingAdminApprovalAction{ groupChangeActions.PromoteRequestingMembers = append(groupChangeActions.PromoteRequestingMembers, &signalpb.GroupChange_Actions_PromoteRequestingMemberAction{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
Role: signalpb.Member_Role(promoteRequestingMember.Role), Role: signalpb.Member_Role(promoteRequestingMember.Role),
}) })
@ -1320,8 +1355,8 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember") log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.AddMembersBanned = append(groupChangeActions.AddMembersBanned, &signalpb.GroupChange_Actions_AddMemberBannedAction{ groupChangeActions.AddBannedMembers = append(groupChangeActions.AddBannedMembers, &signalpb.GroupChange_Actions_AddBannedMemberAction{
Added: &signalpb.MemberBanned{ Added: &signalpb.BannedMember{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
Timestamp: addBannedMember.Timestamp, Timestamp: addBannedMember.Timestamp,
}, },
@ -1333,7 +1368,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember") log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err return nil, err
} }
groupChangeActions.DeleteMembersBanned = append(groupChangeActions.DeleteMembersBanned, &signalpb.GroupChange_Actions_DeleteMemberBannedAction{ groupChangeActions.DeleteBannedMembers = append(groupChangeActions.DeleteBannedMembers, &signalpb.GroupChange_Actions_DeleteBannedMemberAction{
DeletedUserId: encryptedUserID[:], DeletedUserId: encryptedUserID[:],
}) })
} }
@ -1364,7 +1399,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
log.Err(err).Msg("Could not get encrypt Title") log.Err(err).Msg("Could not get encrypt Title")
return nil, err return nil, err
} }
groupChangeActions.ModifyDisappearingMessageTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessageTimerAction{Timer: *encryptedTimer} groupChangeActions.ModifyDisappearingMessagesTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessagesTimerAction{Timer: *encryptedTimer}
} }
if decryptedGroupChange.ModifyInviteLinkPassword != nil { if decryptedGroupChange.ModifyInviteLinkPassword != nil {
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword) inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword)
@ -1379,7 +1414,7 @@ func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroup
return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil) return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil)
} }
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.MemberPendingProfileKey, error) { func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI) expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI)
if err != nil { if err != nil {
@ -1407,7 +1442,7 @@ func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, group
return &encryptedMember, nil, nil return &encryptedMember, nil, nil
} }
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.MemberPendingProfileKey, error) { func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID) encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID)
if err != nil { if err != nil {
@ -1419,7 +1454,7 @@ func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *Pend
log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember") log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember")
return nil, err return nil, err
} }
encryptedPendingMember := signalpb.MemberPendingProfileKey{ encryptedPendingMember := signalpb.PendingMember{
AddedByUserId: encryptedAddedByUserID[:], AddedByUserId: encryptedAddedByUserID[:],
Member: &signalpb.Member{ Member: &signalpb.Member{
UserId: encryptedUserID[:], UserId: encryptedUserID[:],
@ -1472,9 +1507,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Body: requestBody, Body: requestBody,
Host: web.StorageHostname,
} }
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPatch, path, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodPatch, path, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) return nil, fmt.Errorf("SendRequest error: %w", err)
} }
@ -1513,21 +1548,17 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
return &changeResp, nil return &changeResp, nil
} }
var ErrGroupMasterKeyNotFound = errors.New("group master key not found in store")
func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) { func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger() log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid) groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to get master key for group: %w", err) return 0, fmt.Errorf("failed to get master key for group: %w", err)
} else if groupMasterKey == "" {
return 0, ErrGroupMasterKeyNotFound
} }
groupChange.GroupMasterKey = groupMasterKey groupChange.GroupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChangeResponse var signedGroupChange *signalpb.GroupChangeResponse
group, _, err := cli.RetrieveGroupByID(ctx, gid, 0) group, err := cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch group info to update: %w", err) return 0, fmt.Errorf("failed to fetch group info to update: %w", err)
} }
@ -1550,8 +1581,10 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("failed to update group: %w", err) return 0, fmt.Errorf("failed to update group: %w", err)
} }
} else if errors.Is(err, ConflictError) { } else if errors.Is(err, ConflictError) {
cli.GroupCache.Delete(gid) delete(cli.GroupCache.groups, gid)
group, _, err = cli.RetrieveGroupByID(ctx, gid, 0) delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
group, err = cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to fetch group after conflict: %w", err) return 0, fmt.Errorf("failed to fetch group after conflict: %w", err)
} }
@ -1564,13 +1597,12 @@ func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gi
return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err) return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err)
} }
} }
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
if signedGroupChange == nil { if signedGroupChange == nil {
return 0, fmt.Errorf("no signed group change returned: %w", err) return 0, fmt.Errorf("no signed group change returned: %w", err)
} }
err = cli.GroupCache.ApplyUpdate(groupChange, signedGroupChange.GroupSendEndorsementsResponse)
if err != nil {
log.Err(err).Msg("Failed to apply group change to cache")
}
groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange) groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to marshal signed group change: %w", err) return 0, fmt.Errorf("failed to marshal signed group change: %w", err)
@ -1603,12 +1635,12 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
encryptedGroup := &signalpb.Group{ encryptedGroup := &signalpb.Group{
PublicKey: groupPublicParams[:], PublicKey: groupPublicParams[:],
Title: *encryptedTitle, Title: *encryptedTitle,
AvatarUrl: decryptedGroup.AvatarPath, Avatar: decryptedGroup.AvatarPath,
AnnouncementsOnly: decryptedGroup.AnnouncementsOnly, AnnouncementsOnly: decryptedGroup.AnnouncementsOnly,
Version: 0, Revision: 0,
} }
if decryptedGroup.Description != "" { if decryptedGroup.Description != "" {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DescriptionText{DescriptionText: decryptedGroup.Description}} attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: decryptedGroup.Description}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob) encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get encrypt Description") log.Err(err).Msg("Could not get encrypt Description")
@ -1634,7 +1666,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
if encryptedMember != nil { if encryptedMember != nil {
encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember) encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember)
} else { } else {
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember) encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
} }
} }
for _, pendingMember := range decryptedGroup.PendingMembers { for _, pendingMember := range decryptedGroup.PendingMembers {
@ -1643,7 +1675,7 @@ func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, grou
log.Err(err).Msg("Failed to encrypt pendingMember") log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err return nil, err
} }
encryptedGroup.MembersPendingProfileKey = append(encryptedGroup.MembersPendingProfileKey, encryptedPendingMember) encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
} }
return encryptedGroup, nil return encryptedGroup, nil
} }
@ -1666,7 +1698,7 @@ func PrepareGroupCreation(decryptedGroup *Group) (libsignalgo.GroupMasterKey, er
return masterKeyBytes, nil return masterKeyBytes, nil
} }
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group) (*Group, error) { func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger() log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger()
masterKeyBytes, err := PrepareGroupCreation(decryptedGroup) masterKeyBytes, err := PrepareGroupCreation(decryptedGroup)
if err != nil { if err != nil {
@ -1681,6 +1713,14 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error") log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err return nil, err
} }
if len(avatarBytes) > 0 {
avatarPath, err := cli.UploadGroupAvatar(ctx, avatarBytes, decryptedGroup.GroupIdentifier)
if err != nil {
log.Err(err).Msg("Failed to upload group avatar")
return nil, err
}
decryptedGroup.AvatarPath = avatarPath
}
encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams) encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to encrypt group") log.Err(err).Msg("Failed to encrypt group")
@ -1702,9 +1742,9 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Body: requestBody, Body: requestBody,
Host: web.StorageHostname,
} }
resp, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodPut, path, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodPut, path, opts)
defer web.CloseBody(resp)
if err != nil { if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err) return nil, fmt.Errorf("SendRequest error: %w", err)
} }
@ -1731,9 +1771,9 @@ func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
return InviteLinkPasswordFromBytes(random.Bytes(16)) return InviteLinkPasswordFromBytes(random.Bytes(16))
} }
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group) (*Group, error) { func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroup").Logger() log := zerolog.Ctx(ctx).With().Str("action", "CreateGroup").Logger()
group, err := cli.createGroupOnServer(ctx, decryptedGroup) group, err := cli.createGroupOnServer(ctx, decryptedGroup, avatarBytes)
if err != nil { if err != nil {
log.Err(err).Msg("Error creating group on server") log.Err(err).Msg("Error creating group on server")
return nil, err return nil, err
@ -1756,7 +1796,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
return nil, err return nil, err
} }
if groupMasterKey == "" { if groupMasterKey == "" {
return nil, ErrGroupMasterKeyNotFound return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
} }
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
@ -1767,15 +1807,11 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
Username: &groupAuth.Username, Username: &groupAuth.Username,
Password: &groupAuth.Password, Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Headers: map[string]string{ Host: web.StorageHostname,
// TODO actually cache the data and provide real expiry timestamp
"Cached-Send-Endorsements": "0",
},
} }
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false // 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, 5, includeFirstState)
response, err := web.SendHTTPRequest(ctx, web.StorageHostname, http.MethodGet, path, opts) response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts)
defer web.CloseBody(response)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -22,7 +22,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math/rand/v2" "math/rand"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -43,6 +43,25 @@ type GeneratedPreKeys struct {
IdentityKey []uint8 IdentityKey []uint8
} }
func (cli *Client) GenerateAndRegisterPreKeys(ctx context.Context, pks store.PreKeyStore) error {
_, err := cli.GenerateAndSaveNextPreKeyBatch(ctx, pks, 0)
if err != nil {
return fmt.Errorf("failed to generate and save next prekey batch: %w", err)
}
_, err = cli.GenerateAndSaveNextKyberPreKeyBatch(ctx, pks, 0)
if err != nil {
return fmt.Errorf("failed to generate and save next kyber prekey batch: %w", err)
}
// We need to upload all currently valid prekeys, not just the ones we just generated
err = cli.RegisterAllPreKeys(ctx, pks)
if err != nil {
return fmt.Errorf("failed to register prekey batches: %w", err)
}
return err
}
func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore) error { func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore) error {
var identityKeyPair *libsignalgo.IdentityKeyPair var identityKeyPair *libsignalgo.IdentityKeyPair
var pni bool var pni bool
@ -78,11 +97,10 @@ func (cli *Client) RegisterAllPreKeys(ctx context.Context, pks store.PreKeyStore
KyberPreKeys: kyberPreKeys, KyberPreKeys: kyberPreKeys,
IdentityKey: identityKey, IdentityKey: identityKey,
} }
zerolog.Ctx(ctx).Debug(). preKeyUsername := fmt.Sprintf("%s.%d", cli.Store.ACI, cli.Store.DeviceID)
Int("num_prekeys", len(preKeys)). log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
Int("num_kyber_prekeys", len(kyberPreKeys)). log.Debug().Int("num_prekeys", len(preKeys)).Int("num_kyber_prekeys", len(kyberPreKeys)).Msg("Registering prekeys")
Msg("Registering all prekeys") err = RegisterPreKeys(ctx, &generatedPreKeys, pni, preKeyUsername, cli.Store.Password)
err = cli.RegisterPreKeys(ctx, &generatedPreKeys, pni)
if err != nil { if err != nil {
return fmt.Errorf("failed to register prekeys: %w", err) return fmt.Errorf("failed to register prekeys: %w", err)
} }
@ -328,11 +346,11 @@ func KyberPreKeyToJSON(kyberPreKey *libsignalgo.KyberPreKeyRecord) (map[string]i
var errPrekeyUpload422 = errors.New("http 422 while registering prekeys") var errPrekeyUpload422 = errors.New("http 422 while registering prekeys")
func (cli *Client) RegisterPreKeys(ctx context.Context, generatedPreKeys *GeneratedPreKeys, pni bool) error { func RegisterPreKeys(ctx context.Context, generatedPreKeys *GeneratedPreKeys, pni bool, username string, password string) error {
log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger() log := zerolog.Ctx(ctx).With().Str("action", "register prekeys").Logger()
// Convert generated prekeys to JSON // Convert generated prekeys to JSON
preKeysJson := []map[string]any{} preKeysJson := []map[string]interface{}{}
kyberPreKeysJson := []map[string]any{} kyberPreKeysJson := []map[string]interface{}{}
for _, preKey := range generatedPreKeys.PreKeys { for _, preKey := range generatedPreKeys.PreKeys {
preKeyJson, err := PreKeyToJSON(preKey) preKeyJson, err := PreKeyToJSON(preKey)
if err != nil { if err != nil {
@ -349,27 +367,32 @@ func (cli *Client) RegisterPreKeys(ctx context.Context, generatedPreKeys *Genera
} }
identityKey := generatedPreKeys.IdentityKey identityKey := generatedPreKeys.IdentityKey
registerJSON := map[string]any{ register_json := map[string]interface{}{
"preKeys": preKeysJson, "preKeys": preKeysJson,
"pqPreKeys": kyberPreKeysJson, "pqPreKeys": kyberPreKeysJson,
"identityKey": base64.StdEncoding.EncodeToString(identityKey), "identityKey": base64.StdEncoding.EncodeToString(identityKey),
} }
// Send request // Send request
jsonBytes, err := json.Marshal(registerJSON) jsonBytes, err := json.Marshal(register_json)
if err != nil { if err != nil {
log.Err(err).Msg("Error marshalling register JSON") log.Err(err).Msg("Error marshalling register JSON")
return err return err
} }
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, keysPath(pni), jsonBytes, nil) opts := &web.HTTPReqOpt{Body: jsonBytes, Username: &username, Password: &password}
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, keysPath(pni), opts)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request") log.Err(err).Msg("Error sending request")
return err return err
} }
if resp.GetStatus() == 422 { defer resp.Body.Close()
// status code not 2xx
if resp.StatusCode == 422 {
return errPrekeyUpload422 return errPrekeyUpload422
} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("error registering prekeys: %v", resp.Status)
} }
return web.DecodeWSResponseBody(ctx, nil, resp) return err
} }
type prekeyResponse struct { type prekeyResponse struct {
@ -404,40 +427,25 @@ func addBase64PaddingAndDecode(data string) ([]byte, error) {
return base64.StdEncoding.DecodeString(data) return base64.StdEncoding.DecodeString(data)
} }
var (
ErrUnregisteredUser = errors.New("user is unregistered")
ErrDevicesChanged = errors.New("device list changed while sending skdm")
)
func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID libsignalgo.ServiceID, specificDeviceID int) error { func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID libsignalgo.ServiceID, specificDeviceID int) error {
if cli.Store.RecipientStore.IsUnregistered(ctx, theirServiceID) {
return fmt.Errorf("%w (cached)", ErrUnregisteredUser)
}
localAddress, err := cli.Store.ACIServiceID().Address(uint(cli.Store.DeviceID))
if err != nil {
return fmt.Errorf("failed to get own address: %w", err)
}
// Fetch prekey // Fetch prekey
deviceIDPath := "/*" deviceIDPath := "/*"
if specificDeviceID >= 0 { if specificDeviceID >= 0 {
deviceIDPath = "/" + fmt.Sprint(specificDeviceID) deviceIDPath = "/" + fmt.Sprint(specificDeviceID)
} }
// TODO this should be done via the unauthed websocket if possible
path := "/v2/keys/" + theirServiceID.String() + deviceIDPath + "?pq=true" path := "/v2/keys/" + theirServiceID.String() + deviceIDPath + "?pq=true"
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, path, nil, nil) username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, path, &web.HTTPReqOpt{Username: &username, Password: &password})
if err != nil { if err != nil {
return fmt.Errorf("error sending request: %w", err) return fmt.Errorf("error sending request: %w", err)
} else if resp.GetStatus() == 404 {
cli.Store.RecipientStore.MarkUnregistered(ctx, theirServiceID, true)
return fmt.Errorf("%w (404 while querying keys)", ErrUnregisteredUser)
} }
var respData prekeyResponse var prekeyResponse prekeyResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp) err = web.DecodeHTTPResponseBody(ctx, &prekeyResponse, resp)
if err != nil { if err != nil {
return fmt.Errorf("error decoding response body: %w", err) return fmt.Errorf("error decoding response body: %w", err)
} }
rawIdentityKey, err := addBase64PaddingAndDecode(respData.IdentityKey) rawIdentityKey, err := addBase64PaddingAndDecode(prekeyResponse.IdentityKey)
if err != nil { if err != nil {
return fmt.Errorf("error decoding identity key: %w", err) return fmt.Errorf("error decoding identity key: %w", err)
} }
@ -450,7 +458,7 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
} }
// Process each prekey in response (should only be one at the moment) // Process each prekey in response (should only be one at the moment)
for _, d := range respData.Devices { for _, d := range prekeyResponse.Devices {
var publicKey *libsignalgo.PublicKey var publicKey *libsignalgo.PublicKey
var preKeyID uint32 var preKeyID uint32
if d.PreKey != nil { if d.PreKey != nil {
@ -522,7 +530,6 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
ctx, ctx,
preKeyBundle, preKeyBundle,
address, address,
localAddress,
cli.Store.ACISessionStore, cli.Store.ACISessionStore,
cli.Store.ACIIdentityStore, cli.Store.ACIIdentityStore,
) )
@ -548,18 +555,19 @@ func keysPath(pni bool) string {
func (cli *Client) GetMyKeyCounts(ctx context.Context, pni bool) (int, int, error) { func (cli *Client) GetMyKeyCounts(ctx context.Context, pni bool) (int, int, error) {
log := zerolog.Ctx(ctx).With().Str("action", "get my key counts").Logger() log := zerolog.Ctx(ctx).With().Str("action", "get my key counts").Logger()
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, keysPath(pni), nil, nil) username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, keysPath(pni), &web.HTTPReqOpt{Username: &username, Password: &password})
if err != nil { if err != nil {
log.Err(err).Msg("Error sending request") log.Err(err).Msg("Error sending request")
return 0, 0, err return 0, 0, err
} }
var respData preKeyCountResponse var preKeyCountResponse preKeyCountResponse
err = web.DecodeWSResponseBody(ctx, &respData, resp) err = web.DecodeHTTPResponseBody(ctx, &preKeyCountResponse, resp)
if err != nil { if err != nil {
log.Err(err).Msg("Fetching prekey counts, error with response body") log.Err(err).Msg("Fetching prekey counts, error with response body")
return 0, 0, err return 0, 0, err
} }
return respData.Count, respData.PQCount, err return preKeyCountResponse.Count, preKeyCountResponse.PQCount, err
} }
func (cli *Client) CheckAndUploadNewPreKeys(ctx context.Context, pks store.PreKeyStore) error { func (cli *Client) CheckAndUploadNewPreKeys(ctx context.Context, pks store.PreKeyStore) error {
@ -596,29 +604,23 @@ func (cli *Client) keyCheckLoop(ctx context.Context) {
log := zerolog.Ctx(ctx).With().Str("action", "start key check loop").Logger() log := zerolog.Ctx(ctx).With().Str("action", "start key check loop").Logger()
// Do the initial check in 5-10 minutes after starting the loop // Do the initial check in 5-10 minutes after starting the loop
windowStart := 0 window_start := 0
windowSize := 1 window_size := 1
firstRun := true
for { for {
randomMinutesInWindow := rand.IntN(windowSize) + windowStart random_minutes_in_window := rand.Intn(window_size) + window_start
checkTime := time.Duration(randomMinutesInWindow) * time.Minute check_time := time.Duration(random_minutes_in_window) * time.Minute
if firstRun { log.Debug().Dur("check_time", check_time).Msg("Waiting to check for new prekeys")
checkTime = 0
firstRun = false
} else {
log.Debug().Dur("check_time", checkTime).Msg("Waiting to check for new prekeys")
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-time.After(checkTime): case <-time.After(check_time):
err := cli.CheckAndUploadNewPreKeys(ctx, cli.Store.ACIPreKeyStore) err := cli.CheckAndUploadNewPreKeys(ctx, cli.Store.ACIPreKeyStore)
if err != nil { if err != nil {
log.Err(err).Msg("Error checking and uploading new prekeys for ACI identity") log.Err(err).Msg("Error checking and uploading new prekeys for ACI identity")
// Retry within half an hour // Retry within half an hour
windowStart = 5 window_start = 5
windowSize = 25 window_size = 25
continue continue
} }
err = cli.CheckAndUploadNewPreKeys(ctx, cli.Store.PNIPreKeyStore) err = cli.CheckAndUploadNewPreKeys(ctx, cli.Store.PNIPreKeyStore)
@ -634,13 +636,13 @@ func (cli *Client) keyCheckLoop(ctx context.Context) {
} }
log.Err(err).Msg("Error checking and uploading new prekeys for PNI identity") log.Err(err).Msg("Error checking and uploading new prekeys for PNI identity")
// Retry within half an hour // Retry within half an hour
windowStart = 5 window_start = 5
windowSize = 25 window_size = 25
continue continue
} }
// After a successful check, check again in 36 to 60 hours // After a successful check, check again in 36 to 60 hours
windowStart = 36 * 60 window_start = 36 * 60
windowSize = 24 * 60 window_size = 24 * 60
} }
} }
} }

View file

@ -18,10 +18,7 @@ package signalmeow
import ( import (
_ "embed" _ "embed"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exerrors" "go.mau.fi/util/exerrors"
@ -69,8 +66,6 @@ func (l FFILogger) Log(level libsignalgo.LogLevel, file string, line uint, messa
func (FFILogger) Flush() {} func (FFILogger) Flush() {}
func (FFILogger) Destroy() {}
// Ensure FFILogger implements the Logger interface // Ensure FFILogger implements the Logger interface
var _ libsignalgo.Logger = FFILogger{} var _ libsignalgo.Logger = FFILogger{}
@ -81,28 +76,3 @@ var prodServerPublicParams *libsignalgo.ServerPublicParams
func init() { func init() {
prodServerPublicParams = exerrors.Must(libsignalgo.DeserializeServerPublicParams(prodServerPublicParamsSlice)) prodServerPublicParams = exerrors.Must(libsignalgo.DeserializeServerPublicParams(prodServerPublicParamsSlice))
} }
var ErrEmptyUUIDInput = errors.New("both input variables are empty")
func ParseStringOrBinaryServiceID(str string, bytes []byte) (libsignalgo.ServiceID, error) {
if str != "" {
return libsignalgo.ServiceIDFromString(str)
}
if bytes != nil {
return libsignalgo.ServiceIDFromBytes(bytes)
}
return libsignalgo.EmptyServiceID, ErrEmptyUUIDInput
}
func ParseStringOrBinaryUUID(str string, bytes []byte) (uuid.UUID, error) {
if str != "" {
return uuid.Parse(str)
}
if bytes != nil {
if len(bytes) != 16 {
return uuid.Nil, fmt.Errorf("invalid UUID length %d (expected 16)", len(bytes))
}
return uuid.UUID(bytes), nil
}
return uuid.Nil, ErrEmptyUUIDInput
}

View file

@ -134,6 +134,14 @@ func (cli *Client) getCachedProfileByID(signalID uuid.UUID, refreshAfter time.Du
} }
func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID, refreshAfter time.Duration) (*types.Profile, error) { func (cli *Client) RetrieveProfileByID(ctx context.Context, signalID uuid.UUID, refreshAfter time.Duration) (*types.Profile, error) {
if cli.ProfileCache == nil {
cli.ProfileCache = &ProfileCache{
profiles: make(map[string]*types.Profile),
errors: make(map[string]*error),
lastFetched: make(map[string]time.Time),
}
}
// Check if we have a cached profile that is less than an hour old // Check if we have a cached profile that is less than an hour old
// or if we have a cached error that is less than an hour old // or if we have a cached error that is less than an hour old
profile, err := cli.getCachedProfileByID(signalID, refreshAfter) profile, err := cli.getCachedProfileByID(signalID, refreshAfter)
@ -210,18 +218,18 @@ func (cli *Client) fetchProfileWithRequestAndKey(ctx context.Context, signalID u
path += "/" + string(credentialRequest) path += "/" + string(credentialRequest)
path += "?credentialType=expiringProfileKey" path += "?credentialType=expiringProfileKey"
} }
headers := http.Header{} profileRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
if useUnidentified { if useUnidentified {
headers.Set("Unidentified-Access-Key", base64AccessKey) profileRequest.Headers = append(profileRequest.Headers, "unidentified-access-key:"+base64AccessKey)
headers.Set("Accept-Language", "en-US") profileRequest.Headers = append(profileRequest.Headers, "accept-language:en-CA")
} }
resp, err := cli.UnauthedWS.SendRequest(ctx, http.MethodGet, path, nil, headers) resp, err := cli.UnauthedWS.SendRequest(ctx, profileRequest)
if err != nil { if err != nil {
return nil, fmt.Errorf("error sending request: %w", err) return nil, fmt.Errorf("error sending request: %w", err)
} }
var profile types.Profile var profile types.Profile
profile.FetchedAt = time.Now() profile.FetchedAt = time.Now()
logEvt := log.Trace().Uint32("status_code", resp.GetStatus()).Str("resp_message", resp.GetMessage()) logEvt := log.Trace().Uint32("status_code", resp.GetStatus())
if logEvt.Enabled() { if logEvt.Enabled() {
if json.Valid(resp.Body) { if json.Valid(resp.Body) {
logEvt.RawJSON("response_data", resp.Body) logEvt.RawJSON("response_data", resp.Body)
@ -280,14 +288,15 @@ func (cli *Client) fetchProfileWithRequestAndKey(ctx context.Context, signalID u
func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey libsignalgo.ProfileKey) ([]byte, error) { func (cli *Client) DownloadUserAvatar(ctx context.Context, avatarPath string, profileKey libsignalgo.ProfileKey) ([]byte, error) {
username, password := cli.Store.BasicAuthCreds() username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{ opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username, Username: &username,
Password: &password, Password: &password,
} }
resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, avatarPath, opts) resp, err := web.SendHTTPRequest(ctx, http.MethodGet, avatarPath, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err) return nil, fmt.Errorf("failed to send request: %w", err)
} }
defer web.CloseBody(resp) defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode) return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode)
} }

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: ContactDiscovery.proto // source: ContactDiscovery.proto
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: DeviceName.proto // source: DeviceName.proto
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,11 @@
/* /**
* Copyright 2020 Signal Messenger, LLC * Copyright (C) 2019 Open Whisper Systems
* SPDX-License-Identifier: AGPL-3.0-only *
* Licensed according to the LICENSE file in this repository.
*/ */
syntax = "proto3"; syntax = "proto3";
package signal; option java_package = "org.signal.storageservice.protos.groups";
option java_package = "org.signal.storageservice.storage.protos.groups";
option java_outer_classname = "GroupProtos";
option java_multiple_files = true; option java_multiple_files = true;
message AvatarUploadAttributes { message AvatarUploadAttributes {
@ -21,8 +18,6 @@ message AvatarUploadAttributes {
string signature = 7; string signature = 7;
} }
// Stored data
message Member { message Member {
enum Role { enum Role {
UNKNOWN = 0; UNKNOWN = 0;
@ -33,28 +28,26 @@ message Member {
bytes userId = 1; bytes userId = 1;
Role role = 2; Role role = 2;
bytes profileKey = 3; bytes profileKey = 3;
bytes presentation = 4; bytes presentation = 4; // Only set when sending to server
uint32 joinedAtVersion = 5; uint32 joinedAtRevision = 5;
bytes labelEmoji = 6; // decrypts to a UTF-8 string
bytes labelString = 7; // decrypts to a UTF-8 string
} }
message MemberPendingProfileKey { message PendingMember {
Member member = 1; Member member = 1;
bytes addedByUserId = 2; bytes addedByUserId = 2;
uint64 timestamp = 3; // ms since epoch uint64 timestamp = 3;
} }
message MemberPendingAdminApproval { message RequestingMember {
bytes userId = 1; bytes userId = 1;
bytes profileKey = 2; bytes profileKey = 2;
bytes presentation = 3; bytes presentation = 3; // Only set when sending to server
uint64 timestamp = 4; // ms since epoch uint64 timestamp = 4;
} }
message MemberBanned { message BannedMember {
bytes userId = 1; bytes userId = 1;
uint64 timestamp = 2; // ms since epoch uint64 timestamp = 2;
} }
message AccessControl { message AccessControl {
@ -69,64 +62,24 @@ message AccessControl {
AccessRequired attributes = 1; AccessRequired attributes = 1;
AccessRequired members = 2; AccessRequired members = 2;
AccessRequired addFromInviteLink = 3; AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
} }
message Group { message Group {
bytes publicKey = 1; bytes publicKey = 1;
bytes title = 2; bytes title = 2;
bytes description = 11; string avatar = 3;
// The URL for this group's avatar. The content at this URL can be
// decrypted/deserialized into a `GroupAttributeBlob`.
string avatarUrl = 3;
bytes disappearingMessagesTimer = 4; bytes disappearingMessagesTimer = 4;
AccessControl accessControl = 5; AccessControl accessControl = 5;
uint32 version = 6; uint32 revision = 6;
repeated Member members = 7; repeated Member members = 7;
repeated MemberPendingProfileKey membersPendingProfileKey = 8; repeated PendingMember pendingMembers = 8;
repeated MemberPendingAdminApproval membersPendingAdminApproval = 9; repeated RequestingMember requestingMembers = 9;
bytes inviteLinkPassword = 10; bytes inviteLinkPassword = 10;
bool announcements_only = 12; bytes description = 11;
repeated MemberBanned members_banned = 13; bool announcementsOnly = 12;
bool terminated = 14; repeated BannedMember bannedMembers = 13;
// next: 15
} }
message GroupAttributeBlob {
oneof content {
string title = 1;
bytes avatar = 2;
uint32 disappearingMessagesDuration = 3;
string descriptionText = 4;
}
}
message GroupInviteLink {
message GroupInviteLinkContentsV1 {
bytes groupMasterKey = 1;
bytes inviteLinkPassword = 2;
}
oneof contents {
GroupInviteLinkContentsV1 contentsV1 = 1;
}
}
message GroupJoinInfo {
bytes publicKey = 1;
bytes title = 2;
bytes description = 8;
string avatar = 3;
uint32 memberCount = 4;
AccessControl.AccessRequired addFromInviteLink = 5;
uint32 version = 6;
bool pendingAdminApproval = 7;
// bool pendingAdminApprovalFull = 9;
// next: 10
}
// Deltas
message GroupChange { message GroupChange {
message Actions { message Actions {
@ -145,57 +98,51 @@ message GroupChange {
Member.Role role = 2; Member.Role role = 2;
} }
message ModifyMemberLabelAction {
bytes userId = 1;
bytes labelEmoji = 2; // decrypts to a UTF-8 string
bytes labelString = 3; // decrypts to a UTF-8 string
}
message ModifyMemberProfileKeyAction { message ModifyMemberProfileKeyAction {
bytes presentation = 1; bytes presentation = 1; // Only set when sending to server
bytes user_id = 2; bytes user_id = 2; // Only set when receiving from server
bytes profile_key = 3; bytes profile_key = 3; // Only set when receiving from server
} }
message AddMemberPendingProfileKeyAction { message AddPendingMemberAction {
MemberPendingProfileKey added = 1; PendingMember added = 1;
} }
message DeleteMemberPendingProfileKeyAction { message DeletePendingMemberAction {
bytes deletedUserId = 1; bytes deletedUserId = 1;
} }
message PromoteMemberPendingProfileKeyAction { message PromotePendingMemberAction {
bytes presentation = 1; bytes presentation = 1; // Only set when sending to server
bytes user_id = 2; bytes user_id = 2; // Only set when receiving from server
bytes profile_key = 3; bytes profile_key = 3; // Only set when receiving from server
} }
message PromoteMemberPendingPniAciProfileKeyAction { message PromotePendingPniAciMemberProfileKeyAction {
bytes presentation = 1; bytes presentation = 1; // Only set when sending to server
bytes user_id = 2; bytes userId = 2; // Only set when receiving from server
bytes pni = 3; bytes pni = 3; // Only set when receiving from server
bytes profile_key = 4; bytes profileKey = 4; // Only set when receiving from server
} }
message AddMemberPendingAdminApprovalAction { message AddRequestingMemberAction {
MemberPendingAdminApproval added = 1; RequestingMember added = 1;
} }
message DeleteMemberPendingAdminApprovalAction { message DeleteRequestingMemberAction {
bytes deletedUserId = 1; bytes deletedUserId = 1;
} }
message PromoteMemberPendingAdminApprovalAction { message PromoteRequestingMemberAction {
bytes userId = 1; bytes userId = 1;
Member.Role role = 2; Member.Role role = 2;
} }
message AddMemberBannedAction { message AddBannedMemberAction {
MemberBanned added = 1; BannedMember added = 1;
} }
message DeleteMemberBannedAction { message DeleteBannedMemberAction {
bytes deletedUserId = 1; bytes deletedUserId = 1;
} }
@ -211,7 +158,7 @@ message GroupChange {
string avatar = 1; string avatar = 1;
} }
message ModifyDisappearingMessageTimerAction { message ModifyDisappearingMessagesTimerAction {
bytes timer = 1; bytes timer = 1;
} }
@ -227,52 +174,39 @@ message GroupChange {
AccessControl.AccessRequired addFromInviteLinkAccess = 1; AccessControl.AccessRequired addFromInviteLinkAccess = 1;
} }
message ModifyMemberLabelAccessControlAction {
AccessControl.AccessRequired memberLabelAccess = 1;
}
message ModifyInviteLinkPasswordAction { message ModifyInviteLinkPasswordAction {
bytes inviteLinkPassword = 1; bytes inviteLinkPassword = 1;
} }
message ModifyAnnouncementsOnlyAction { message ModifyAnnouncementsOnlyAction {
bool announcements_only = 1; bool announcementsOnly = 1;
} }
message TerminateGroupAction {} bytes sourceServiceId = 1;
bytes groupId = 25; // Only set when receiving from server
bytes sourceUserId = 1; uint32 revision = 2;
// clients should not provide this value; the server will provide it in the response buffer to ensure the signature is binding to a particular group
// if clients set it during a request the server will respond with 400.
bytes group_id = 25;
uint32 version = 2;
repeated AddMemberAction addMembers = 3; repeated AddMemberAction addMembers = 3;
repeated DeleteMemberAction deleteMembers = 4; repeated DeleteMemberAction deleteMembers = 4;
repeated ModifyMemberRoleAction modifyMemberRoles = 5; repeated ModifyMemberRoleAction modifyMemberRoles = 5;
repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6; repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6;
repeated AddMemberPendingProfileKeyAction addMembersPendingProfileKey = 7; repeated AddPendingMemberAction addPendingMembers = 7;
repeated DeleteMemberPendingProfileKeyAction deleteMembersPendingProfileKey = 8; repeated DeletePendingMemberAction deletePendingMembers = 8;
repeated PromoteMemberPendingProfileKeyAction promoteMembersPendingProfileKey = 9; repeated PromotePendingMemberAction promotePendingMembers = 9;
ModifyTitleAction modifyTitle = 10; ModifyTitleAction modifyTitle = 10;
ModifyAvatarAction modifyAvatar = 11; ModifyAvatarAction modifyAvatar = 11;
ModifyDisappearingMessageTimerAction modifyDisappearingMessageTimer = 12; ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12;
ModifyAttributesAccessControlAction modifyAttributesAccess = 13; ModifyAttributesAccessControlAction modifyAttributesAccess = 13;
ModifyMembersAccessControlAction modifyMemberAccess = 14; ModifyMembersAccessControlAction modifyMemberAccess = 14;
ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15; // change epoch = 1 ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15;
repeated AddMemberPendingAdminApprovalAction addMembersPendingAdminApproval = 16; // change epoch = 1 repeated AddRequestingMemberAction addRequestingMembers = 16;
repeated DeleteMemberPendingAdminApprovalAction deleteMembersPendingAdminApproval = 17; // change epoch = 1 repeated DeleteRequestingMemberAction deleteRequestingMembers = 17;
repeated PromoteMemberPendingAdminApprovalAction promoteMembersPendingAdminApproval = 18; // change epoch = 1 repeated PromoteRequestingMemberAction promoteRequestingMembers = 18;
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; // change epoch = 1 ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19;
ModifyDescriptionAction modifyDescription = 20; // change epoch = 2 ModifyDescriptionAction modifyDescription = 20;
ModifyAnnouncementsOnlyAction modify_announcements_only = 21; // change epoch = 3 ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21;
repeated AddMemberBannedAction add_members_banned = 22; // change epoch = 4 repeated AddBannedMemberAction addBannedMembers = 22;
repeated DeleteMemberBannedAction delete_members_banned = 23; // change epoch = 4 repeated DeleteBannedMemberAction deleteBannedMembers = 23;
repeated PromoteMemberPendingPniAciProfileKeyAction promote_members_pending_pni_aci_profile_key = 24; // change epoch = 5 repeated PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMembers = 24;
repeated ModifyMemberLabelAction modifyMemberLabels = 26; // change epoch = 6;
ModifyMemberLabelAccessControlAction modifyMemberLabelAccess = 27; // change epoch = 6
TerminateGroupAction terminate_group = 28; // change epoch = 7
// next: 29
} }
bytes actions = 1; bytes actions = 1;
@ -280,17 +214,9 @@ message GroupChange {
uint32 changeEpoch = 3; uint32 changeEpoch = 3;
} }
// External credentials
message ExternalGroupCredential {
string token = 1;
}
// API responses
message GroupResponse { message GroupResponse {
Group group = 1; Group group = 1;
bytes group_send_endorsements_response = 2; bytes groupSendEndorsementsResponse = 2;
} }
message GroupChanges { message GroupChanges {
@ -300,10 +226,45 @@ message GroupChanges {
} }
repeated GroupChangeState groupChanges = 1; repeated GroupChangeState groupChanges = 1;
bytes group_send_endorsements_response = 2; bytes groupSendEndorsementsResponse = 2;
} }
message GroupChangeResponse { message GroupChangeResponse {
GroupChange group_change = 1; GroupChange groupChange = 1;
bytes group_send_endorsements_response = 2; bytes groupSendEndorsementsResponse = 2;
}
message GroupAttributeBlob {
oneof content {
string title = 1;
bytes avatar = 2;
uint32 disappearingMessagesDuration = 3;
string description = 4;
}
}
message GroupInviteLink {
message GroupInviteLinkContentsV1 {
bytes groupMasterKey = 1;
bytes inviteLinkPassword = 2;
}
oneof contents {
GroupInviteLinkContentsV1 v1Contents = 1;
}
}
message GroupJoinInfo {
bytes publicKey = 1;
bytes title = 2;
string avatar = 3;
uint32 memberCount = 4;
AccessControl.AccessRequired addFromInviteLink = 5;
uint32 revision = 6;
bool pendingAdminApproval = 7;
bytes description = 8;
}
message GroupExternalCredential {
string token = 1;
} }

View file

@ -4,8 +4,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: Provisioning.proto // source: Provisioning.proto
package signalpb package signalpb
@ -199,6 +199,7 @@ type ProvisionMessage struct {
ProfileKey []byte `protobuf:"bytes,6,opt,name=profileKey" json:"profileKey,omitempty"` ProfileKey []byte `protobuf:"bytes,6,opt,name=profileKey" json:"profileKey,omitempty"`
ReadReceipts *bool `protobuf:"varint,7,opt,name=readReceipts" json:"readReceipts,omitempty"` ReadReceipts *bool `protobuf:"varint,7,opt,name=readReceipts" json:"readReceipts,omitempty"`
ProvisioningVersion *uint32 `protobuf:"varint,9,opt,name=provisioningVersion" json:"provisioningVersion,omitempty"` ProvisioningVersion *uint32 `protobuf:"varint,9,opt,name=provisioningVersion" json:"provisioningVersion,omitempty"`
MasterKey []byte `protobuf:"bytes,13,opt,name=masterKey" json:"masterKey,omitempty"` // Deprecated, but required by linked devices
EphemeralBackupKey []byte `protobuf:"bytes,14,opt,name=ephemeralBackupKey" json:"ephemeralBackupKey,omitempty"` // 32 bytes EphemeralBackupKey []byte `protobuf:"bytes,14,opt,name=ephemeralBackupKey" json:"ephemeralBackupKey,omitempty"` // 32 bytes
AccountEntropyPool *string `protobuf:"bytes,15,opt,name=accountEntropyPool" json:"accountEntropyPool,omitempty"` AccountEntropyPool *string `protobuf:"bytes,15,opt,name=accountEntropyPool" json:"accountEntropyPool,omitempty"`
MediaRootBackupKey []byte `protobuf:"bytes,16,opt,name=mediaRootBackupKey" json:"mediaRootBackupKey,omitempty"` // 32-bytes MediaRootBackupKey []byte `protobuf:"bytes,16,opt,name=mediaRootBackupKey" json:"mediaRootBackupKey,omitempty"` // 32-bytes
@ -322,6 +323,13 @@ func (x *ProvisionMessage) GetProvisioningVersion() uint32 {
return 0 return 0
} }
func (x *ProvisionMessage) GetMasterKey() []byte {
if x != nil {
return x.MasterKey
}
return nil
}
func (x *ProvisionMessage) GetEphemeralBackupKey() []byte { func (x *ProvisionMessage) GetEphemeralBackupKey() []byte {
if x != nil { if x != nil {
return x.EphemeralBackupKey return x.EphemeralBackupKey
@ -366,7 +374,7 @@ const file_Provisioning_proto_rawDesc = "" +
"\aaddress\x18\x01 \x01(\tR\aaddress\"E\n" + "\aaddress\x18\x01 \x01(\tR\aaddress\"E\n" +
"\x11ProvisionEnvelope\x12\x1c\n" + "\x11ProvisionEnvelope\x12\x1c\n" +
"\tpublicKey\x18\x01 \x01(\fR\tpublicKey\x12\x12\n" + "\tpublicKey\x18\x01 \x01(\fR\tpublicKey\x12\x12\n" +
"\x04body\x18\x02 \x01(\fR\x04body\"\xb4\x05\n" + "\x04body\x18\x02 \x01(\fR\x04body\"\xcc\x05\n" +
"\x10ProvisionMessage\x122\n" + "\x10ProvisionMessage\x122\n" +
"\x14aciIdentityKeyPublic\x18\x01 \x01(\fR\x14aciIdentityKeyPublic\x124\n" + "\x14aciIdentityKeyPublic\x18\x01 \x01(\fR\x14aciIdentityKeyPublic\x124\n" +
"\x15aciIdentityKeyPrivate\x18\x02 \x01(\fR\x15aciIdentityKeyPrivate\x122\n" + "\x15aciIdentityKeyPrivate\x18\x02 \x01(\fR\x15aciIdentityKeyPrivate\x122\n" +
@ -382,12 +390,13 @@ const file_Provisioning_proto_rawDesc = "" +
"profileKey\x18\x06 \x01(\fR\n" + "profileKey\x18\x06 \x01(\fR\n" +
"profileKey\x12\"\n" + "profileKey\x12\"\n" +
"\freadReceipts\x18\a \x01(\bR\freadReceipts\x120\n" + "\freadReceipts\x18\a \x01(\bR\freadReceipts\x120\n" +
"\x13provisioningVersion\x18\t \x01(\rR\x13provisioningVersion\x12.\n" + "\x13provisioningVersion\x18\t \x01(\rR\x13provisioningVersion\x12\x1c\n" +
"\tmasterKey\x18\r \x01(\fR\tmasterKey\x12.\n" +
"\x12ephemeralBackupKey\x18\x0e \x01(\fR\x12ephemeralBackupKey\x12.\n" + "\x12ephemeralBackupKey\x18\x0e \x01(\fR\x12ephemeralBackupKey\x12.\n" +
"\x12accountEntropyPool\x18\x0f \x01(\tR\x12accountEntropyPool\x12.\n" + "\x12accountEntropyPool\x18\x0f \x01(\tR\x12accountEntropyPool\x12.\n" +
"\x12mediaRootBackupKey\x18\x10 \x01(\fR\x12mediaRootBackupKey\x12\x1c\n" + "\x12mediaRootBackupKey\x18\x10 \x01(\fR\x12mediaRootBackupKey\x12\x1c\n" +
"\taciBinary\x18\x11 \x01(\fR\taciBinary\x12\x1c\n" + "\taciBinary\x18\x11 \x01(\fR\taciBinary\x12\x1c\n" +
"\tpniBinary\x18\x12 \x01(\fR\tpniBinaryJ\x04\b\r\x10\x0e*G\n" + "\tpniBinary\x18\x12 \x01(\fR\tpniBinary*G\n" +
"\x13ProvisioningVersion\x12\v\n" + "\x13ProvisioningVersion\x12\v\n" +
"\aINITIAL\x10\x00\x12\x12\n" + "\aINITIAL\x10\x00\x12\x12\n" +
"\x0eTABLET_SUPPORT\x10\x01\x12\v\n" + "\x0eTABLET_SUPPORT\x10\x01\x12\v\n" +

View file

@ -38,7 +38,7 @@ message ProvisionMessage {
optional bytes profileKey = 6; optional bytes profileKey = 6;
optional bool readReceipts = 7; optional bool readReceipts = 7;
optional uint32 provisioningVersion = 9; optional uint32 provisioningVersion = 9;
reserved /*masterKey*/ 13; // Deprecated in favor of accountEntropyPool optional bytes masterKey = 13; // Deprecated, but required by linked devices
optional bytes ephemeralBackupKey = 14; // 32 bytes optional bytes ephemeralBackupKey = 14; // 32 bytes
optional string accountEntropyPool = 15; optional string accountEntropyPool = 15;
optional bytes mediaRootBackupKey = 16; // 32-bytes optional bytes mediaRootBackupKey = 16; // 32-bytes

File diff suppressed because it is too large Load diff

View file

@ -13,79 +13,23 @@ option java_outer_classname = "SignalServiceProtos";
message Envelope { message Envelope {
enum Type { enum Type {
UNKNOWN = 0; UNKNOWN = 0;
CIPHERTEXT = 1; // content => (version byte | SignalMessage{Content})
/**
* A double-ratchet message represents a "normal," "unsealed-sender" message
* encrypted using the Double Ratchet within an established Signal session.
* Double-ratchet messages include sender information in the plaintext
* portion of the `Envelope`.
*/
DOUBLE_RATCHET = 1; // content => (version byte | SignalMessage{Content})
reserved 2; reserved 2;
reserved "KEY_EXCHANGE"; reserved "KEY_EXCHANGE";
PREKEY_BUNDLE = 3; // content => (version byte | PreKeySignalMessage{Content})
/** SERVER_DELIVERY_RECEIPT = 5; // legacyMessage => [] AND content => []
* A prekey message begins a new Signal session. The `content` of a prekey UNIDENTIFIED_SENDER = 6; // legacyMessage => [] AND content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
* message is a superset of a double-ratchet message's `content` and SENDERKEY_MESSAGE = 7; // legacyMessage => [] AND content => (version byte | SenderKeyMessage)
* contains the sender's identity public key and information identifying the PLAINTEXT_CONTENT = 8; // legacyMessage => [] AND content => (marker byte | Content)
* pre-keys used in the message's ciphertext. Like double-ratchet messages,
* prekey messages contain sender information in the plaintext portion of
* the `Envelope`.
*/
PREKEY_MESSAGE = 3; // content => (version byte | PreKeySignalMessage{Content})
/**
* Server delivery receipts are generated by the server when
* "unsealed-sender" messages are delivered to and acknowledged by the
* destination device. Server delivery receipts identify the sender in the
* plaintext portion of the `Envelope` and have no `content`. Note that
* receipts for sealed-sender messages are generated by clients as
* `UNIDENTIFIED_SENDER` messages.
*
* Note that, with server delivery receipts, the "client timestamp" on
* the envelope refers to the timestamp of the original message (i.e. the
* message the server just delivered) and not to the time of delivery. The
* "server timestamp" refers to the time of delivery.
*/
SERVER_DELIVERY_RECEIPT = 5; // content => []
/**
* An unidentified sender message represents a message with no sender
* information in the plaintext portion of the `Envelope`. Unidentified
* sender messages always contain an additional `subtype` in their
* `content`. They may or may not be part of an existing Signal session
* (i.e. an unidentified sender message may have a "prekey message"
* subtype or may indicate an encryption error).
*/
UNIDENTIFIED_SENDER = 6; // content => ((version byte | UnidentifiedSenderMessage) OR (version byte | Multi-Recipient Sealed Sender Format))
reserved 7;
reserved "SENDERKEY_MESSAGE";
/**
* A plaintext message is used solely to convey encryption error receipts
* and never contains encrypted message content. Encryption error receipts
* must be delivered in plaintext because, encryption/decryption of a prior
* message failed and there is no reason to believe that
* encryption/decryption of subsequent messages with the same key material
* would succeed.
*
* Critically, plaintext messages never have "real" message content
* generated by users. Plaintext messages include sender information.
*/
PLAINTEXT_CONTENT = 8; // content => (marker byte | Content)
// next: 9
} }
optional Type type = 1; optional Type type = 1;
reserved 2; // formerly optional string sourceE164 = 2; reserved 2; // formerly optional string sourceE164 = 2;
optional string sourceServiceId = 11; optional string sourceServiceId = 11;
optional uint32 sourceDeviceId = 7; optional uint32 sourceDevice = 7;
optional string destinationServiceId = 13; optional string destinationServiceId = 13;
reserved 3; // formerly optional string relay = 3; reserved 3; // formerly optional string relay = 3;
optional uint64 clientTimestamp = 5; optional uint64 timestamp = 5;
reserved 6; // formerly optional bytes legacyMessage = 6; // Contains an encrypted DataMessage; this field could have been set historically for type 1 or 3 messages; no longer in use reserved 6; // formerly optional bytes legacyMessage = 6; // Contains an encrypted DataMessage; this field could have been set historically for type 1 or 3 messages; no longer in use
optional bytes content = 8; // Contains an encrypted Content optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9; optional string serverGuid = 9;
@ -96,28 +40,21 @@ message Envelope {
optional bool story = 16; // indicates that the content is a story. optional bool story = 16; // indicates that the content is a story.
optional bytes report_spam_token = 17; // token sent when reporting spam optional bytes report_spam_token = 17; // token sent when reporting spam
reserved 18; // internal server use reserved 18; // internal server use
optional bytes sourceServiceIdBinary = 19; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI) // next: 19
optional bytes destinationServiceIdBinary = 20; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
optional bytes serverGuidBinary = 21; // 16-byte UUID
optional bytes updatedPniBinary = 22; // 16-byte UUID
// next: 22
} }
message Content { message Content {
oneof content { optional DataMessage dataMessage = 1;
DataMessage dataMessage = 1; optional SyncMessage syncMessage = 2;
SyncMessage syncMessage = 2; optional CallMessage callMessage = 3;
CallMessage callMessage = 3; optional NullMessage nullMessage = 4;
NullMessage nullMessage = 4; optional ReceiptMessage receiptMessage = 5;
ReceiptMessage receiptMessage = 5; optional TypingMessage typingMessage = 6;
TypingMessage typingMessage = 6;
bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8;
StoryMessage storyMessage = 9;
EditMessage editMessage = 11;
}
optional bytes /* SenderKeyDistributionMessage */ senderKeyDistributionMessage = 7; optional bytes /* SenderKeyDistributionMessage */ senderKeyDistributionMessage = 7;
optional bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8;
optional StoryMessage storyMessage = 9;
optional PniSignatureMessage pniSignatureMessage = 10; optional PniSignatureMessage pniSignatureMessage = 10;
optional EditMessage editMessage = 11;
} }
message CallMessage { message CallMessage {
@ -240,7 +177,6 @@ message DataMessage {
enum Type { enum Type {
NORMAL = 0; NORMAL = 0;
GIFT_BADGE = 1; GIFT_BADGE = 1;
POLL = 2;
} }
message QuotedAttachment { message QuotedAttachment {
@ -256,7 +192,6 @@ message DataMessage {
repeated QuotedAttachment attachments = 4; repeated QuotedAttachment attachments = 4;
repeated BodyRange bodyRanges = 6; repeated BodyRange bodyRanges = 6;
optional Type type = 7; optional Type type = 7;
optional bytes authorAciBinary = 8; // 16-byte UUID
} }
message Contact { message Contact {
@ -341,7 +276,6 @@ message DataMessage {
reserved /* targetAuthorE164 */ 3; reserved /* targetAuthorE164 */ 3;
optional string targetAuthorAci = 4; optional string targetAuthorAci = 4;
optional uint64 targetSentTimestamp = 5; optional uint64 targetSentTimestamp = 5;
optional bytes targetAuthorAciBinary = 6; // 16-byte UUID
} }
message Delete { message Delete {
@ -355,7 +289,6 @@ message DataMessage {
message StoryContext { message StoryContext {
optional string authorAci = 1; optional string authorAci = 1;
optional uint64 sentTimestamp = 2; optional uint64 sentTimestamp = 2;
optional bytes authorAciBinary = 3; // 16-byte UUID
} }
enum ProtocolVersion { enum ProtocolVersion {
@ -369,50 +302,13 @@ message DataMessage {
CDN_SELECTOR_ATTACHMENTS = 5; CDN_SELECTOR_ATTACHMENTS = 5;
MENTIONS = 6; MENTIONS = 6;
PAYMENTS = 7; PAYMENTS = 7;
POLLS = 8; CURRENT = 7;
CURRENT = 8;
} }
message GiftBadge { message GiftBadge {
optional bytes receiptCredentialPresentation = 1; optional bytes receiptCredentialPresentation = 1;
} }
message PollCreate {
optional string question = 1;
optional bool allowMultiple = 2;
repeated string options = 3;
}
message PollTerminate {
optional uint64 targetSentTimestamp = 1;
}
message PollVote {
optional bytes targetAuthorAciBinary = 1;
optional uint64 targetSentTimestamp = 2;
repeated uint32 optionIndexes = 3;
optional uint32 voteCount = 4;
}
message PinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
oneof pinDuration {
uint32 pinDurationSeconds = 3;
bool pinDurationForever = 4;
}
}
message UnpinMessage {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
}
message AdminDelete {
optional bytes targetAuthorAciBinary = 1; // 16-byte UUID
optional uint64 targetSentTimestamp = 2;
}
optional string body = 1; optional string body = 1;
repeated AttachmentPointer attachments = 2; repeated AttachmentPointer attachments = 2;
reserved /*groupV1*/ 3; reserved /*groupV1*/ 3;
@ -435,13 +331,7 @@ message DataMessage {
optional Payment payment = 20; optional Payment payment = 20;
optional StoryContext storyContext = 21; optional StoryContext storyContext = 21;
optional GiftBadge giftBadge = 22; optional GiftBadge giftBadge = 22;
optional PollCreate pollCreate = 24; // NEXT ID: 24
optional PollTerminate pollTerminate = 25;
optional PollVote pollVote = 26;
optional PinMessage pinMessage = 27;
optional UnpinMessage unpinMessage = 28;
optional AdminDelete adminDelete = 29;
// NEXT ID: 30
} }
message NullMessage { message NullMessage {
@ -500,12 +390,6 @@ message TextAttachment {
} }
message Gradient { message Gradient {
// Color ordering:
// 0 degrees: bottom-to-top
// 90 degrees: left-to-right
// 180 degrees: top-to-bottom
// 270 degrees: right-to-left
optional uint32 startColor = 1; // deprecated: this field will be removed in a future release. optional uint32 startColor = 1; // deprecated: this field will be removed in a future release.
optional uint32 endColor = 2; // deprecated: this field will be removed in a future release. optional uint32 endColor = 2; // deprecated: this field will be removed in a future release.
optional uint32 angle = 3; // degrees optional uint32 angle = 3; // degrees
@ -536,7 +420,6 @@ message Verified {
optional bytes identityKey = 2; optional bytes identityKey = 2;
optional State state = 3; optional State state = 3;
optional bytes nullMessage = 4; optional bytes nullMessage = 4;
optional bytes destinationAciBinary = 6; // 16-byte UUID
} }
message SyncMessage { message SyncMessage {
@ -547,7 +430,6 @@ message SyncMessage {
optional bool unidentified = 2; optional bool unidentified = 2;
reserved /*destinationPni */ 4; reserved /*destinationPni */ 4;
optional bytes destinationPniIdentityKey = 5; // Only set for PNI destinations optional bytes destinationPniIdentityKey = 5; // Only set for PNI destinations
optional bytes destinationServiceIdBinary = 6; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
message StoryMessageRecipient { message StoryMessageRecipient {
@ -555,7 +437,6 @@ message SyncMessage {
repeated string distributionListIds = 2; repeated string distributionListIds = 2;
optional bool isAllowedToReply = 3; optional bool isAllowedToReply = 3;
reserved /*destinationPni */ 4; reserved /*destinationPni */ 4;
optional bytes destinationServiceIdBinary = 5; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
optional string destinationE164 = 1; optional string destinationE164 = 1;
@ -569,8 +450,7 @@ message SyncMessage {
repeated StoryMessageRecipient storyMessageRecipients = 9; repeated StoryMessageRecipient storyMessageRecipients = 9;
optional EditMessage editMessage = 10; optional EditMessage editMessage = 10;
reserved /*destinationPni */ 11; reserved /*destinationPni */ 11;
optional bytes destinationServiceIdBinary = 12; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI) // Next ID: 12
// Next ID: 13
} }
message Contacts { message Contacts {
@ -582,7 +462,6 @@ message SyncMessage {
repeated string numbers = 1; repeated string numbers = 1;
repeated string acis = 3; repeated string acis = 3;
repeated bytes groupIds = 2; repeated bytes groupIds = 2;
repeated bytes acisBinary = 4; // 16-byte UUID
} }
message Request { message Request {
@ -603,14 +482,12 @@ message SyncMessage {
reserved /*senderE164*/ 1; reserved /*senderE164*/ 1;
optional string senderAci = 3; optional string senderAci = 3;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
} }
message Viewed { message Viewed {
reserved /*senderE164*/ 1; reserved /*senderE164*/ 1;
optional string senderAci = 3; optional string senderAci = 3;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
} }
message Configuration { message Configuration {
@ -618,7 +495,7 @@ message SyncMessage {
optional bool unidentifiedDeliveryIndicators = 2; optional bool unidentifiedDeliveryIndicators = 2;
optional bool typingIndicators = 3; optional bool typingIndicators = 3;
reserved /* linkPreviews */ 4; reserved /* linkPreviews */ 4;
reserved /* provisioningVersion */ 5; optional uint32 provisioningVersion = 5;
optional bool linkPreviews = 6; optional bool linkPreviews = 6;
} }
@ -637,7 +514,6 @@ message SyncMessage {
reserved /*senderE164*/ 1; reserved /*senderE164*/ 1;
optional string senderAci = 3; optional string senderAci = 3;
optional uint64 timestamp = 2; optional uint64 timestamp = 2;
optional bytes senderAciBinary = 4; // 16-byte UUID
} }
message FetchLatest { message FetchLatest {
@ -653,7 +529,7 @@ message SyncMessage {
message Keys { message Keys {
reserved /* storageService */ 1; reserved /* storageService */ 1;
reserved /* master */ 2; optional bytes master = 2; // deprecated: this field will be removed in a future release.
optional string accountEntropyPool = 3; optional string accountEntropyPool = 3;
optional bytes mediaRootBackupKey = 4; optional bytes mediaRootBackupKey = 4;
} }
@ -678,7 +554,6 @@ message SyncMessage {
optional string threadAci = 2; optional string threadAci = 2;
optional bytes groupId = 3; optional bytes groupId = 3;
optional Type type = 4; optional Type type = 4;
optional bytes threadAciBinary = 5; // 16-byte UUID
} }
message OutgoingPayment { message OutgoingPayment {
@ -753,7 +628,7 @@ message SyncMessage {
optional bytes rootKey = 1; optional bytes rootKey = 1;
optional bytes adminPasskey = 2; optional bytes adminPasskey = 2;
optional Type type = 3; // defaults to UPDATE optional Type type = 3; // defaults to UPDATE
reserved /*epoch*/ 4; optional bytes epoch = 4;
} }
message CallLogEvent { message CallLogEvent {
@ -850,40 +725,31 @@ message SyncMessage {
} }
} }
oneof content { optional Sent sent = 1;
Sent sent = 1; optional Contacts contacts = 2;
Contacts contacts = 2;
Request request = 4;
Blocked blocked = 6;
Verified verified = 7;
Configuration configuration = 9;
ViewOnceOpen viewOnceOpen = 11;
FetchLatest fetchLatest = 12;
Keys keys = 13;
MessageRequestResponse messageRequestResponse = 14;
OutgoingPayment outgoingPayment = 15;
PniChangeNumber pniChangeNumber = 18;
CallEvent callEvent = 19;
CallLinkUpdate callLinkUpdate = 20;
CallLogEvent callLogEvent = 21;
DeleteForMe deleteForMe = 22;
DeviceNameChange deviceNameChange = 23;
AttachmentBackfillRequest attachmentBackfillRequest = 24;
AttachmentBackfillResponse attachmentBackfillResponse = 25;
}
reserved /*groups*/ 3; reserved /*groups*/ 3;
optional Request request = 4;
// Protobufs don't allow `repeated` fields to be inside of `oneof` so while
// the fields below are mutually exclusive with the rest of the values above
// we have to place them outside of `oneof`.
repeated Read read = 5; repeated Read read = 5;
repeated StickerPackOperation stickerPackOperation = 10; optional Blocked blocked = 6;
repeated Viewed viewed = 16; optional Verified verified = 7;
optional Configuration configuration = 9;
reserved /*pniIdentity*/ 17;
optional bytes padding = 8; optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10;
optional ViewOnceOpen viewOnceOpen = 11;
optional FetchLatest fetchLatest = 12;
optional Keys keys = 13;
optional MessageRequestResponse messageRequestResponse = 14;
optional OutgoingPayment outgoingPayment = 15;
repeated Viewed viewed = 16;
reserved /*pniIdentity*/ 17;
optional PniChangeNumber pniChangeNumber = 18;
optional CallEvent callEvent = 19;
optional CallLinkUpdate callLinkUpdate = 20;
optional CallLogEvent callLogEvent = 21;
optional DeleteForMe deleteForMe = 22;
optional DeviceNameChange deviceNameChange = 23;
optional AttachmentBackfillRequest attachmentBackfillRequest = 24;
optional AttachmentBackfillResponse attachmentBackfillResponse = 25;
} }
message AttachmentPointer { message AttachmentPointer {
@ -935,7 +801,6 @@ message ContactDetails {
optional string number = 1; optional string number = 1;
optional string aci = 9; optional string aci = 9;
optional bytes aciBinary = 13; // 16-byte UUID
optional string name = 2; optional string name = 2;
optional Avatar avatar = 3; optional Avatar avatar = 3;
reserved /* color */ 4; reserved /* color */ 4;
@ -946,7 +811,7 @@ message ContactDetails {
optional uint32 expireTimerVersion = 12; optional uint32 expireTimerVersion = 12;
optional uint32 inboxPosition = 10; optional uint32 inboxPosition = 10;
reserved /* archived */ 11; reserved /* archived */ 11;
// NEXT ID: 14 // NEXT ID: 13
} }
message PaymentAddress { message PaymentAddress {
@ -993,7 +858,6 @@ message BodyRange {
oneof associatedValue { oneof associatedValue {
string mentionAci = 3; string mentionAci = 3;
Style style = 4; Style style = 4;
bytes mentionAciBinary = 5; // 16-byte UUID
} }
} }
@ -1001,7 +865,6 @@ message AddressableMessage {
oneof author { oneof author {
string authorServiceId = 1; string authorServiceId = 1;
string authorE164 = 2; string authorE164 = 2;
bytes authorServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
optional uint64 sentTimestamp = 3; optional uint64 sentTimestamp = 3;
} }
@ -1011,6 +874,5 @@ message ConversationIdentifier {
string threadServiceId = 1; string threadServiceId = 1;
bytes threadGroupId = 2; bytes threadGroupId = 2;
string threadE164 = 3; string threadE164 = 3;
bytes threadServiceIdBinary = 4; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
} }

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: StickerResources.proto // source: StickerResources.proto
package signalpb package signalpb

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: StorageService.proto // source: StorageService.proto
package signalpb package signalpb
@ -1086,9 +1086,7 @@ type ContactRecord struct {
PniSignatureVerified bool `protobuf:"varint,21,opt,name=pniSignatureVerified,proto3" json:"pniSignatureVerified,omitempty"` PniSignatureVerified bool `protobuf:"varint,21,opt,name=pniSignatureVerified,proto3" json:"pniSignatureVerified,omitempty"`
Nickname *ContactRecord_Name `protobuf:"bytes,22,opt,name=nickname,proto3" json:"nickname,omitempty"` Nickname *ContactRecord_Name `protobuf:"bytes,22,opt,name=nickname,proto3" json:"nickname,omitempty"`
Note string `protobuf:"bytes,23,opt,name=note,proto3" json:"note,omitempty"` Note string `protobuf:"bytes,23,opt,name=note,proto3" json:"note,omitempty"`
AvatarColor *AvatarColor `protobuf:"varint,24,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"` AvatarColor *AvatarColor `protobuf:"varint,24,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"` // Next ID: 25
AciBinary []byte `protobuf:"bytes,25,opt,name=aciBinary,proto3" json:"aciBinary,omitempty"` // 16-byte UUID
PniBinary []byte `protobuf:"bytes,26,opt,name=pniBinary,proto3" json:"pniBinary,omitempty"` // 16-byte UUID
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1291,20 +1289,6 @@ func (x *ContactRecord) GetAvatarColor() AvatarColor {
return AvatarColor_A100 return AvatarColor_A100
} }
func (x *ContactRecord) GetAciBinary() []byte {
if x != nil {
return x.AciBinary
}
return nil
}
func (x *ContactRecord) GetPniBinary() []byte {
if x != nil {
return x.PniBinary
}
return nil
}
type GroupV1Record struct { type GroupV1Record struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@ -1401,7 +1385,6 @@ type GroupV2Record struct {
HideStory bool `protobuf:"varint,8,opt,name=hideStory,proto3" json:"hideStory,omitempty"` HideStory bool `protobuf:"varint,8,opt,name=hideStory,proto3" json:"hideStory,omitempty"`
StorySendMode GroupV2Record_StorySendMode `protobuf:"varint,10,opt,name=storySendMode,proto3,enum=signalservice.GroupV2Record_StorySendMode" json:"storySendMode,omitempty"` StorySendMode GroupV2Record_StorySendMode `protobuf:"varint,10,opt,name=storySendMode,proto3,enum=signalservice.GroupV2Record_StorySendMode" json:"storySendMode,omitempty"`
AvatarColor *AvatarColor `protobuf:"varint,11,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"` AvatarColor *AvatarColor `protobuf:"varint,11,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
VerifiedNameHash []byte `protobuf:"bytes,12,opt,name=verifiedNameHash,proto3" json:"verifiedNameHash,omitempty"` // SHA-256 of UTF-8 encoded decrypted group title that was last verified
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1506,13 +1489,6 @@ func (x *GroupV2Record) GetAvatarColor() AvatarColor {
return AvatarColor_A100 return AvatarColor_A100
} }
func (x *GroupV2Record) GetVerifiedNameHash() []byte {
if x != nil {
return x.VerifiedNameHash
}
return nil
}
type Payments struct { type Payments struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
@ -1604,9 +1580,6 @@ type AccountRecord struct {
AvatarColor *AvatarColor `protobuf:"varint,42,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"` AvatarColor *AvatarColor `protobuf:"varint,42,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
BackupTierHistory *AccountRecord_BackupTierHistory `protobuf:"bytes,43,opt,name=backupTierHistory,proto3" json:"backupTierHistory,omitempty"` BackupTierHistory *AccountRecord_BackupTierHistory `protobuf:"bytes,43,opt,name=backupTierHistory,proto3" json:"backupTierHistory,omitempty"`
NotificationProfileManualOverride *AccountRecord_NotificationProfileManualOverride `protobuf:"bytes,44,opt,name=notificationProfileManualOverride,proto3" json:"notificationProfileManualOverride,omitempty"` NotificationProfileManualOverride *AccountRecord_NotificationProfileManualOverride `protobuf:"bytes,44,opt,name=notificationProfileManualOverride,proto3" json:"notificationProfileManualOverride,omitempty"`
NotificationProfileSyncDisabled bool `protobuf:"varint,45,opt,name=notificationProfileSyncDisabled,proto3" json:"notificationProfileSyncDisabled,omitempty"`
AutomaticKeyVerificationDisabled bool `protobuf:"varint,46,opt,name=automaticKeyVerificationDisabled,proto3" json:"automaticKeyVerificationDisabled,omitempty"`
HasSeenAdminDeleteEducationDialog bool `protobuf:"varint,47,opt,name=hasSeenAdminDeleteEducationDialog,proto3" json:"hasSeenAdminDeleteEducationDialog,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1900,27 +1873,6 @@ func (x *AccountRecord) GetNotificationProfileManualOverride() *AccountRecord_No
return nil return nil
} }
func (x *AccountRecord) GetNotificationProfileSyncDisabled() bool {
if x != nil {
return x.NotificationProfileSyncDisabled
}
return false
}
func (x *AccountRecord) GetAutomaticKeyVerificationDisabled() bool {
if x != nil {
return x.AutomaticKeyVerificationDisabled
}
return false
}
func (x *AccountRecord) GetHasSeenAdminDeleteEducationDialog() bool {
if x != nil {
return x.HasSeenAdminDeleteEducationDialog
}
return false
}
type StoryDistributionListRecord struct { type StoryDistributionListRecord struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Identifier []byte `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"` Identifier []byte `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
@ -1929,7 +1881,6 @@ type StoryDistributionListRecord struct {
DeletedAtTimestamp uint64 `protobuf:"varint,4,opt,name=deletedAtTimestamp,proto3" json:"deletedAtTimestamp,omitempty"` DeletedAtTimestamp uint64 `protobuf:"varint,4,opt,name=deletedAtTimestamp,proto3" json:"deletedAtTimestamp,omitempty"`
AllowsReplies bool `protobuf:"varint,5,opt,name=allowsReplies,proto3" json:"allowsReplies,omitempty"` AllowsReplies bool `protobuf:"varint,5,opt,name=allowsReplies,proto3" json:"allowsReplies,omitempty"`
IsBlockList bool `protobuf:"varint,6,opt,name=isBlockList,proto3" json:"isBlockList,omitempty"` IsBlockList bool `protobuf:"varint,6,opt,name=isBlockList,proto3" json:"isBlockList,omitempty"`
RecipientServiceIdsBinary [][]byte `protobuf:"bytes,7,rep,name=recipientServiceIdsBinary,proto3" json:"recipientServiceIdsBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2006,18 +1957,12 @@ func (x *StoryDistributionListRecord) GetIsBlockList() bool {
return false return false
} }
func (x *StoryDistributionListRecord) GetRecipientServiceIdsBinary() [][]byte {
if x != nil {
return x.RecipientServiceIdsBinary
}
return nil
}
type CallLinkRecord struct { type CallLinkRecord struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
RootKey []byte `protobuf:"bytes,1,opt,name=rootKey,proto3" json:"rootKey,omitempty"` RootKey []byte `protobuf:"bytes,1,opt,name=rootKey,proto3" json:"rootKey,omitempty"`
AdminPasskey []byte `protobuf:"bytes,2,opt,name=adminPasskey,proto3" json:"adminPasskey,omitempty"` AdminPasskey []byte `protobuf:"bytes,2,opt,name=adminPasskey,proto3" json:"adminPasskey,omitempty"`
DeletedAtTimestampMs uint64 `protobuf:"varint,3,opt,name=deletedAtTimestampMs,proto3" json:"deletedAtTimestampMs,omitempty"` DeletedAtTimestampMs uint64 `protobuf:"varint,3,opt,name=deletedAtTimestampMs,proto3" json:"deletedAtTimestampMs,omitempty"`
Epoch []byte `protobuf:"bytes,4,opt,name=epoch,proto3,oneof" json:"epoch,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2073,6 +2018,13 @@ func (x *CallLinkRecord) GetDeletedAtTimestampMs() uint64 {
return 0 return 0
} }
func (x *CallLinkRecord) GetEpoch() []byte {
if x != nil {
return x.Epoch
}
return nil
}
type Recipient struct { type Recipient struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Identifier: // Types that are valid to be assigned to Identifier:
@ -2934,7 +2886,6 @@ type AccountRecord_PinnedConversation_Contact struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"` ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"` E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
ServiceIdBinary []byte `protobuf:"bytes,3,opt,name=serviceIdBinary,proto3" json:"serviceIdBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -2983,13 +2934,6 @@ func (x *AccountRecord_PinnedConversation_Contact) GetE164() string {
return "" return ""
} }
func (x *AccountRecord_PinnedConversation_Contact) GetServiceIdBinary() []byte {
if x != nil {
return x.ServiceIdBinary
}
return nil
}
type AccountRecord_NotificationProfileManualOverride_ManuallyEnabled struct { type AccountRecord_NotificationProfileManualOverride_ManuallyEnabled struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
@ -3047,7 +2991,6 @@ type Recipient_Contact struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"` ServiceId string `protobuf:"bytes,1,opt,name=serviceId,proto3" json:"serviceId,omitempty"`
E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"` E164 string `protobuf:"bytes,2,opt,name=e164,proto3" json:"e164,omitempty"`
ServiceIdBinary []byte `protobuf:"bytes,3,opt,name=serviceIdBinary,proto3" json:"serviceIdBinary,omitempty"` // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -3096,13 +3039,6 @@ func (x *Recipient_Contact) GetE164() string {
return "" return ""
} }
func (x *Recipient_Contact) GetServiceIdBinary() []byte {
if x != nil {
return x.ServiceIdBinary
}
return nil
}
var File_StorageService_proto protoreflect.FileDescriptor var File_StorageService_proto protoreflect.FileDescriptor
const file_StorageService_proto_rawDesc = "" + const file_StorageService_proto_rawDesc = "" +
@ -3155,7 +3091,7 @@ const file_StorageService_proto_rawDesc = "" +
"chatFolder\x18\b \x01(\v2\x1f.signalservice.ChatFolderRecordH\x00R\n" + "chatFolder\x18\b \x01(\v2\x1f.signalservice.ChatFolderRecordH\x00R\n" +
"chatFolder\x12V\n" + "chatFolder\x12V\n" +
"\x13notificationProfile\x18\t \x01(\v2\".signalservice.NotificationProfileH\x00R\x13notificationProfileB\b\n" + "\x13notificationProfile\x18\t \x01(\v2\".signalservice.NotificationProfileH\x00R\x13notificationProfileB\b\n" +
"\x06record\"\xd9\b\n" + "\x06record\"\x9d\b\n" +
"\rContactRecord\x12\x10\n" + "\rContactRecord\x12\x10\n" +
"\x03aci\x18\x01 \x01(\tR\x03aci\x12\x12\n" + "\x03aci\x18\x01 \x01(\tR\x03aci\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12\x10\n" + "\x04e164\x18\x02 \x01(\tR\x04e164\x12\x10\n" +
@ -3185,9 +3121,7 @@ const file_StorageService_proto_rawDesc = "" +
"\x14pniSignatureVerified\x18\x15 \x01(\bR\x14pniSignatureVerified\x12=\n" + "\x14pniSignatureVerified\x18\x15 \x01(\bR\x14pniSignatureVerified\x12=\n" +
"\bnickname\x18\x16 \x01(\v2!.signalservice.ContactRecord.NameR\bnickname\x12\x12\n" + "\bnickname\x18\x16 \x01(\v2!.signalservice.ContactRecord.NameR\bnickname\x12\x12\n" +
"\x04note\x18\x17 \x01(\tR\x04note\x12A\n" + "\x04note\x18\x17 \x01(\tR\x04note\x12A\n" +
"\vavatarColor\x18\x18 \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x12\x1c\n" + "\vavatarColor\x18\x18 \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x1a4\n" +
"\taciBinary\x18\x19 \x01(\fR\taciBinary\x12\x1c\n" +
"\tpniBinary\x18\x1a \x01(\fR\tpniBinary\x1a4\n" +
"\x04Name\x12\x14\n" + "\x04Name\x12\x14\n" +
"\x05given\x18\x01 \x01(\tR\x05given\x12\x16\n" + "\x05given\x18\x01 \x01(\tR\x05given\x12\x16\n" +
"\x06family\x18\x02 \x01(\tR\x06family\":\n" + "\x06family\x18\x02 \x01(\tR\x06family\":\n" +
@ -3203,7 +3137,7 @@ const file_StorageService_proto_rawDesc = "" +
"\vwhitelisted\x18\x03 \x01(\bR\vwhitelisted\x12\x1a\n" + "\vwhitelisted\x18\x03 \x01(\bR\vwhitelisted\x12\x1a\n" +
"\barchived\x18\x04 \x01(\bR\barchived\x12\"\n" + "\barchived\x18\x04 \x01(\bR\barchived\x12\"\n" +
"\fmarkedUnread\x18\x05 \x01(\bR\fmarkedUnread\x120\n" + "\fmarkedUnread\x18\x05 \x01(\bR\fmarkedUnread\x120\n" +
"\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xcd\x04\n" + "\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xa1\x04\n" +
"\rGroupV2Record\x12\x1c\n" + "\rGroupV2Record\x12\x1c\n" +
"\tmasterKey\x18\x01 \x01(\fR\tmasterKey\x12\x18\n" + "\tmasterKey\x18\x01 \x01(\fR\tmasterKey\x12\x18\n" +
"\ablocked\x18\x02 \x01(\bR\ablocked\x12 \n" + "\ablocked\x18\x02 \x01(\bR\ablocked\x12 \n" +
@ -3215,8 +3149,7 @@ const file_StorageService_proto_rawDesc = "" +
"\thideStory\x18\b \x01(\bR\thideStory\x12P\n" + "\thideStory\x18\b \x01(\bR\thideStory\x12P\n" +
"\rstorySendMode\x18\n" + "\rstorySendMode\x18\n" +
" \x01(\x0e2*.signalservice.GroupV2Record.StorySendModeR\rstorySendMode\x12A\n" + " \x01(\x0e2*.signalservice.GroupV2Record.StorySendModeR\rstorySendMode\x12A\n" +
"\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x12*\n" + "\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\"7\n" +
"\x10verifiedNameHash\x18\f \x01(\fR\x10verifiedNameHash\"7\n" +
"\rStorySendMode\x12\v\n" + "\rStorySendMode\x12\v\n" +
"\aDEFAULT\x10\x00\x12\f\n" + "\aDEFAULT\x10\x00\x12\f\n" +
"\bDISABLED\x10\x01\x12\v\n" + "\bDISABLED\x10\x01\x12\v\n" +
@ -3225,7 +3158,7 @@ const file_StorageService_proto_rawDesc = "" +
"\">\n" + "\">\n" +
"\bPayments\x12\x18\n" + "\bPayments\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x18\n" +
"\aentropy\x18\x02 \x01(\fR\aentropy\"\x99\x1d\n" + "\aentropy\x18\x02 \x01(\fR\aentropy\"\x8b\x1b\n" +
"\rAccountRecord\x12\x1e\n" + "\rAccountRecord\x12\x1e\n" +
"\n" + "\n" +
"profileKey\x18\x01 \x01(\fR\n" + "profileKey\x18\x01 \x01(\fR\n" +
@ -3270,18 +3203,14 @@ const file_StorageService_proto_rawDesc = "" +
"\x14backupSubscriberData\x18) \x01(\v2..signalservice.AccountRecord.IAPSubscriberDataR\x14backupSubscriberData\x12A\n" + "\x14backupSubscriberData\x18) \x01(\v2..signalservice.AccountRecord.IAPSubscriberDataR\x14backupSubscriberData\x12A\n" +
"\vavatarColor\x18* \x01(\x0e2\x1a.signalservice.AvatarColorH\x02R\vavatarColor\x88\x01\x01\x12\\\n" + "\vavatarColor\x18* \x01(\x0e2\x1a.signalservice.AvatarColorH\x02R\vavatarColor\x88\x01\x01\x12\\\n" +
"\x11backupTierHistory\x18+ \x01(\v2..signalservice.AccountRecord.BackupTierHistoryR\x11backupTierHistory\x12\x8c\x01\n" + "\x11backupTierHistory\x18+ \x01(\v2..signalservice.AccountRecord.BackupTierHistoryR\x11backupTierHistory\x12\x8c\x01\n" +
"!notificationProfileManualOverride\x18, \x01(\v2>.signalservice.AccountRecord.NotificationProfileManualOverrideR!notificationProfileManualOverride\x12H\n" + "!notificationProfileManualOverride\x18, \x01(\v2>.signalservice.AccountRecord.NotificationProfileManualOverrideR!notificationProfileManualOverride\x1a\x86\x02\n" +
"\x1fnotificationProfileSyncDisabled\x18- \x01(\bR\x1fnotificationProfileSyncDisabled\x12J\n" +
" automaticKeyVerificationDisabled\x18. \x01(\bR automaticKeyVerificationDisabled\x12L\n" +
"!hasSeenAdminDeleteEducationDialog\x18/ \x01(\bR!hasSeenAdminDeleteEducationDialog\x1a\xb0\x02\n" +
"\x12PinnedConversation\x12S\n" + "\x12PinnedConversation\x12S\n" +
"\acontact\x18\x01 \x01(\v27.signalservice.AccountRecord.PinnedConversation.ContactH\x00R\acontact\x12&\n" + "\acontact\x18\x01 \x01(\v27.signalservice.AccountRecord.PinnedConversation.ContactH\x00R\acontact\x12&\n" +
"\rlegacyGroupId\x18\x03 \x01(\fH\x00R\rlegacyGroupId\x12(\n" + "\rlegacyGroupId\x18\x03 \x01(\fH\x00R\rlegacyGroupId\x12(\n" +
"\x0egroupMasterKey\x18\x04 \x01(\fH\x00R\x0egroupMasterKey\x1ae\n" + "\x0egroupMasterKey\x18\x04 \x01(\fH\x00R\x0egroupMasterKey\x1a;\n" +
"\aContact\x12\x1c\n" + "\aContact\x12\x1c\n" +
"\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" + "\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12(\n" + "\x04e164\x18\x02 \x01(\tR\x04e164B\f\n" +
"\x0fserviceIdBinary\x18\x03 \x01(\fR\x0fserviceIdBinaryB\f\n" +
"\n" + "\n" +
"identifier\x1a\xf8\x01\n" + "identifier\x1a\xf8\x01\n" +
"\fUsernameLink\x12\x18\n" + "\fUsernameLink\x12\x18\n" +
@ -3329,7 +3258,7 @@ const file_StorageService_proto_rawDesc = "" +
"_hasBackupB\r\n" + "_hasBackupB\r\n" +
"\v_backupTierB\x0e\n" + "\v_backupTierB\x0e\n" +
"\f_avatarColorJ\x04\b\t\x10\n" + "\f_avatarColorJ\x04\b\t\x10\n" +
"J\x04\b\x13\x10\x14J\x04\b\x1c\x10\x1dJ\x04\b\x1f\x10 J\x04\b$\x10%J\x04\b%\x10&J\x04\b&\x10'\"\xb9\x02\n" + "J\x04\b\x13\x10\x14J\x04\b\x1c\x10\x1dJ\x04\b\x1f\x10 J\x04\b$\x10%J\x04\b%\x10&J\x04\b&\x10'\"\xfb\x01\n" +
"\x1bStoryDistributionListRecord\x12\x1e\n" + "\x1bStoryDistributionListRecord\x12\x1e\n" +
"\n" + "\n" +
"identifier\x18\x01 \x01(\fR\n" + "identifier\x18\x01 \x01(\fR\n" +
@ -3338,20 +3267,20 @@ const file_StorageService_proto_rawDesc = "" +
"\x13recipientServiceIds\x18\x03 \x03(\tR\x13recipientServiceIds\x12.\n" + "\x13recipientServiceIds\x18\x03 \x03(\tR\x13recipientServiceIds\x12.\n" +
"\x12deletedAtTimestamp\x18\x04 \x01(\x04R\x12deletedAtTimestamp\x12$\n" + "\x12deletedAtTimestamp\x18\x04 \x01(\x04R\x12deletedAtTimestamp\x12$\n" +
"\rallowsReplies\x18\x05 \x01(\bR\rallowsReplies\x12 \n" + "\rallowsReplies\x18\x05 \x01(\bR\rallowsReplies\x12 \n" +
"\visBlockList\x18\x06 \x01(\bR\visBlockList\x12<\n" + "\visBlockList\x18\x06 \x01(\bR\visBlockList\"\xa7\x01\n" +
"\x19recipientServiceIdsBinary\x18\a \x03(\fR\x19recipientServiceIdsBinary\"\x88\x01\n" +
"\x0eCallLinkRecord\x12\x18\n" + "\x0eCallLinkRecord\x12\x18\n" +
"\arootKey\x18\x01 \x01(\fR\arootKey\x12\"\n" + "\arootKey\x18\x01 \x01(\fR\arootKey\x12\"\n" +
"\fadminPasskey\x18\x02 \x01(\fR\fadminPasskey\x122\n" + "\fadminPasskey\x18\x02 \x01(\fR\fadminPasskey\x122\n" +
"\x14deletedAtTimestampMs\x18\x03 \x01(\x04R\x14deletedAtTimestampMsJ\x04\b\x04\x10\x05\"\x90\x02\n" + "\x14deletedAtTimestampMs\x18\x03 \x01(\x04R\x14deletedAtTimestampMs\x12\x19\n" +
"\x05epoch\x18\x04 \x01(\fH\x00R\x05epoch\x88\x01\x01B\b\n" +
"\x06_epoch\"\xe6\x01\n" +
"\tRecipient\x12<\n" + "\tRecipient\x12<\n" +
"\acontact\x18\x01 \x01(\v2 .signalservice.Recipient.ContactH\x00R\acontact\x12&\n" + "\acontact\x18\x01 \x01(\v2 .signalservice.Recipient.ContactH\x00R\acontact\x12&\n" +
"\rlegacyGroupId\x18\x02 \x01(\fH\x00R\rlegacyGroupId\x12(\n" + "\rlegacyGroupId\x18\x02 \x01(\fH\x00R\rlegacyGroupId\x12(\n" +
"\x0egroupMasterKey\x18\x03 \x01(\fH\x00R\x0egroupMasterKey\x1ae\n" + "\x0egroupMasterKey\x18\x03 \x01(\fH\x00R\x0egroupMasterKey\x1a;\n" +
"\aContact\x12\x1c\n" + "\aContact\x12\x1c\n" +
"\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" + "\tserviceId\x18\x01 \x01(\tR\tserviceId\x12\x12\n" +
"\x04e164\x18\x02 \x01(\tR\x04e164\x12(\n" + "\x04e164\x18\x02 \x01(\tR\x04e164B\f\n" +
"\x0fserviceIdBinary\x18\x03 \x01(\fR\x0fserviceIdBinaryB\f\n" +
"\n" + "\n" +
"identifier\"\xe8\x04\n" + "identifier\"\xe8\x04\n" +
"\x10ChatFolderRecord\x12\x1e\n" + "\x10ChatFolderRecord\x12\x1e\n" +
@ -3539,6 +3468,7 @@ func file_StorageService_proto_init() {
file_StorageService_proto_msgTypes[7].OneofWrappers = []any{} file_StorageService_proto_msgTypes[7].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[9].OneofWrappers = []any{} file_StorageService_proto_msgTypes[9].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[11].OneofWrappers = []any{} file_StorageService_proto_msgTypes[11].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[13].OneofWrappers = []any{}
file_StorageService_proto_msgTypes[14].OneofWrappers = []any{ file_StorageService_proto_msgTypes[14].OneofWrappers = []any{
(*Recipient_Contact_)(nil), (*Recipient_Contact_)(nil),
(*Recipient_LegacyGroupId)(nil), (*Recipient_LegacyGroupId)(nil),

View file

@ -140,9 +140,7 @@ message ContactRecord {
Name nickname = 22; Name nickname = 22;
string note = 23; string note = 23;
optional AvatarColor avatarColor = 24; optional AvatarColor avatarColor = 24;
bytes aciBinary = 25; // 16-byte UUID // Next ID: 25
bytes pniBinary = 26; // 16-byte UUID
// Next ID: 27
} }
message GroupV1Record { message GroupV1Record {
@ -172,7 +170,6 @@ message GroupV2Record {
reserved /* storySendEnabled */ 9; reserved /* storySendEnabled */ 9;
StorySendMode storySendMode = 10; StorySendMode storySendMode = 10;
optional AvatarColor avatarColor = 11; optional AvatarColor avatarColor = 11;
bytes verifiedNameHash = 12; // SHA-256 of UTF-8 encoded decrypted group title that was last verified
} }
message Payments { message Payments {
@ -192,7 +189,6 @@ message AccountRecord {
message Contact { message Contact {
string serviceId = 1; string serviceId = 1;
string e164 = 2; string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
oneof identifier { oneof identifier {
@ -295,9 +291,6 @@ message AccountRecord {
optional AvatarColor avatarColor = 42; optional AvatarColor avatarColor = 42;
BackupTierHistory backupTierHistory = 43; BackupTierHistory backupTierHistory = 43;
NotificationProfileManualOverride notificationProfileManualOverride = 44; NotificationProfileManualOverride notificationProfileManualOverride = 44;
bool notificationProfileSyncDisabled = 45;
bool automaticKeyVerificationDisabled = 46;
bool hasSeenAdminDeleteEducationDialog = 47;
} }
message StoryDistributionListRecord { message StoryDistributionListRecord {
@ -307,21 +300,19 @@ message StoryDistributionListRecord {
uint64 deletedAtTimestamp = 4; uint64 deletedAtTimestamp = 4;
bool allowsReplies = 5; bool allowsReplies = 5;
bool isBlockList = 6; bool isBlockList = 6;
repeated bytes recipientServiceIdsBinary = 7; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
message CallLinkRecord { message CallLinkRecord {
bytes rootKey = 1; bytes rootKey = 1;
bytes adminPasskey = 2; bytes adminPasskey = 2;
uint64 deletedAtTimestampMs = 3; uint64 deletedAtTimestampMs = 3;
reserved 4; // was epoch field, never used optional bytes epoch = 4;
} }
message Recipient { message Recipient {
message Contact { message Contact {
string serviceId = 1; string serviceId = 1;
string e164 = 2; string e164 = 2;
bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI)
} }
oneof identifier { oneof identifier {

View file

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: UnidentifiedDelivery.proto // source: UnidentifiedDelivery.proto
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC

View file

@ -5,8 +5,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.11 // protoc-gen-go v1.36.6
// protoc v7.34.1 // protoc v3.21.12
// source: WebSocketResources.proto // source: WebSocketResources.proto
package signalpb package signalpb

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ syntax = "proto3";
package signal.backup; package signal.backup;
option java_package = "org.signal.archive.proto"; option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
option swift_prefix = "BackupProto_"; option swift_prefix = "BackupProto_";
message BackupInfo { message BackupInfo {
@ -68,40 +68,6 @@ message AccountData {
Color color = 3; Color color = 3;
} }
enum SentMediaQuality {
UNKNOWN_QUALITY = 0; // Interpret as "Standard"
STANDARD = 1;
HIGH = 2;
}
message AutoDownloadSettings {
enum AutoDownloadOption {
UNKNOWN = 0; // Interpret as "Never"
NEVER = 1;
WIFI = 2;
WIFI_AND_CELLULAR = 3;
}
AutoDownloadOption images = 1;
AutoDownloadOption audio = 2;
AutoDownloadOption video = 3;
AutoDownloadOption documents = 4;
}
enum AppTheme {
UNKNOWN_APP_THEME = 0; // Interpret as "System"
SYSTEM = 1;
LIGHT = 2;
DARK = 3;
}
enum CallsUseLessDataSetting {
UNKNOWN_CALL_DATA_SETTING = 0; // Interpret as "Never"
NEVER = 1;
MOBILE_DATA_ONLY = 2;
WIFI_AND_MOBILE_DATA = 3;
}
message AccountSettings { message AccountSettings {
bool readReceipts = 1; bool readReceipts = 1;
bool sealedSenderIndicators = 2; bool sealedSenderIndicators = 2;
@ -125,17 +91,6 @@ message AccountData {
bool optimizeOnDeviceStorage = 20; bool optimizeOnDeviceStorage = 20;
// See zkgroup for integer particular values. Unset if backups are not enabled. // See zkgroup for integer particular values. Unset if backups are not enabled.
optional uint64 backupTier = 21; optional uint64 backupTier = 21;
reserved /* showSealedSenderIndicators */ 22;
SentMediaQuality defaultSentMediaQuality = 23;
AutoDownloadSettings autoDownloadSettings = 24;
reserved /* wifiAutoDownloadSettings */ 25;
optional uint32 screenLockTimeoutMinutes = 26; // If unset, consider screen lock to be disabled.
optional bool pinReminders = 27; // If unset, consider pin reminders to be enabled.
AppTheme appTheme = 28; // If unset, treat the same as "Unknown" case
CallsUseLessDataSetting callsUseLessDataSetting = 29; // If unset, treat the same as "Unknown" case
bool allowSealedSenderFromAnyone = 30;
bool allowAutomaticKeyVerification = 31;
bool hasSeenAdminDeleteEducationDialog = 32;
} }
message SubscriberData { message SubscriberData {
@ -156,18 +111,6 @@ message AccountData {
} }
} }
message AndroidSpecificSettings {
enum NavigationBarSize {
UNKNOWN_BAR_SIZE = 0; // Intepret as "Normal"
NORMAL = 1;
COMPACT = 2;
}
bool useSystemEmoji = 1;
bool screenshotSecurity = 2;
NavigationBarSize navigationBarSize = 3; // If unset, treat the same as "Unknown" case
}
bytes profileKey = 1; bytes profileKey = 1;
optional string username = 2; optional string username = 2;
UsernameLink usernameLink = 3; UsernameLink usernameLink = 3;
@ -179,10 +122,6 @@ message AccountData {
AccountSettings accountSettings = 9; AccountSettings accountSettings = 9;
IAPSubscriberData backupsSubscriberData = 10; IAPSubscriberData backupsSubscriberData = 10;
string svrPin = 11; string svrPin = 11;
AndroidSpecificSettings androidSpecificSettings = 12;
string bioText = 13;
string bioEmoji = 14;
optional bytes keyTransparencyData = 15;
} }
message Recipient { message Recipient {
@ -271,7 +210,6 @@ message Contact {
string systemFamilyName = 19; string systemFamilyName = 19;
string systemNickname = 20; string systemNickname = 20;
optional AvatarColor avatarColor = 21; optional AvatarColor avatarColor = 21;
optional bytes keyTransparencyData = 22;
} }
message Group { message Group {
@ -308,7 +246,6 @@ message Group {
bytes inviteLinkPassword = 10; bytes inviteLinkPassword = 10;
bool announcements_only = 12; bool announcements_only = 12;
repeated MemberBanned members_banned = 13; repeated MemberBanned members_banned = 13;
bool terminated = 14;
} }
message GroupAttributeBlob { message GroupAttributeBlob {
@ -333,8 +270,6 @@ message Group {
reserved /*profileKey*/ 3; // This field is ignored in Backups, in favor of Contact frames for members reserved /*profileKey*/ 3; // This field is ignored in Backups, in favor of Contact frames for members
reserved /*presentation*/ 4; // This field is deprecated in the context of static group state reserved /*presentation*/ 4; // This field is deprecated in the context of static group state
uint32 joinedAtVersion = 5; uint32 joinedAtVersion = 5;
string labelEmoji = 6;
string labelString = 7;
} }
message MemberPendingProfileKey { message MemberPendingProfileKey {
@ -367,7 +302,6 @@ message Group {
AccessRequired attributes = 1; AccessRequired attributes = 1;
AccessRequired members = 2; AccessRequired members = 2;
AccessRequired addFromInviteLink = 3; AccessRequired addFromInviteLink = 3;
AccessRequired memberLabel = 4;
} }
} }
@ -410,7 +344,7 @@ message CallLink {
string name = 3; string name = 3;
Restrictions restrictions = 4; Restrictions restrictions = 4;
uint64 expirationMs = 5; uint64 expirationMs = 5;
reserved /*epoch*/ 6; optional bytes epoch = 6; // May be absent/empty for older links
} }
message AdHocCall { message AdHocCall {
@ -468,14 +402,6 @@ message ChatItem {
message DirectionlessMessageDetails { message DirectionlessMessageDetails {
} }
message PinDetails {
uint64 pinnedAtTimestamp = 1;
oneof pinExpiry {
uint64 pinExpiresAtTimestamp = 2; // timestamp when the pin should expire
bool pinNeverExpires = 3;
}
}
uint64 chatId = 1; // conversation id uint64 chatId = 1; // conversation id
uint64 authorId = 2; // recipient id uint64 authorId = 2; // recipient id
uint64 dateSent = 3; uint64 dateSent = 3;
@ -502,11 +428,7 @@ message ChatItem {
GiftBadge giftBadge = 17; GiftBadge giftBadge = 17;
ViewOnceMessage viewOnceMessage = 18; ViewOnceMessage viewOnceMessage = 18;
DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up
Poll poll = 20;
AdminDeletedMessage adminDeletedMessage = 22;
} }
PinDetails pinDetails = 21; // only set if message is pinned
} }
message SendStatus { message SendStatus {
@ -837,7 +759,6 @@ message Quote {
NORMAL = 1; NORMAL = 1;
GIFT_BADGE = 2; GIFT_BADGE = 2;
VIEW_ONCE = 3; VIEW_ONCE = 3;
POLL = 4;
} }
message QuotedAttachment { message QuotedAttachment {
@ -884,30 +805,6 @@ message Reaction {
uint64 sortOrder = 4; uint64 sortOrder = 4;
} }
message Poll {
message PollOption {
message PollVote {
uint64 voterId = 1; // A direct reference to Recipient proto id. Must be self or contact.
uint32 voteCount = 2; // Tracks how many times you voted.
}
string option = 1; // Between 1-100 characters
repeated PollVote votes = 2;
}
string question = 1; // Between 1-100 characters
bool allowMultiple = 2;
repeated PollOption options = 3; // At least two
bool hasEnded = 4;
repeated Reaction reactions = 5;
}
message AdminDeletedMessage {
uint64 adminId = 1; // id of the admin that deleted the message
}
message ChatUpdateMessage { message ChatUpdateMessage {
// If unset, importers should ignore the update message without throwing an error. // If unset, importers should ignore the update message without throwing an error.
oneof update { oneof update {
@ -920,8 +817,6 @@ message ChatUpdateMessage {
IndividualCall individualCall = 7; IndividualCall individualCall = 7;
GroupCall groupCall = 8; GroupCall groupCall = 8;
LearnedProfileChatUpdate learnedProfileChange = 9; LearnedProfileChatUpdate learnedProfileChange = 9;
PollTerminateUpdate pollTerminate = 10;
PinMessageUpdate pinMessage = 11;
} }
} }
@ -1078,8 +973,6 @@ message GroupChangeChatUpdate {
GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32; GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32;
GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33; GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33;
GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34; GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34;
GroupMemberLabelAccessLevelChangeUpdate groupMemberLabelAccessLevelChangeUpdate = 35;
GroupTerminateChangeUpdate groupTerminateChangeUpdate = 36;
} }
} }
@ -1131,15 +1024,6 @@ message GroupAttributesAccessLevelChangeUpdate {
GroupV2AccessLevel accessLevel = 2; GroupV2AccessLevel accessLevel = 2;
} }
message GroupMemberLabelAccessLevelChangeUpdate {
optional bytes updaterAci = 1;
GroupV2AccessLevel accessLevel = 2;
}
message GroupTerminateChangeUpdate {
optional bytes updaterAci = 1;
}
message GroupAnnouncementOnlyChangeUpdate { message GroupAnnouncementOnlyChangeUpdate {
optional bytes updaterAci = 1; optional bytes updaterAci = 1;
bool isAnnouncementOnly = 2; bool isAnnouncementOnly = 2;
@ -1298,16 +1182,6 @@ message GroupExpirationTimerUpdate {
optional bytes updaterAci = 2; optional bytes updaterAci = 2;
} }
message PollTerminateUpdate {
uint64 targetSentTimestamp = 1;
string question = 2; // Between 1-100 characters
}
message PinMessageUpdate {
uint64 targetSentTimestamp = 1;
uint64 authorId = 2; // recipient id
}
message StickerPack { message StickerPack {
bytes packId = 1; bytes packId = 1;
bytes packKey = 2; bytes packKey = 2;

View file

@ -1,19 +1,19 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
ANDROID_GIT_REVISION=${1:-439760e7732585bfd078d92d93732c04cc31e29e} ANDROID_GIT_REVISION=${1:-62fdf3d1aa9f637729ae67b55aadcc24f38f0117}
DESKTOP_GIT_REVISION=${1:-1b2a3e7b283c32c5654a39da12fc04139fd26dbd} DESKTOP_GIT_REVISION=${1:-203a1cc5e3f9c1533a58caff72e13aa6eaeeddc7}
update_proto() { update_proto() {
case "$1" in case "$1" in
Signal-Android) Signal-Android)
REPO="Signal-Android" REPO="Signal-Android"
prefix="lib/libsignal-service/src/main/protowire/" prefix="libsignal-service/src/main/protowire/"
GIT_REVISION=$ANDROID_GIT_REVISION GIT_REVISION=$ANDROID_GIT_REVISION
;; ;;
Signal-Android-Archive) Signal-Android-App)
REPO="Signal-Android" REPO="Signal-Android"
prefix="lib/archive/src/main/protowire/" prefix="app/src/main/protowire/"
GIT_REVISION=$ANDROID_GIT_REVISION GIT_REVISION=$ANDROID_GIT_REVISION
;; ;;
Signal-Desktop) Signal-Desktop)
@ -34,10 +34,10 @@ update_proto Signal-Android StickerResources.proto
update_proto Signal-Android WebSocketResources.proto update_proto Signal-Android WebSocketResources.proto
update_proto Signal-Android StorageService.proto update_proto Signal-Android StorageService.proto
update_proto Signal-Android-Archive Backup.proto update_proto Signal-Android-App Backup.proto
mv Backup.proto backuppb/Backup.proto mv Backup.proto backuppb/Backup.proto
update_proto Signal-Desktop DeviceName.proto update_proto Signal-Desktop DeviceName.proto
# TODO these were moved to libsignal only update_proto Signal-Desktop UnidentifiedDelivery.proto
#update_proto Signal-Desktop UnidentifiedDelivery.proto # Android has CDSI.proto too, but the types have more generic names (since android uses a different package name)
#update_proto Signal-Desktop ContactDiscovery.proto update_proto Signal-Desktop ContactDiscovery.proto

View file

@ -18,10 +18,11 @@ package signalmeow
import ( import (
"context" "context"
"crypto/hmac"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
mrand "math/rand/v2" mrand "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
@ -53,6 +54,7 @@ const (
StateProvisioningError ProvisioningState = iota StateProvisioningError ProvisioningState = iota
StateProvisioningURLReceived StateProvisioningURLReceived
StateProvisioningDataReceived StateProvisioningDataReceived
StateProvisioningPreKeysRegistered
) )
func (s ProvisioningState) String() string { func (s ProvisioningState) String() string {
@ -63,6 +65,8 @@ func (s ProvisioningState) String() string {
return "StateProvisioningURLReceived" return "StateProvisioningURLReceived"
case StateProvisioningDataReceived: case StateProvisioningDataReceived:
return "StateProvisioningDataReceived" return "StateProvisioningDataReceived"
case StateProvisioningPreKeysRegistered:
return "StateProvisioningPreKeysRegistered"
default: default:
return fmt.Sprintf("ProvisioningState(%d)", s) return fmt.Sprintf("ProvisioningState(%d)", s)
} }
@ -124,8 +128,8 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
username := *provisioningMessage.Number username := *provisioningMessage.Number
password := random.String(22) password := random.String(22)
code := provisioningMessage.ProvisioningCode code := provisioningMessage.ProvisioningCode
aciRegistrationID := mrand.IntN(16383) + 1 aciRegistrationID := mrand.Intn(16383) + 1
pniRegistrationID := mrand.IntN(16383) + 1 pniRegistrationID := mrand.Intn(16383) + 1
aciSignedPreKey := GenerateSignedPreKey(1, aciIdentityKeyPair) aciSignedPreKey := GenerateSignedPreKey(1, aciIdentityKeyPair)
pniSignedPreKey := GenerateSignedPreKey(1, pniIdentityKeyPair) pniSignedPreKey := GenerateSignedPreKey(1, pniIdentityKeyPair)
aciPQLastResortPreKey := GenerateKyberPreKeys(1, 1, aciIdentityKeyPair)[0] aciPQLastResortPreKey := GenerateKyberPreKeys(1, 1, aciIdentityKeyPair)[0]
@ -165,19 +169,24 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
DeviceID: deviceId, DeviceID: deviceId,
Number: *provisioningMessage.Number, Number: *provisioningMessage.Number,
Password: password, Password: password,
MasterKey: provisioningMessage.GetMasterKey(),
AccountEntropyPool: libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()), AccountEntropyPool: libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()),
EphemeralBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetEphemeralBackupKey()), EphemeralBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetEphemeralBackupKey()),
MediaRootBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetMediaRootBackupKey()), MediaRootBackupKey: libsignalgo.BytesToBackupKey(provisioningMessage.GetMediaRootBackupKey()),
} }
if provisioningMessage.GetAccountEntropyPool() != "" { if provisioningMessage.GetAccountEntropyPool() != "" {
data.MasterKey, err = libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()).DeriveSVRKey() var masterKey []byte
masterKey, err = libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()).DeriveSVRKey()
if err != nil { if err != nil {
log.Err(err).Msg("Failed to derive master key from account entropy pool") log.Err(err).Msg("Failed to derive master key from account entropy pool")
} else { } else {
log.Debug().Msg("Derived master key from account entropy pool") log.Debug().Msg("Derived master key from account entropy pool")
} }
} else { if data.MasterKey == nil {
log.Warn().Msg("No account entropy pool in provisioning message") data.MasterKey = masterKey
} else if !hmac.Equal(data.MasterKey, masterKey) {
log.Warn().Msg("Master key mismatch")
}
} }
// Store the provisioning data // Store the provisioning data
@ -241,6 +250,28 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev
// Return the provisioning data // Return the provisioning data
c <- ProvisioningResponse{State: StateProvisioningDataReceived, ProvisioningData: data} c <- ProvisioningResponse{State: StateProvisioningDataReceived, ProvisioningData: data}
// Generate, store, and register prekeys
// TODO hacky client construction
cli := &Client{Store: device}
err = cli.GenerateAndRegisterPreKeys(ctx, device.ACIPreKeyStore)
if err != nil {
c <- ProvisioningResponse{
State: StateProvisioningError,
Err: fmt.Errorf("error generating and registering ACI prekeys: %w", err),
}
return
}
err = cli.GenerateAndRegisterPreKeys(ctx, device.PNIPreKeyStore)
if err != nil {
c <- ProvisioningResponse{
State: StateProvisioningError,
Err: fmt.Errorf("error generating and registering PNI prekeys: %w", err),
}
return
}
c <- ProvisioningResponse{State: StateProvisioningPreKeysRegistered}
}() }()
return c return c
} }
@ -268,7 +299,7 @@ func startProvisioning(ctx context.Context, ws *websocket.Conn, provisioningCiph
return "", fmt.Errorf("failed to unmarshal provisioning UUID: %w", err) return "", fmt.Errorf("failed to unmarshal provisioning UUID: %w", err)
} }
linkCapabilities := []string{"backup4,backup5"} linkCapabilities := []string{"backup4"}
if !allowBackup { if !allowBackup {
linkCapabilities = []string{} linkCapabilities = []string{}
} }
@ -329,25 +360,42 @@ func continueProvisioning(ctx context.Context, ws *websocket.Conn, provisioningC
var signalCapabilities = map[string]any{ var signalCapabilities = map[string]any{
"attachmentBackfill": true, "attachmentBackfill": true,
"spqr": true, "sqpr": true,
} }
var signalCapabilitiesBody = exerrors.Must(json.Marshal(signalCapabilities)) var signalCapabilitiesBody = exerrors.Must(json.Marshal(signalCapabilities))
func (cli *Client) RegisterCapabilities(ctx context.Context) error { func (cli *Client) RegisterCapabilities(ctx context.Context) error {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/devices/capabilities", signalCapabilitiesBody, nil) username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, "/v1/devices/capabilities", &web.HTTPReqOpt{
Body: signalCapabilitiesBody,
Username: &username,
Password: &password,
ContentType: web.ContentTypeJSON,
})
if resp != nil {
_ = resp.Body.Close()
}
if err != nil { if err != nil {
return err return err
} else if resp.StatusCode >= 400 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
} }
return web.DecodeWSResponseBody(ctx, nil, resp) return nil
} }
func (cli *Client) Unlink(ctx context.Context) error { func (cli *Client) Unlink(ctx context.Context) error {
resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodDelete, fmt.Sprintf("/v1/devices/%d", cli.Store.DeviceID), nil, nil) username, password := cli.Store.BasicAuthCreds()
resp, err := web.SendHTTPRequest(ctx, http.MethodDelete, fmt.Sprintf("/v1/devices/%d", cli.Store.DeviceID), &web.HTTPReqOpt{
Username: &username,
Password: &password,
})
if err != nil { if err != nil {
return err return err
} else if resp.StatusCode >= 400 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
} }
return web.DecodeWSResponseBody(ctx, nil, resp) return nil
} }
func confirmDevice( func confirmDevice(

View file

@ -19,9 +19,9 @@ package signalmeow
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web" "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
) )
@ -36,21 +36,30 @@ type ReqRegisterAPNs struct {
} }
func (cli *Client) registerPush(ctx context.Context, pushType string, data any) error { func (cli *Client) registerPush(ctx context.Context, pushType string, data any) error {
var resp *signalpb.WebSocketResponseMessage username, password := cli.Store.BasicAuthCreds()
var err error req := &web.HTTPReqOpt{
Username: &username,
Password: &password,
}
var method string
if data != nil { if data != nil {
body, err := json.Marshal(data) method = http.MethodPut
req.ContentType = web.ContentTypeJSON
var err error
req.Body, err = json.Marshal(data)
if err != nil { if err != nil {
return err return err
} }
resp, err = cli.AuthedWS.SendRequest(ctx, http.MethodPut, "/v1/accounts/"+pushType, body, nil)
} else { } else {
resp, err = cli.AuthedWS.SendRequest(ctx, http.MethodDelete, "/v1/accounts/"+pushType, nil, nil) method = http.MethodDelete
} }
resp, err := web.SendHTTPRequest(ctx, method, "/v1/accounts/"+pushType, req)
if err != nil { if err != nil {
return err return err
} else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
} }
return web.DecodeWSResponseBody(ctx, nil, resp) return nil
} }
func (cli *Client) RegisterFCM(ctx context.Context, token string) error { func (cli *Client) RegisterFCM(ctx context.Context, token string) error {

View file

@ -29,7 +29,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/libsignalgo"
@ -47,7 +46,6 @@ const (
SignalConnectionEventDisconnected SignalConnectionEventDisconnected
SignalConnectionEventLoggedOut SignalConnectionEventLoggedOut
SignalConnectionEventError SignalConnectionEventError
SignalConnectionEventFatalError
SignalConnectionCleanShutdown SignalConnectionCleanShutdown
) )
@ -58,7 +56,6 @@ var signalConnectionEventNames = map[SignalConnectionEvent]string{
SignalConnectionEventDisconnected: "SignalConnectionEventDisconnected", SignalConnectionEventDisconnected: "SignalConnectionEventDisconnected",
SignalConnectionEventLoggedOut: "SignalConnectionEventLoggedOut", SignalConnectionEventLoggedOut: "SignalConnectionEventLoggedOut",
SignalConnectionEventError: "SignalConnectionEventError", SignalConnectionEventError: "SignalConnectionEventError",
SignalConnectionEventFatalError: "SignalConnectionEventFatalError",
SignalConnectionCleanShutdown: "SignalConnectionCleanShutdown", SignalConnectionCleanShutdown: "SignalConnectionCleanShutdown",
} }
@ -167,8 +164,6 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
//StopReceiveLoops(d) //StopReceiveLoops(d)
case web.SignalWebsocketConnectionEventError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Authed websocket error") log.Err(status.Err).Msg("Authed websocket error")
case web.SignalWebsocketConnectionEventFatalError:
log.Err(status.Err).Msg("Authed websocket fatal error")
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
log.Info().Msg("Authed websocket clean shutdown") log.Info().Msg("Authed websocket clean shutdown")
} }
@ -197,8 +192,6 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
log.Err(status.Err).Msg("Unauthed websocket logged out ** THIS SHOULD BE IMPOSSIBLE **") log.Err(status.Err).Msg("Unauthed websocket logged out ** THIS SHOULD BE IMPOSSIBLE **")
case web.SignalWebsocketConnectionEventError: case web.SignalWebsocketConnectionEventError:
log.Err(status.Err).Msg("Unauthed websocket error") log.Err(status.Err).Msg("Unauthed websocket error")
case web.SignalWebsocketConnectionEventFatalError:
log.Err(status.Err).Msg("Unauthed websocket fatal error")
case web.SignalWebsocketConnectionEventCleanShutdown: case web.SignalWebsocketConnectionEventCleanShutdown:
log.Info().Msg("Unauthed websocket clean shutdown") log.Info().Msg("Unauthed websocket clean shutdown")
} }
@ -228,11 +221,6 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
Event: SignalConnectionEventError, Event: SignalConnectionEventError,
Err: currentStatus.Err, Err: currentStatus.Err,
} }
} else if currentStatus.Event == web.SignalWebsocketConnectionEventFatalError {
statusToSend = SignalConnectionStatus{
Event: SignalConnectionEventFatalError,
Err: currentStatus.Err,
}
} else if currentStatus.Event == web.SignalWebsocketConnectionEventCleanShutdown { } else if currentStatus.Event == web.SignalWebsocketConnectionEventCleanShutdown {
statusToSend = SignalConnectionStatus{ statusToSend = SignalConnectionStatus{
Event: SignalConnectionCleanShutdown, Event: SignalConnectionCleanShutdown,
@ -250,23 +238,12 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
cli.loopWg.Add(1) cli.loopWg.Add(1)
go func() { go func() {
defer cli.loopWg.Done() defer cli.loopWg.Done()
for {
select { select {
case <-loopCtx.Done(): case <-loopCtx.Done():
return return
case <-initialConnectChan: case <-initialConnectChan:
log.Info().Msg("Both websockets connected, sending contacts sync request") log.Info().Msg("Both websockets connected, sending contacts sync request")
err = cli.RegisterCapabilities(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to register capabilities")
} else {
zerolog.Ctx(ctx).Debug().Msg("Successfully registered capabilities")
}
// Start loop to check for and upload more prekeys
cli.loopWg.Add(1)
go func() {
defer cli.loopWg.Done()
cli.keyCheckLoop(loopCtx)
}()
// TODO hacky // TODO hacky
if cli.SyncContactsOnConnect { if cli.SyncContactsOnConnect {
cli.SendContactSyncRequest(loopCtx) cli.SendContactSyncRequest(loopCtx)
@ -274,15 +251,19 @@ func (cli *Client) StartReceiveLoops(ctx context.Context) (chan SignalConnection
if cli.Store.MasterKey == nil { if cli.Store.MasterKey == nil {
cli.SendStorageMasterKeyRequest(loopCtx) cli.SendStorageMasterKeyRequest(loopCtx)
} }
return
}
} }
}() }()
return statusChan, nil // Start loop to check for and upload more prekeys
} cli.loopWg.Add(1)
go func() {
defer cli.loopWg.Done()
cli.keyCheckLoop(loopCtx)
}()
func (cli *Client) ForceReconnect() { return statusChan, nil
cli.AuthedWS.ForceReconnect()
cli.UnauthedWS.ForceReconnect()
} }
func (cli *Client) StopReceiveLoops() error { func (cli *Client) StopReceiveLoops() error {
@ -357,25 +338,21 @@ func (cli *Client) incomingAPIMessageHandler(ctx context.Context, req *signalpb.
return nil, err return nil, err
} }
log = log.With(). log = log.With().
Uint64("envelope_timestamp", envelope.GetClientTimestamp()). Uint64("envelope_timestamp", envelope.GetTimestamp()).
Uint64("server_timestamp", envelope.GetServerTimestamp()). Uint64("server_timestamp", envelope.GetServerTimestamp()).
Logger() Logger()
ctx = log.WithContext(ctx) ctx = log.WithContext(ctx)
destinationServiceID, _ := ParseStringOrBinaryServiceID(envelope.GetDestinationServiceId(), envelope.GetDestinationServiceIdBinary()) destinationServiceID, err := libsignalgo.ServiceIDFromString(envelope.GetDestinationServiceId())
sourceServiceID, _ := ParseStringOrBinaryServiceID(envelope.GetSourceServiceId(), envelope.GetSourceServiceIdBinary())
log.Debug(). log.Debug().
Str("destination_service_id", envelope.GetDestinationServiceId()). Str("destination_service_id", envelope.GetDestinationServiceId()).
Str("source_service_id", envelope.GetSourceServiceId()). Str("source_service_id", envelope.GetSourceServiceId()).
Hex("destination_service_id_bytes", envelope.GetDestinationServiceIdBinary()). Uint32("source_device_id", envelope.GetSourceDevice()).
Hex("source_service_id_bytes", envelope.GetSourceServiceIdBinary()).
Uint32("source_device_id", envelope.GetSourceDeviceId()).
Object("parsed_destination_service_id", destinationServiceID). Object("parsed_destination_service_id", destinationServiceID).
Object("parsed_source_service_id", sourceServiceID).
Int32("envelope_type_id", int32(envelope.GetType())). Int32("envelope_type_id", int32(envelope.GetType())).
Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]). Str("envelope_type", signalpb.Envelope_Type_name[int32(envelope.GetType())]).
Msg("Received envelope") Msg("Received envelope")
result := cli.decryptEnvelope(ctx, envelope, sourceServiceID, destinationServiceID) result := cli.decryptEnvelope(ctx, envelope)
err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID) err = cli.handleDecryptedResult(ctx, result, envelope, destinationServiceID)
if err != nil { if err != nil {
@ -407,7 +384,7 @@ func (cli *Client) handleDecryptedResult(
result DecryptionResult, result DecryptionResult,
envelope *signalpb.Envelope, envelope *signalpb.Envelope,
destinationServiceID libsignalgo.ServiceID, destinationServiceID libsignalgo.ServiceID,
) (retErr error) { ) error {
if errors.Is(result.Err, context.Canceled) { if errors.Is(result.Err, context.Canceled) {
return result.Err return result.Err
} else if ctx.Err() != nil { } else if ctx.Err() != nil {
@ -429,58 +406,37 @@ func (cli *Client) handleDecryptedResult(
}() }()
} }
var theirServiceID libsignalgo.ServiceID
var err error
if result.SenderAddress == nil {
log.Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("No sender address received")
return nil
} else if theirServiceID, err = result.SenderAddress.NameServiceID(); err != nil {
log.Warn().
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("Failed to get sender name as service ID")
return fmt.Errorf("failed to get sender name as service ID: %w", err)
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().
Any("their_service_id", theirServiceID).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Msg("Dropping message from non-ACI sender")
return nil
}
cli.Store.RecipientStore.MarkUnregistered(ctx, theirServiceID, false)
handlerSuccess := true handlerSuccess := true
defer func() {
if retErr == nil && !handlerSuccess {
retErr = ErrHandlerFailed
}
}()
// result.Err is set if there was an error during decryption and we // result.Err is set if there was an error during decryption and we
// should notifiy the user that the message could not be decrypted // should notifiy the user that the message could not be decrypted
if result.Err != nil { if result.Err != nil {
logEvt := log.Err(result.Err).
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetTimestamp())
if result.SenderAddress == nil {
logEvt.Msg("Decryption error with unknown sender")
return nil
}
theirServiceID, err := result.SenderAddress.NameServiceID()
if err != nil {
log.Err(err).Msg("Name error handling decryption error")
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().Any("their_service_id", theirServiceID).Msg("Sender ServiceID is not an ACI")
}
if errors.Is(result.Err, EventAlreadyProcessed) { if errors.Is(result.Err, EventAlreadyProcessed) {
logEvt.Discard().Msg("")
log.Debug().Err(result.Err). log.Debug().Err(result.Err).
Bool("urgent", envelope.GetUrgent()). Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint). Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()). Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()). Uint64("client_ts", envelope.GetTimestamp()).
Stringer("sender", theirServiceID). Stringer("sender", theirServiceID).
Msg("Ignoring already processed event") Msg("Ignoring already processed event")
return nil return nil
} }
log.Err(result.Err). logEvt.Stringer("sender", theirServiceID).Msg("Decryption error with known sender")
Bool("urgent", envelope.GetUrgent()).
Stringer("content_hint", result.ContentHint).
Uint64("server_ts", envelope.GetServerTimestamp()).
Uint64("client_ts", envelope.GetClientTimestamp()).
Stringer("sender", theirServiceID).
Msg("Decryption error with known sender")
// Only send decryption error event if the message was urgent, // Only send decryption error event if the message was urgent,
// to prevent spamming errors for typing notifications and whatnot // to prevent spamming errors for typing notifications and whatnot
if envelope.GetUrgent() && if envelope.GetUrgent() &&
@ -489,38 +445,27 @@ func (cli *Client) handleDecryptedResult(
handlerSuccess = cli.handleEvent(&events.DecryptionError{ handlerSuccess = cli.handleEvent(&events.DecryptionError{
Sender: theirServiceID.UUID, Sender: theirServiceID.UUID,
Err: result.Err, Err: result.Err,
Timestamp: envelope.GetClientTimestamp(), Timestamp: envelope.GetTimestamp(),
}) })
} }
if result.Retriable {
go func() {
err := cli.sendRetryRequest(ctx, result, envelope.GetClientTimestamp())
if err != nil {
log.Err(err).Msg("Failed to send retry request in background")
}
}()
}
if !handlerSuccess { if !handlerSuccess {
return ErrHandlerFailed return ErrHandlerFailed
} }
return nil return nil
} }
rawContent := result.Content content := result.Content
if rawContent == nil { if content == nil {
log.Warn().Msg("Decrypted content is nil") log.Warn().Msg("Decrypted content is nil")
return nil return nil
} }
deviceID, _ := result.SenderAddress.DeviceID() name, _ := result.SenderAddress.Name()
log.Trace(). deviceId, _ := result.SenderAddress.DeviceID()
Any("raw_data", rawContent). log.Trace().Any("raw_data", content).Str("sender", name).Uint("sender_device", deviceId).Msg("Raw event data")
Stringer("sender", theirServiceID).
Uint("sender_device", deviceID).
Msg("Raw event data")
newLog := log.With(). newLog := log.With().
Stringer("sender_name", theirServiceID). Str("sender_name", name).
Uint("sender_device_id", deviceID). Uint("sender_device_id", deviceId).
Str("destination_service_id", destinationServiceID.String()). Str("destination_service_id", destinationServiceID.String()).
Logger() Logger()
log = &newLog log = &newLog
@ -529,32 +474,12 @@ func (cli *Client) handleDecryptedResult(
if result.CiphertextHash != nil { if result.CiphertextHash != nil {
logEvt = logEvt.Hex("ciphertext_hash", result.CiphertextHash[:]) logEvt = logEvt.Hex("ciphertext_hash", result.CiphertextHash[:])
} }
logEvt.Bool("unencrypted", result.Unencrypted).Msg("Decrypted message") logEvt.Msg("Decrypted message")
// Handle unencrypted types early and refuse any other unencrypted message
if rawContent.GetDecryptionErrorMessage() != nil {
handlerSuccess = true
dem, err := libsignalgo.DeserializeDecryptionErrorMessage(rawContent.GetDecryptionErrorMessage())
if err != nil {
log.Warn().Err(err).Msg("Failed to unmarshal decryption error message")
} else {
go func() {
err := cli.handleRetryRequest(ctx, result, dem)
if err != nil {
log.Err(err).Msg("Failed to handle decryption error message in background")
}
}()
}
return
} else if result.Unencrypted {
log.Warn().Msg("Unexpected non-decryption-error content in unencrypted message")
return nil
}
// If there's a sender key distribution message, process it // If there's a sender key distribution message, process it
if rawContent.SenderKeyDistributionMessage != nil { if content.GetSenderKeyDistributionMessage() != nil {
log.Debug().Msg("content includes sender key distribution message") log.Debug().Msg("content includes sender key distribution message")
skdm, err := libsignalgo.DeserializeSenderKeyDistributionMessage(rawContent.SenderKeyDistributionMessage) skdm, err := libsignalgo.DeserializeSenderKeyDistributionMessage(content.GetSenderKeyDistributionMessage())
if err != nil { if err != nil {
log.Err(err).Msg("DeserializeSenderKeyDistributionMessage error") log.Err(err).Msg("DeserializeSenderKeyDistributionMessage error")
return err return err
@ -571,137 +496,45 @@ func (cli *Client) handleDecryptedResult(
} }
} }
// If we're getting a message to our PNI, mark it as needing a PNI signature message on the next send theirServiceID, err := result.SenderAddress.NameServiceID()
if err != nil {
log.Err(err).Msg("Name error")
return err
} else if theirServiceID.Type != libsignalgo.ServiceIDTypeACI {
log.Warn().Any("their_service_id", theirServiceID).Msg("Sender ServiceID is not an ACI")
return nil
}
if destinationServiceID == cli.Store.PNIServiceID() { if destinationServiceID == cli.Store.PNIServiceID() {
_, err = cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, theirServiceID.UUID, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) { _, err = cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, theirServiceID.UUID, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) {
if recipient.Whitelisted == nil {
log.Debug().Msg("Marking recipient as not whitelisted")
recipient.Whitelisted = ptr.Ptr(false)
changed = true
}
if !recipient.NeedsPNISignature { if !recipient.NeedsPNISignature {
log.Debug().Msg("Marking recipient as needing PNI signature") log.Debug().Msg("Marking recipient as needing PNI signature")
recipient.NeedsPNISignature = true recipient.NeedsPNISignature = true
changed = true return true, nil
} }
return return false, nil
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Failed to set needs_pni_signature flag after receiving message to PNI service ID") log.Err(err).Msg("Failed to set needs_pni_signature flag after receiving message to PNI service ID")
} }
} }
// If we receive a PNI signature message (because we sent to a PNI earlier), process it if content.GetPniSignatureMessage() != nil {
if rawContent.PniSignatureMessage != nil {
log.Debug().Msg("Content includes PNI signature message") log.Debug().Msg("Content includes PNI signature message")
err = cli.handlePNISignatureMessage(ctx, theirServiceID, rawContent.PniSignatureMessage) err = cli.handlePNISignatureMessage(ctx, theirServiceID, content.GetPniSignatureMessage())
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Hex("pni_raw", rawContent.PniSignatureMessage.GetPni()). Hex("pni_raw", content.GetPniSignatureMessage().GetPni()).
Stringer("aci", theirServiceID.UUID). Stringer("aci", theirServiceID.UUID).
Msg("Failed to verify ACI-PNI mapping") Msg("Failed to verify ACI-PNI mapping")
} }
} }
isBlocked, err := cli.Store.RecipientStore.IsBlocked(ctx, theirServiceID.UUID)
if err != nil {
log.Err(err).Stringer("sender", theirServiceID).Msg("Failed to check if sender is blocked")
}
var sendDeliveryReceipt bool
var deliveryReceiptTS uint64
switch content := rawContent.Content.(type) {
case *signalpb.Content_SyncMessage:
if theirServiceID == cli.Store.ACIServiceID() {
handlerSuccess = cli.handleSyncMessage(ctx, content.SyncMessage, envelope)
}
return nil
case *signalpb.Content_DataMessage:
handlerSuccess, sendDeliveryReceipt = cli.incomingDataMessage(
ctx, content.DataMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp(), isBlocked,
)
deliveryReceiptTS = content.DataMessage.GetTimestamp()
case *signalpb.Content_EditMessage:
handlerSuccess, sendDeliveryReceipt = cli.incomingEditMessage(
ctx, content.EditMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp(), isBlocked,
)
deliveryReceiptTS = content.EditMessage.GetDataMessage().GetTimestamp()
case *signalpb.Content_ReceiptMessage:
if content.ReceiptMessage.GetType() == signalpb.ReceiptMessage_DELIVERY && theirServiceID == cli.Store.ACIServiceID() {
// Ignore delivery receipts from other own devices
return nil
}
handlerSuccess = cli.handleEvent(&events.Receipt{
Sender: theirServiceID.UUID,
Content: content.ReceiptMessage,
})
case *signalpb.Content_TypingMessage:
var groupID types.GroupIdentifier
if content.TypingMessage.GetGroupId() != nil {
gidBytes := content.TypingMessage.GetGroupId()
groupID = types.GroupIdentifier(base64.StdEncoding.EncodeToString(gidBytes))
}
if !isBlocked || groupID != "" {
// No handler success check here, nobody cares if typing notifications are dropped
cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: groupOrUserID(groupID, theirServiceID),
ServerTimestamp: envelope.GetServerTimestamp(),
},
Event: content.TypingMessage,
})
}
case *signalpb.Content_CallMessage:
if !isBlocked && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) {
handlerSuccess = cli.handleEvent(&events.Call{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: theirServiceID.String(),
ServerTimestamp: envelope.GetServerTimestamp(),
},
// CallMessage doesn't have its own timestamp, use one from the envelope
Timestamp: envelope.GetClientTimestamp(),
IsRinging: content.CallMessage.Offer != nil,
})
}
case *signalpb.Content_DecryptionErrorMessage:
// These should've been handled earlier
log.Warn().Msg("Unexpected decryption error message content in decrypted message")
case *signalpb.Content_NullMessage:
// This is intentionally ignored
case *signalpb.Content_StoryMessage:
// This is also ignored for now
default:
if rawContent.PniSignatureMessage == nil && rawContent.SenderKeyDistributionMessage == nil {
log.Warn().Type("content_type", content).Msg("Unrecognized message content type")
}
}
if sendDeliveryReceipt && handlerSuccess {
err = cli.sendDeliveryReceipts(ctx, []uint64{deliveryReceiptTS}, theirServiceID.UUID)
if err != nil {
log.Err(err).Msg("sendDeliveryReceipts error")
}
}
return nil
}
func groupOrUserID(groupID types.GroupIdentifier, userID libsignalgo.ServiceID) string {
if groupID == "" {
return userID.String()
}
return string(groupID)
}
func (cli *Client) handleSyncMessage(ctx context.Context, msg *signalpb.SyncMessage, envelope *signalpb.Envelope) (handlerSuccess bool) {
// TODO: handle more sync messages // TODO: handle more sync messages
handlerSuccess = true if content.SyncMessage != nil {
log := zerolog.Ctx(ctx) if content.SyncMessage.Keys != nil {
switch content := msg.Content.(type) { aep := libsignalgo.AccountEntropyPool(content.SyncMessage.Keys.GetAccountEntropyPool())
case *signalpb.SyncMessage_Keys_: cli.Store.MasterKey = content.SyncMessage.Keys.GetMaster()
aep := libsignalgo.AccountEntropyPool(content.Keys.GetAccountEntropyPool())
if aep != "" { if aep != "" {
aepMasterKey, err := aep.DeriveSVRKey() aepMasterKey, err := aep.DeriveSVRKey()
if err != nil { if err != nil {
@ -717,72 +550,49 @@ func (cli *Client) handleSyncMessage(ctx context.Context, msg *signalpb.SyncMess
} else { } else {
log.Debug().Msg("No account entropy pool in sync message") log.Debug().Msg("No account entropy pool in sync message")
} }
err := cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData) err = cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to save device after receiving master key") log.Err(err).Msg("Failed to save device after receiving master key")
} else { } else {
log.Info().Msg("Received master key") log.Info().Msg("Received master key")
go cli.SyncStorage(ctx) go cli.SyncStorage(ctx)
} }
case *signalpb.SyncMessage_FetchLatest_: } else if content.SyncMessage.GetFetchLatest().GetType() == signalpb.SyncMessage_FetchLatest_STORAGE_MANIFEST {
switch content.FetchLatest.GetType() {
case signalpb.SyncMessage_FetchLatest_STORAGE_MANIFEST:
log.Debug().Msg("Received storage manifest fetch latest notice") log.Debug().Msg("Received storage manifest fetch latest notice")
go cli.SyncStorage(ctx) go cli.SyncStorage(ctx)
default:
log.Debug().
Stringer("fetch_latest_type", content.FetchLatest.GetType()).
Msg("Received unknown fetch latest notice")
} }
case *signalpb.SyncMessage_Sent_: syncSent := content.SyncMessage.GetSent()
syncSent := content.Sent
if syncSent.GetMessage() != nil || syncSent.GetEditMessage() != nil { if syncSent.GetMessage() != nil || syncSent.GetEditMessage() != nil {
syncDestinationServiceID, err := ParseStringOrBinaryServiceID(syncSent.GetDestinationServiceId(), syncSent.GetDestinationServiceIdBinary()) destination := syncSent.DestinationServiceId
if err != nil && !errors.Is(err, ErrEmptyUUIDInput) { var syncDestinationServiceID libsignalgo.ServiceID
if destination != nil {
syncDestinationServiceID, err = libsignalgo.ServiceIDFromString(*destination)
if err != nil {
log.Err(err).Msg("Sync message destination parse error") log.Err(err).Msg("Sync message destination parse error")
return err
} }
if syncSent.GetDestinationE164() != "" && !syncDestinationServiceID.IsEmpty() { if syncSent.GetDestinationE164() != "" {
aci, pni := syncDestinationServiceID.ToACIAndPNI() aci, pni := syncDestinationServiceID.ToACIAndPNI()
_, err = cli.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, syncSent.GetDestinationE164()) _, err = cli.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, syncSent.GetDestinationE164())
if err != nil { if err != nil {
log.Err(err).Msg("Failed to update recipient E164 after receiving sync message") log.Err(err).Msg("Failed to update recipient E164 after receiving sync message")
} }
} }
for _, unident := range syncSent.GetUnidentifiedStatus() {
serviceID, err := ParseStringOrBinaryServiceID(unident.GetDestinationServiceId(), unident.GetDestinationServiceIdBinary())
if err != nil {
log.Err(err).
Str("destination_service_id", unident.GetDestinationServiceId()).
Hex("destination_service_id_bytes", unident.GetDestinationServiceIdBinary()).
Msg("Failed to parse destination service ID of unidentified send")
continue
} }
changed, err := cli.saveSyncPNIIdentityKey(ctx, serviceID, unident.GetDestinationPniIdentityKey()) if destination == nil && syncSent.GetMessage().GetGroupV2() == nil && syncSent.GetEditMessage().GetDataMessage().GetGroupV2() == nil {
if err != nil {
log.Err(err).
Stringer("destination_service_id", serviceID).
Msg("Failed to save PNI identity key from sync message")
} else if changed {
log.Debug().
Stringer("destination_service_id", serviceID).
Msg("Saved new PNI identity key from sync message")
}
}
if syncDestinationServiceID.IsEmpty() && syncSent.GetMessage().GetGroupV2() == nil && syncSent.GetEditMessage().GetDataMessage().GetGroupV2() == nil {
log.Warn().Msg("sync message sent destination is nil") log.Warn().Msg("sync message sent destination is nil")
} else if syncSent.Message != nil { } else if content.SyncMessage.Sent.Message != nil {
// TODO handle expiration start ts, and maybe the sync message ts? // TODO handle expiration start ts, and maybe the sync message ts?
cli.incomingDataMessage(ctx, syncSent.Message, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp(), false) cli.incomingDataMessage(ctx, content.SyncMessage.Sent.Message, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp())
} else if syncSent.EditMessage != nil { } else if content.SyncMessage.Sent.EditMessage != nil {
cli.incomingEditMessage(ctx, syncSent.EditMessage, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp(), false) cli.incomingEditMessage(ctx, content.SyncMessage.Sent.EditMessage, cli.Store.ACI, syncDestinationServiceID, envelope.GetServerTimestamp())
} }
} }
case *signalpb.SyncMessage_Contacts_: if content.SyncMessage.Contacts != nil {
log.Debug().Msg("Recieved sync message contacts") log.Debug().Msg("Recieved sync message contacts")
if content.Contacts.Blob != nil { blob := content.SyncMessage.Contacts.Blob
// TODO roundtrip via disk to save memory if blob != nil {
contactsBytes, err := DownloadAttachmentWithPointer(ctx, content.Contacts.Blob, nil, nil) contactsBytes, err := DownloadAttachmentWithPointer(ctx, blob, nil)
if err != nil { if err != nil {
log.Err(err).Msg("Contacts Sync DownloadAttachment error") log.Err(err).Msg("Contacts Sync DownloadAttachment error")
} }
@ -795,7 +605,7 @@ func (cli *Client) handleSyncMessage(ctx context.Context, msg *signalpb.SyncMess
convertedContacts := make([]*types.Recipient, 0, len(contacts)) convertedContacts := make([]*types.Recipient, 0, len(contacts))
err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error { err = cli.Store.DoContactTxn(ctx, func(ctx context.Context) error {
for i, signalContact := range contacts { for i, signalContact := range contacts {
if (signalContact.Aci == nil || *signalContact.Aci == "") && len(signalContact.AciBinary) != 16 { if signalContact.Aci == nil || *signalContact.Aci == "" {
// TODO lookup PNI via CDSI and store that when ACI is missing? // TODO lookup PNI via CDSI and store that when ACI is missing?
log.Info(). log.Info().
Any("contact", signalContact). Any("contact", signalContact).
@ -818,59 +628,90 @@ func (cli *Client) handleSyncMessage(ctx context.Context, msg *signalpb.SyncMess
}) })
} }
} }
case *signalpb.SyncMessage_DeleteForMe_:
handlerSuccess = cli.handleEvent(&events.DeleteForMe{
Timestamp: envelope.GetClientTimestamp(),
SyncMessage_DeleteForMe: content.DeleteForMe,
})
case *signalpb.SyncMessage_MessageRequestResponse_:
aciUUID, _ := ParseStringOrBinaryUUID(content.MessageRequestResponse.GetThreadAci(), content.MessageRequestResponse.GetThreadAciBinary())
if aciUUID != uuid.Nil && content.MessageRequestResponse.GetType() == signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
_, err := cli.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aciUUID, uuid.Nil, func(recipient *types.Recipient) (changed bool, err error) {
changed = !ptr.Val(recipient.Whitelisted) || recipient.NeedsPNISignature
recipient.Whitelisted = ptr.Ptr(true)
recipient.NeedsPNISignature = false
return
})
if err != nil {
log.Err(err).Msg("Failed to clear needs_pni_signature flag after message request accept")
} }
} if content.SyncMessage.Read != nil {
var groupID *libsignalgo.GroupIdentifier
if len(content.MessageRequestResponse.GroupId) == libsignalgo.GroupIdentifierLength {
groupID = (*libsignalgo.GroupIdentifier)(content.MessageRequestResponse.GroupId)
}
handlerSuccess = cli.handleEvent(&events.MessageRequestResponse{
Timestamp: envelope.GetClientTimestamp(),
ThreadACI: aciUUID,
GroupID: groupID,
Type: content.MessageRequestResponse.GetType(),
Raw: content.MessageRequestResponse,
})
default:
if msg.Read != nil {
handlerSuccess = cli.handleEvent(&events.ReadSelf{ handlerSuccess = cli.handleEvent(&events.ReadSelf{
Timestamp: envelope.GetClientTimestamp(), Timestamp: envelope.GetTimestamp(),
Messages: msg.Read, Messages: content.SyncMessage.GetRead(),
}) })
} }
} if content.SyncMessage.DeleteForMe != nil {
return handlerSuccess = cli.handleEvent(&events.DeleteForMe{
Timestamp: envelope.GetTimestamp(),
SyncMessage_DeleteForMe: content.SyncMessage.DeleteForMe,
})
} }
func (cli *Client) saveSyncPNIIdentityKey(ctx context.Context, serviceID libsignalgo.ServiceID, identityKeyBytes []byte) (bool, error) {
if identityKeyBytes == nil || serviceID.Type != libsignalgo.ServiceIDTypePNI {
return false, nil
} }
identityKey, err := libsignalgo.DeserializeIdentityKey(identityKeyBytes)
sendDeliveryReceipt := true
if content.DataMessage != nil {
handlerSuccess = cli.incomingDataMessage(ctx, content.DataMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp())
} else if content.EditMessage != nil {
handlerSuccess = cli.incomingEditMessage(ctx, content.EditMessage, theirServiceID.UUID, theirServiceID, envelope.GetServerTimestamp())
} else {
sendDeliveryReceipt = false
}
if sendDeliveryReceipt && handlerSuccess {
err = cli.sendDeliveryReceipts(ctx, []uint64{content.DataMessage.GetTimestamp()}, theirServiceID.UUID)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to deserialize PNI identity key: %w", err) log.Err(err).Msg("sendDeliveryReceipts error")
} }
changed, err := cli.Store.IdentityKeyStore.SaveIdentityKey(ctx, serviceID, identityKey)
if err != nil {
return false, fmt.Errorf("failed to save PNI identity key: %w", err)
} }
return changed, nil
if content.TypingMessage != nil {
var groupID types.GroupIdentifier
if content.TypingMessage.GetGroupId() != nil {
gidBytes := content.TypingMessage.GetGroupId()
groupID = types.GroupIdentifier(base64.StdEncoding.EncodeToString(gidBytes))
}
// No handler success check here, nobody cares if typing notifications are dropped
cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: groupOrUserID(groupID, theirServiceID),
ServerTimestamp: envelope.GetServerTimestamp(),
},
Event: content.TypingMessage,
})
}
// DM call message (group call is an opaque callMessage and a groupCallUpdate in a dataMessage)
if content.CallMessage != nil && (content.CallMessage.Offer != nil || content.CallMessage.Hangup != nil) {
handlerSuccess = cli.handleEvent(&events.Call{
Info: events.MessageInfo{
Sender: theirServiceID.UUID,
ChatID: theirServiceID.String(),
ServerTimestamp: envelope.GetServerTimestamp(),
},
// CallMessage doesn't have its own timestamp, use one from the envelope
Timestamp: envelope.GetTimestamp(),
IsRinging: content.CallMessage.Offer != nil,
}) && handlerSuccess
}
// Read and delivery receipts
if content.ReceiptMessage != nil {
if content.GetReceiptMessage().GetType() == signalpb.ReceiptMessage_DELIVERY && theirServiceID == cli.Store.ACIServiceID() {
// Ignore delivery receipts from other own devices
return nil
}
handlerSuccess = cli.handleEvent(&events.Receipt{
Sender: theirServiceID.UUID,
Content: content.ReceiptMessage,
}) && handlerSuccess
}
if !handlerSuccess {
return ErrHandlerFailed
}
return nil
}
func groupOrUserID(groupID types.GroupIdentifier, userID libsignalgo.ServiceID) string {
if groupID == "" {
return userID.String()
}
return string(groupID)
} }
func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsignalgo.ServiceID, msg *signalpb.PniSignatureMessage) error { func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsignalgo.ServiceID, msg *signalpb.PniSignatureMessage) error {
@ -923,14 +764,7 @@ func (cli *Client) handlePNISignatureMessage(ctx context.Context, sender libsign
return nil return nil
} }
func (cli *Client) incomingEditMessage( func (cli *Client) incomingEditMessage(ctx context.Context, editMessage *signalpb.EditMessage, messageSenderACI uuid.UUID, chatRecipient libsignalgo.ServiceID, serverTimestamp uint64) bool {
ctx context.Context,
editMessage *signalpb.EditMessage,
messageSenderACI uuid.UUID,
chatRecipient libsignalgo.ServiceID,
serverTimestamp uint64,
isBlocked bool,
) (handlerSuccess, sendDeliveryReceipt bool) {
// If it's a group message, get the ID and invalidate cache if necessary // If it's a group message, get the ID and invalidate cache if necessary
var groupID types.GroupIdentifier var groupID types.GroupIdentifier
var groupRevision uint32 var groupRevision uint32
@ -942,12 +776,9 @@ func (cli *Client) incomingEditMessage(
groupID, err = cli.StoreMasterKey(ctx, masterKey) groupID, err = cli.StoreMasterKey(ctx, masterKey)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error") zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error")
return return false
} }
groupRevision = editMessage.GetDataMessage().GetGroupV2().GetRevision() groupRevision = editMessage.GetDataMessage().GetGroupV2().GetRevision()
} else if isBlocked {
zerolog.Ctx(ctx).Debug().Msg("Dropping direct message from blocked user")
return true, false
} }
return cli.handleEvent(&events.ChatEvent{ return cli.handleEvent(&events.ChatEvent{
Info: events.MessageInfo{ Info: events.MessageInfo{
@ -957,24 +788,17 @@ func (cli *Client) incomingEditMessage(
ServerTimestamp: serverTimestamp, ServerTimestamp: serverTimestamp,
}, },
Event: editMessage, Event: editMessage,
}), true })
} }
func (cli *Client) incomingDataMessage( func (cli *Client) incomingDataMessage(ctx context.Context, dataMessage *signalpb.DataMessage, messageSenderACI uuid.UUID, chatRecipient libsignalgo.ServiceID, serverTimestamp uint64) bool {
ctx context.Context,
dataMessage *signalpb.DataMessage,
messageSenderACI uuid.UUID,
chatRecipient libsignalgo.ServiceID,
serverTimestamp uint64,
isBlocked bool,
) (handlerSuccess, sendDeliveryReceipt bool) {
// If there's a profile key, save it // If there's a profile key, save it
if dataMessage.ProfileKey != nil { if dataMessage.ProfileKey != nil {
profileKey := libsignalgo.ProfileKey(dataMessage.ProfileKey) profileKey := libsignalgo.ProfileKey(dataMessage.ProfileKey)
err := cli.Store.RecipientStore.StoreProfileKey(ctx, messageSenderACI, profileKey) err := cli.Store.RecipientStore.StoreProfileKey(ctx, messageSenderACI, profileKey)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreProfileKey error") zerolog.Ctx(ctx).Err(err).Msg("StoreProfileKey error")
return return false
} }
} }
@ -989,12 +813,9 @@ func (cli *Client) incomingDataMessage(
groupID, err = cli.StoreMasterKey(ctx, masterKey) groupID, err = cli.StoreMasterKey(ctx, masterKey)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error") zerolog.Ctx(ctx).Err(err).Msg("StoreMasterKey error")
return return false
} }
groupRevision = dataMessage.GetGroupV2().GetRevision() groupRevision = dataMessage.GetGroupV2().GetRevision()
} else if isBlocked {
zerolog.Ctx(ctx).Debug().Msg("Dropping direct message from blocked user")
return true, false
} }
evtInfo := events.MessageInfo{ evtInfo := events.MessageInfo{
@ -1005,17 +826,17 @@ func (cli *Client) incomingDataMessage(
} }
// Hacky special case for group calls to cache the state // Hacky special case for group calls to cache the state
if dataMessage.GroupCallUpdate != nil { if dataMessage.GroupCallUpdate != nil {
isRinging := cli.GroupCache.UpdateActiveCall(groupID, dataMessage.GroupCallUpdate.GetEraId()) isRinging := cli.UpdateActiveCalls(groupID, dataMessage.GroupCallUpdate.GetEraId())
return cli.handleEvent(&events.Call{ return cli.handleEvent(&events.Call{
Info: evtInfo, Info: evtInfo,
Timestamp: dataMessage.GetTimestamp(), Timestamp: dataMessage.GetTimestamp(),
IsRinging: isRinging, IsRinging: isRinging,
}), true })
} else { } else {
return cli.handleEvent(&events.ChatEvent{ return cli.handleEvent(&events.ChatEvent{
Info: evtInfo, Info: evtInfo,
Event: dataMessage, Event: dataMessage,
}), true })
} }
} }

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