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

Compare commits

..
Author SHA1 Message Date
Scott Weber
650c7c243b Remove PNI prekeys and sessions along with ACI 2024-03-31 19:14:57 -04:00
311 changed files with 35329 additions and 99487 deletions

8
.envrc Normal file
View file

@ -0,0 +1,8 @@
if [[ $(uname -s) == "Linux" && $(uname --kernel-version | grep "NixOS") ]]; then
echo "The best OS (NixOS) has been detected. Using nice tools."
if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg="
fi
use flake
fi

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
*.pb.go linguist-generated=true
*.pb.raw binary linguist-generated=true

View file

@ -1,18 +1,7 @@
---
name: Bug report
about: If something is definitely wrong in the bridge (rather than just a setup issue),
file a bug report. Remember to include relevant logs. Asking in the Matrix room first
is strongly recommended.
type: Bug
file a bug report. Remember to include relevant logs.
labels: bug
---
<!-- Include relevant logs, the bridge version and other important details here -->
### Checklist
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!signal version` command output is: ``

View file

@ -1,6 +1,6 @@
---
name: Enhancement request
about: Submit a feature request or other suggestion
type: Feature
labels: enhancement
---

View file

@ -2,23 +2,20 @@ name: Go
on: [push, pull_request]
env:
GOTOOLCHAIN: local
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.26", "1.27"]
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
go-version: ["1.21", "1.22"]
name: Lint ${{ matrix.go-version == '1.22' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
@ -30,33 +27,32 @@ jobs:
run: |
go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/beeper/pre-commit-go/bridgelint@latest
export PATH="$HOME/go/bin:$PATH"
- name: Run pre-commit
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@v3.0.0
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.26", "1.27"]
name: Test ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
go-version: ["1.21", "1.22"]
name: Test ${{ matrix.go-version == '1.22' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v7
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true
#- name: Set up gotestfmt
# uses: GoTestTools/gotestfmt-action@v2
# with:
# token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up gotestfmt
uses: GoTestTools/gotestfmt-action@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install libolm
run: sudo apt-get install libolm-dev
@ -69,5 +65,4 @@ jobs:
run: |
set -euo pipefail
export LIBRARY_PATH=.
#go test -v -json ./... -cover | gotestfmt
go test ./...
go test -v -json ./... -cover | gotestfmt

View file

@ -1,29 +0,0 @@
name: 'Lock old issues'
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
permissions:
issues: write
# pull-requests: write
# discussions: write
concurrency:
group: lock-threads
jobs:
lock-stale:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
id: lock
with:
issue-inactive-days: 90
process-only: issues
- name: Log processed threads
run: |
if [ '${{ steps.lock.outputs.issues }}' ]; then
echo "Issues:" && echo '${{ steps.lock.outputs.issues }}' | jq -r '.[] | "https://github.com/\(.owner)/\(.repo)/issues/\(.issue_number)"'
fi

3
.gitignore vendored
View file

@ -6,7 +6,6 @@
*.log*
/mautrix-signal
/mautrix-signalgo
/start
/libsignal_ffi.a
.idea

View file

@ -1,9 +1,10 @@
include:
- project: 'mautrix/ci'
file: '/gov2-as-default.yml'
file: '/go.yml'
variables:
BUILDER_IMAGE: dock.mau.dev/tulir/gomuks-build-docker/signal
BINARY_NAME: mautrix-signal
# 32-bit arm builds aren't supported
build arm:

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v4.5.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
@ -9,23 +9,16 @@ repos:
- id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.4
rev: v1.0.0-rc.1
hooks:
- id: go-imports
exclude: "pb\\.go$"
args:
- "-local"
- "go.mau.fi/mautrix-signal"
- "-w"
- id: go-vet-mod
# - id: go-staticcheck-repo-mod
#- id: go-staticcheck-repo-mod
# TODO: reenable this and fix all the problems
- repo: https://github.com/beeper/pre-commit-go
rev: v0.5.0
rev: v0.3.0
hooks:
- id: zerolog-ban-msgf
- id: zerolog-use-stringer
- id: bridgelint
language: system
verbose: true

View file

@ -1,325 +1,3 @@
# v26.11 (unreleased)
* Updated libsignal to v0.106.1.
* Added support for sending more than 2048 characters of text to Signal by
uploading the rest of the text as an attachment, like official clients do.
* Added automatic retrying of group message sends if the server returns a 500
or 503 error.
* Added support for encrypting direct media with FLOE ([MSC4571]).
* Fixed memory leak in libsignal store implementation.
[MSC4571]: https://github.com/matrix-org/matrix-spec-proposals/pull/4571
# v26.10
* Updated libsignal to v0.105.0.
* Added support for phonenumberless accounts.
* Added support for starting chats with usernames and username links.
* Added support for blocking and unblocking users on Signal.
* Added support for new view-limited media interface in bridgev2.
* Added proper sender certificate validation.
* Changed storage sync to only sync once if repeated fetch latest notices are
received.
* Fixed contact syncs not happening even when `sync_contacts_on_startup` was
set to true.
* Fixed Signal encryption using wrong timestamp precision for session expiry.
* Fixed sender key sharing not handling removed users properly.
* Fixed handling retry receipts with no sender key info.
* Fixed deadlock in prekey upload loop if the client gets logged out.
* Fixed infinite loop when importing sticker packs with duplicate emoji
shortcuts.
# v26.09
* Bumped minimum Go version to 1.26.
* Updated libsignal to v0.102.2.
* Fixed handling messages with invalid values in formatting body ranges.
* Fixed websocket request retrying not working correctly if the first attempt
times out.
* Fixed another potential race condition with edited message bridging.
# v26.08
* Updated libsignal to v0.100.0
* Added support for handling own profile key changes.
* Changed attachment bridging behavior to match Signal Desktop.
* Fixed edited messages being bridged twice if certain race conditions occurred.
# v26.07
* Updated Docker image to Alpine 3.24.
* Updated libsignal to v0.97.2.
* Added automatic retry when sender key send fails due to missing session.
* Fixed changing poll votes from Matrix.
# v26.06
* Updated libsignal to v0.94.4.
* Updated mrenclave to fix looking up phone numbers.
* Changed knock accept handling to auto-join the ghost user afterwards.
# v26.05
* Updated libsignal to v0.93.2.
* Added support for importing sticker packs from Signal.
# v26.04
* Updated libsignal to v0.92.1.
* Added support for admin message deletes from Signal.
* Added support for binary service IDs in storage service.
* Fixed `private_chat_portal_meta` option not setting DM room names correctly.
* Fixed panic if user is logged out during initial chat sync.
* Fixed avatar upload failing when creating new Signal group.
# v26.03
* Switched to sending binary service ID fields in outgoing messages.
* 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)
* Removed legacy provisioning API and database legacy migration.
Upgrading directly from versions prior to v0.7.0 is not supported.
* If you've been using the bridge since before v0.7.0 and have prevented the
bridge from writing to the config, you must either update the config
manually or allow the bridge to update it for you **before** upgrading to
this release (i.e. run v0.8.6 once with config writing allowed).
* Updated libsignal to v0.80.3.
* Added support for `com.beeper.disappearing_timer` state event, which stores
the disappearing setting of chats and allows changing the setting from Matrix.
* Added support for nicknames in displayname templates.
* Like contact list names, nicknames are not safe to use on multi-user instances.
* Added support for creating Signal groups.
* Fixed certain types of logouts not being detected properly.
# v0.8.6 (2025-08-16)
* Deprecated legacy provisioning API. The `/_matrix/provision/v2` endpoints will
be deleted in the next release.
* Bumped minimum Go version to 1.24.
* Updated libsignal to v0.78.2.
* Added support for "delete to me" of chats and messages.
* Added support for latest Signal backup/transfer protocol.
# v0.8.5 (2025-07-16)
* Updated libsignal to v0.76.1.
# v0.8.4 (2025-06-16)
* Updated libsignal to v0.74.1.
* Updated Docker image to Alpine 3.22.
* Fixed avatars when using direct media.
* Fixed starting chats with non-contact users.
* Fixed Matrix media being rejected if the mime type isn't specified.
# v0.8.3 (2025-05-16)
* Updated libsignal to v0.72.1.
* Added initial support for direct media access.
* Note that media is only kept on the Signal servers for 45 days, after which
any direct media links will permanently stop working.
* Added buffer for decrypted events to prevent losing messages if the bridge is
stopped in the middle of event handling.
* Fixed backfilling messages in existing portals after relogining.
# v0.8.2 (2025-04-16)
* Updated libsignal to v0.70.0.
* Fixed panics in some cases when the bridge was under heavy load.
# v0.8.1 (2025-03-16)
* Added QR refreshing when logging in.
* Updated libsignal to v0.67.4.
# v0.8.0 (2025-02-16)
* Bumped minimum Go version to 1.23.
* Added support for history transfer.
* Updated libsignal to v0.66.2.
# v0.7.5 (2025-01-16)
* Added support for bridging mp4 gifs in both directions.
* Added support for signaling supported features to clients using the
`com.beeper.room_features` state event.
* Updated Signal websocket authentication method.
* Fixed some cases where websocket would get stuck after a ping timeout.
# v0.7.4 (2024-12-16)
* Fixed syncing server-side storage after Signal login.
* Added support for new SSRE2 method of receiving the server-side storage key.
* Updated libsignal to v0.64.1.
* Updated Docker image to Alpine 3.21.
# v0.7.3 (2024-11-16)
* Updated libsignal to v0.62.0.
* Note for bridges running in systemd: the new version of libsignal may be
incompatible with the `MemoryDenyWriteExecute=true` option (see [#750]).
* Added basic support for Signal's new file upload protocol.
[#750]: https://github.com/mautrix/signal/issues/570
# v0.7.2 (2024-10-16)
* Updated to libsignal v0.58.3.
* Fixed spurious decryption error notices for Signal messages when the
websocket reconnects and receives old already-bridged messages.
* Fixed signalmeow not respecting account settings for choosing sender
certificate.
* Fixed bugs in storage service decryption, which could cause issues with
missing contact names among other things.
* Fixed call start notices only working once per direct chat.
# v0.7.1 (2024-09-16)
* Updated to libsignal v0.57.1.
* Dropped support for unauthenticated media on Matrix.
* Added support for Matrix->Signal power level bridging
(thanks to [@maltee1] in [#531]).
* Changed voice message conversion to convert to aac instead of m4a,
because Signal iOS doesn't appear to like ffmpeg's m4a files.
* Fixed outgoing sync messages not including disappearing start timestamp,
which would cause native clients to disappear messages at the wrong time.
* Re-added notices about decryption errors.
[#531]: https://github.com/mautrix/signal/pull/531
# v0.7.0 (2024-08-16)
* Bumped minimum Go version to 1.22.
* Updated to libsignal v0.55.0.
* Rewrote bridge using bridgev2 architecture.
* It is recommended to check the config file after upgrading. If you have
prevented the bridge from writing to the config, you should update it
manually.
* Thanks to [@maltee1] for reimplementing Matrix -> Signal membership
handling in the rewrite.
* If you are still somehow using a pre-v0.5.0 versions, upgrading to v0.6.3
is required before upgrading to v0.7.0 or higher.
# v0.6.3 (2024-07-16)
* Updated to libsignal v0.52.0.
* Fixed bridge losing track of user phone numbers in some cases.
* Fixed edge cases in handling new outgoing DMs started from other devices.
* Added `sync groups` command (thanks to [@maltee1] in [#490]).
* Fixed typo in location bridging example config
(thanks to [@AndrewFerr] in [#516]).
[#490]: https://github.com/mautrix/signal/pull/490
[#516]: https://github.com/mautrix/signal/pull/516
[@AndrewFerr]: https://github.com/mautrix/signal/pull/516
# v0.6.2 (2024-06-16)
* Updated to libsignal v0.51.0.
* Fixed voice messages not being rendered correctly in Element X.
* Fixed contact avatars not being bridged correctly even when enabled in
the bridge config.
* Implemented connector for the upcoming bridgev2 architecture.
# v0.6.1 (2024-05-16)
* Added support for bridging location messages from Matrix to Signal
(thanks to [@maltee1] in [#504]).
* Note that Signal doesn't support real location messages, so they're just
bridged as links. The link template is configurable.
* Fixed bridging long text messages from Signal
(thanks to [@maltee1] in [#506]).
* Improved handling of ping timeouts in Signal websocket.
[#504]: https://github.com/mautrix/signal/pull/504
[#506]: https://github.com/mautrix/signal/pull/506
# v0.6.0 (2024-04-16)
* Updated to libsignal v0.44.0.
* Refactored bridge to support Signal's new phone number identifier (PNI)
system in order to fix starting new chats and receiving messages from new
users.
* When starting a chat with a user you haven't talked to before, the portal
room will not have a ghost user for the recipient until they accept the
message request.
* Added support for syncing existing groups on login instead of having to wait
for new messages.
* Added notices if decrypting incoming message from Signal fails.
* Added bridging of group metadata from Matrix to Signal
(thanks to [@maltee1] in [#461]).
* Added command to create new Signal group for Matrix room
(thanks to [@maltee1] in [#461] and [#491]).
* Added commands for inviting users to Signal groups by phone number
(thanks to [@maltee1] in [#495]).
* Improved handling of missed Signal group metadata changes
(thanks to [@maltee1] in [#488]).
[#461]: https://github.com/mautrix/signal/pull/461
[#488]: https://github.com/mautrix/signal/pull/488
[#491]: https://github.com/mautrix/signal/pull/491
[#495]: https://github.com/mautrix/signal/pull/495
# v0.5.1 (2024-03-16)
* Updated to libsignal v0.41.0.

View file

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

View file

@ -1,17 +1,18 @@
# -- Build libsignal (with Rust) --
FROM rust:1-alpine AS rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev protobuf-dev
FROM rust:1-alpine as rust-builder
RUN apk add --no-cache git make cmake protoc musl-dev g++ clang-dev
WORKDIR /build
# Copy all files needed for Rust build, and no Go files
COPY pkg/libsignalgo/libsignal/. pkg/libsignalgo/libsignal/.
COPY build-rust.sh .
ARG DBG=0
RUN ./build-rust.sh
# -- Build mautrix-signal (with Go) --
FROM golang:1-alpine3.24 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev zlib-dev
FROM golang:1-alpine3.19 AS go-builder
RUN apk add --no-cache git ca-certificates build-base olm-dev
WORKDIR /build
# Copy all files needed for Go build, and no Rust files
@ -19,28 +20,39 @@ COPY *.go go.* *.yaml *.sh ./
COPY pkg/signalmeow/. pkg/signalmeow/.
COPY pkg/libsignalgo/* pkg/libsignalgo/
COPY pkg/libsignalgo/resources/. pkg/libsignalgo/resources/.
COPY pkg/msgconv/. pkg/msgconv/.
COPY pkg/signalid/. pkg/signalid/.
COPY pkg/connector/. pkg/connector/.
COPY cmd/. cmd/.
COPY config/. config/.
COPY database/. database/.
COPY msgconv/. msgconv/.
COPY .git .git
ARG DBG=0
ENV LIBRARY_PATH=.
COPY --from=rust-builder /build/pkg/libsignalgo/libsignal/target/*/libsignal_ffi.a ./
RUN <<EOF
if [ "$DBG" = 1 ]; then
go install github.com/go-delve/delve/cmd/dlv@latest
else
touch /go/bin/dlv
fi
EOF
RUN ./build-go.sh
# -- Run mautrix-signal --
FROM alpine:3.24
FROM alpine:3.19
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go olm
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq olm
COPY --from=go-builder /build/mautrix-signal /usr/bin/mautrix-signal
COPY --from=go-builder /build/example-config.yaml /opt/mautrix-signal/example-config.yaml
COPY --from=go-builder /build/docker-run.sh /docker-run.sh
COPY --from=go-builder /go/bin/dlv /usr/bin/dlv
VOLUME /data
ARG DBG
ARG DBGWAIT=0
ENV DBG=${DBG} DBGWAIT=${DBGWAIT}
RUN echo "Debug mode: DBG=${DBG} DBGWAIT=${DBGWAIT}"
CMD ["/docker-run.sh"]

View file

@ -1,17 +1,14 @@
ARG DOCKER_HUB="docker.io"
FROM ${DOCKER_HUB}/alpine:3.24
FROM alpine:3.19
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq
ARG EXECUTABLE=./mautrix-signal
COPY $EXECUTABLE /usr/bin/mautrix-signal
COPY ./example-config.yaml /opt/mautrix-signal/example-config.yaml
COPY ./docker-run.sh /docker-run.sh
ENV BRIDGEV2=1
VOLUME /data
WORKDIR /data
CMD ["/docker-run.sh"]

28
Makefile Normal file
View file

@ -0,0 +1,28 @@
.PHONY: all build_rust copy_library build_go clean
all: build_rust copy_library build_go
LIBRARY_FILENAME=libsignal_ffi.a
RUST_DIR=pkg/libsignalgo/libsignal
GO_BINARY=mautrix-signal
# TODO fix linking with debug library
#ifneq ($(DBG),1)
RUST_TARGET_SUBDIR=release
#else
#RUST_TARGET_SUBDIR=debug
#endif
build_rust:
./build-rust.sh
copy_library:
cp $(RUST_DIR)/target/$(RUST_TARGET_SUBDIR)/$(LIBRARY_FILENAME) .
build_go:
LIBRARY_PATH="$${LIBRARY_PATH}:." ./build-go.sh
clean:
rm -f ./$(LIBRARY_FILENAME)
cd $(RUST_DIR) && cargo clean
rm -f $(GO_BINARY)

View file

@ -1,18 +1,17 @@
# Features & roadmap
* Matrix → Signal
* [x] Message content
* [ ] Message content
* [x] Text
* [x] Formatting
* [x] Mentions
* [x] Polls
* [x] Media
* [ ] Media
* [x] Images
* [x] Audio files
* [x] Voice messages
* [x] Files
* [x] Gifs
* [x] Locations
* [ ] Locations
* [x] Stickers
* [x] Message edits
* [x] Message reactions
@ -35,7 +34,6 @@
* [x] Text
* [x] Formatting
* [x] Mentions
* [x] Polls
* [ ] Media
* [x] Images
* [x] Voice notes
@ -67,8 +65,8 @@
* [ ] Delivery receipts (there's no good way to bridge these)
* [x] Disappearing messages
* Misc
* [x] Automatic portal creation
* [x] After login
* [ ] Automatic portal creation
* [ ] After login
* [x] When receiving message
* [x] Linking as secondary device
* [ ] Registering as primary device

View file

@ -1,2 +1,9 @@
#!/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 '+%b %_d %Y, %H:%M:%S'`' -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 "$@"

View file

@ -1,3 +1,9 @@
#!/bin/sh
git submodule update --init
cd pkg/libsignalgo/libsignal && RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER="" cargo build -p libsignal-ffi --profile=release
# TODO fix linking with debug library
#if [ "$DBG" != 1 ]; then
# RUST_PROFILE=release
#else
# RUST_PROFILE=dev
#fi
RUST_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
set -e
./build-rust.sh
cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh
git submodule init
git submodule update
make

View file

@ -1,50 +0,0 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is 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 main
import (
"fmt"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
"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.
// These are filled at build time with the -X linker flag.
var (
Tag = "unknown"
Commit = "unknown"
BuildTime = "unknown"
)
var m = mxmain.BridgeMain{
Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.",
Version: "26.10",
SemCalVer: true,
Connector: &connector.SignalConnector{},
}
func main() {
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}

918
commands.go Normal file
View file

@ -0,0 +1,918 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2023 Scott Weber
//
// 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 main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/skip2/go-qrcode"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)
var (
HelpSectionConnectionManagement = commands.HelpSection{Name: "Connection management", Order: 11}
HelpSectionCreatingPortals = commands.HelpSection{Name: "Creating portals", Order: 15}
HelpSectionPortalManagement = commands.HelpSection{Name: "Portal management", Order: 20}
HelpSectionInvites = commands.HelpSection{Name: "Group invites", Order: 25}
HelpSectionMiscellaneous = commands.HelpSection{Name: "Miscellaneous", Order: 30}
)
type WrappedCommandEvent struct {
*commands.Event
Bridge *SignalBridge
User *User
Portal *Portal
}
func (br *SignalBridge) RegisterCommands() {
proc := br.CommandProcessor.(*commands.Processor)
proc.AddHandlers(
cmdPing,
cmdLogin,
cmdSetDeviceName,
cmdPM,
cmdResolvePhone,
cmdSyncSpace,
cmdDeleteSession,
cmdSetRelay,
cmdUnsetRelay,
cmdDeletePortal,
cmdDeleteAllPortals,
cmdCleanupLostPortals,
cmdInviteLink,
cmdResetInviteLink,
cmdCreate,
)
}
func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) {
return func(ce *commands.Event) {
user := ce.User.(*User)
var portal *Portal
if ce.Portal != nil {
portal = ce.Portal.(*Portal)
}
br := ce.Bridge.Child.(*SignalBridge)
handler(&WrappedCommandEvent{ce, br, user, portal})
}
}
var cmdSetRelay = &commands.FullHandler{
Func: wrapCommand(fnSetRelay),
Name: "set-relay",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Relay messages in this room through your Signal account.",
},
RequiresPortal: true,
RequiresLogin: true,
}
func fnSetRelay(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge")
} else {
ce.Portal.RelayUserID = ce.User.MXID
ce.Portal.Update(ce.Ctx)
ce.Reply("Messages from non-logged-in users in this room will now be bridged through your Signal account")
}
}
var cmdUnsetRelay = &commands.FullHandler{
Func: wrapCommand(fnUnsetRelay),
Name: "unset-relay",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Stop relaying messages in this room.",
},
RequiresPortal: true,
}
func fnUnsetRelay(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge")
} else {
ce.Portal.RelayUserID = ""
ce.Portal.Update(ce.Ctx)
ce.Reply("Messages from non-logged-in users will no longer be bridged in this room")
}
}
var cmdDeleteSession = &commands.FullHandler{
Func: wrapCommand(fnDeleteSession),
Name: "delete-session",
Help: commands.HelpMeta{
Section: HelpSectionConnectionManagement,
Description: "Disconnect from Signal, clearing sessions but keeping other data. Reconnect with `login`",
},
}
func fnDeleteSession(ce *WrappedCommandEvent) {
if !ce.User.IsLoggedIn() {
ce.Reply("You're not logged in")
return
}
ce.User.Client.ClearKeysAndDisconnect(ce.Ctx)
ce.Reply("Disconnected from Signal")
}
var cmdPing = &commands.FullHandler{
Func: wrapCommand(fnPing),
Name: "ping",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Check your connection to Signal",
},
}
func fnPing(ce *WrappedCommandEvent) {
if ce.User.SignalID == uuid.Nil {
ce.Reply("You're not logged in")
} else if !ce.User.IsLoggedIn() {
ce.Reply("You were logged in at some point, but are not anymore")
} else if !ce.User.Client.IsConnected() {
ce.Reply("You're logged into Signal, but not connected to the server")
} else {
ce.Reply("You're logged into Signal and probably connected to the server")
}
}
var cmdSetDeviceName = &commands.FullHandler{
Func: wrapCommand(fnSetDeviceName),
Name: "set-device-name",
Help: commands.HelpMeta{
Section: HelpSectionConnectionManagement,
Description: "Set the name of this device in Signal",
Args: "<name>",
},
RequiresLogin: true,
}
func fnSetDeviceName(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `set-device-name <name>`")
return
}
name := strings.Join(ce.Args, " ")
err := ce.User.Client.UpdateDeviceName(ce.Ctx, name)
if err != nil {
ce.Reply("Error setting device name: %v", err)
return
}
ce.Reply("Device name updated")
}
var cmdPM = &commands.FullHandler{
Func: wrapCommand(fnPM),
Name: "pm",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Open a private chat with the given phone number.",
Args: "<_international phone number_>",
},
RequiresLogin: true,
}
var numberCleaner = strings.NewReplacer("-", "", " ", "", "(", "", ")", "", "+", "")
func fnPM(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `pm <international phone number>`")
return
}
number, err := strconv.ParseUint(numberCleaner.Replace(strings.Join(ce.Args, "")), 10, 64)
if err != nil {
ce.Reply("Failed to parse number")
return
}
user := ce.User
var aci, pni uuid.UUID
e164 := fmt.Sprintf("+%d", number)
var recipient *types.Recipient
if recipient, err = user.Client.ContactByE164(ce.Ctx, e164); err != nil {
ce.Reply("Error looking up number in local contact list: %v", err)
return
} else if recipient != nil && (recipient.ACI != uuid.Nil || recipient.PNI != uuid.Nil) {
// TODO maybe lookup PNI if there's only ACI and E164 stored?
aci = recipient.ACI
pni = recipient.PNI
} else if resp, err := user.Client.LookupPhone(ce.Ctx, number); err != nil {
ce.ZLog.Err(err).Uint64("e164", number).Msg("Failed to lookup number on server")
ce.Reply("Error looking up number on server: %v", err)
return
} else {
aci = resp[number].ACI
pni = resp[number].PNI
if aci == uuid.Nil && pni == uuid.Nil {
ce.Reply("+%d doesn't seem to be on Signal", number)
return
}
recipient, err = user.Client.Store.RecipientStore.UpdateRecipientE164(ce.Ctx, aci, pni, e164)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to save recipient entry after looking up phone")
}
aci, pni = recipient.ACI, recipient.PNI
}
ce.ZLog.Debug().
Uint64("e164", number).
Stringer("aci", aci).
Stringer("pni", pni).
Msg("Found DM target user")
var targetServiceID libsignalgo.ServiceID
if aci != uuid.Nil {
targetServiceID = libsignalgo.NewACIServiceID(aci)
} else {
targetServiceID = libsignalgo.NewPNIServiceID(pni)
}
portal := user.GetPortalByChatID(targetServiceID.String())
if portal == nil {
ce.Reply("Couldn't get portal with %s/+%d", targetServiceID, number)
return
} else if portal.MXID != "" {
ok := portal.ensureUserInvited(ce.Ctx, ce.User)
if ok {
ce.Reply("You already have a portal with +%d at [%s](%s)", number, portal.MXID, portal.MXID.URI(portal.bridge.Config.Homeserver.Domain).MatrixToURL())
return
}
ce.ZLog.Warn().Stringer("existing_room_id", portal.MXID).Msg("Ensuring user is invited to existing room failed, creating new room")
portal.Cleanup(ce.Ctx, false)
portal.MXID = ""
}
if err = portal.CreateMatrixRoom(ce.Ctx, user, 0); err != nil {
ce.ZLog.Err(err).Msg("Failed to create portal room")
ce.Reply("Error creating Matrix room for portal to +%d", number)
} else {
ce.Reply("Created portal room [%s](%s) with +%d and invited you to it.", portal.MXID, portal.MXID.URI(portal.bridge.Config.Homeserver.Domain).MatrixToURL(), number)
}
}
var cmdResolvePhone = &commands.FullHandler{
Func: wrapCommand(fnResolvePhone),
Name: "resolve-phone",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Look up phone numbers on the Signal servers.",
Args: "<numbers...>",
},
RequiresLogin: true,
}
func fnResolvePhone(ce *WrappedCommandEvent) {
numbers := make([]uint64, len(ce.Args))
for i, arg := range ce.Args {
var err error
numbers[i], err = strconv.ParseUint(numberCleaner.Replace(arg), 10, 64)
if err != nil {
ce.Reply("Failed to parse number %s: %v", arg, err)
return
}
}
resp, err := ce.User.Client.LookupPhone(ce.Ctx, numbers...)
if err != nil {
ce.Reply("Failed to look up: %v", err)
} else {
var out strings.Builder
for _, phone := range numbers {
result, found := resp[phone]
if found {
_, _ = fmt.Fprintf(&out, "+%d: %s / %s\n", phone, result.ACI, result.PNI)
} else {
_, _ = fmt.Fprintf(&out, "+%d: not found\n", phone)
}
}
ce.Reply(strings.TrimSpace(out.String()))
}
}
var cmdSyncSpace = &commands.FullHandler{
Func: wrapCommand(fnSyncSpace),
Name: "sync-space",
Help: commands.HelpMeta{
Section: HelpSectionMiscellaneous,
Description: "Synchronize your personal filtering space",
},
RequiresLogin: true,
}
func fnSyncSpace(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.PersonalFilteringSpaces {
ce.Reply("Personal filtering spaces are not enabled on this instance of the bridge")
return
}
ctx := ce.Ctx
dmKeys, err := ce.Bridge.DB.Portal.FindPrivateChatsNotInSpace(ctx, ce.User.SignalID)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to get private chat keys")
ce.Reply("Failed to get private chat IDs from database")
return
}
count := 0
allPortals := ce.Bridge.GetAllPortalsWithMXID()
for _, portal := range allPortals {
if portal.IsPrivateChat() {
continue
}
if ce.Bridge.StateStore.IsInRoom(ctx, portal.MXID, ce.User.MXID) && portal.addToPersonalSpace(ctx, ce.User) {
count++
}
}
for _, key := range dmKeys {
portal := ce.Bridge.GetPortalByChatID(key)
portal.addToPersonalSpace(ctx, ce.User)
count++
}
plural := "s"
if count == 1 {
plural = ""
}
ce.Reply("Added %d room%s to space", count, plural)
}
var cmdLogin = &commands.FullHandler{
Func: wrapCommand(fnLogin),
Name: "login",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Link the bridge to your Signal account as a web client.",
},
}
func fnLogin(ce *WrappedCommandEvent) {
if ce.User.IsLoggedIn() {
if ce.User.Client.IsConnected() {
ce.Reply("You're already logged in")
} else {
ce.Reply("You're already logged in, but not connected 🤔")
}
return
}
var qrEventID, msgEventID id.EventID
var signalID uuid.UUID
var signalPhone string
// First get the provisioning URL
provChan, err := ce.User.Login()
if err != nil {
ce.ZLog.Err(err).Msg("Failure logging in")
ce.Reply("Failure logging in: %v", err)
return
}
resp := <-provChan
if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
ce.Reply("Error getting provisioning URL: %v", resp.Err)
return
}
if resp.State == signalmeow.StateProvisioningURLReceived {
qrEventID, msgEventID = ce.User.sendQR(ce, resp.ProvisioningURL, qrEventID, msgEventID)
} else {
ce.Reply("Unexpected state: %v", resp.State)
return
}
// Next, get the results of finishing registration
resp = <-provChan
_, _ = ce.Bot.RedactEvent(ce.Ctx, ce.RoomID, qrEventID)
_, _ = ce.Bot.RedactEvent(ce.Ctx, ce.RoomID, msgEventID)
if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
if resp.Err != nil && strings.HasSuffix(resp.Err.Error(), " EOF") {
ce.Reply("Logging in timed out, please try again.")
} else {
ce.Reply("Error finishing registration: %v", resp.Err)
}
return
}
if resp.State == signalmeow.StateProvisioningDataReceived {
signalID = resp.ProvisioningData.ACI
signalPhone = resp.ProvisioningData.Number
} else {
ce.Reply("Unexpected state: %v", resp.State)
return
}
// Finally, get the results of generating and registering prekeys
resp = <-provChan
if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
ce.Reply("Error with prekeys: %v", resp.Err)
return
} else if resp.State != signalmeow.StateProvisioningPreKeysRegistered {
ce.Reply("Unexpected state: %v", resp.State)
return
}
if signalID == uuid.Nil {
ce.Reply("Problem logging in - No SignalID received")
return
}
ce.User.saveSignalID(ce.Ctx, signalID, signalPhone)
// Connect to Signal
ce.User.Connect()
ce.Reply("Successfully logged in as %s (UUID: %s)", ce.User.SignalUsername, ce.User.SignalID)
}
func (user *User) sendQR(ce *WrappedCommandEvent, code string, prevQR, prevMsg id.EventID) (qr, msg id.EventID) {
content, ok := user.uploadQR(ce, code)
if !ok {
return prevQR, prevMsg
}
if len(prevQR) != 0 {
content.SetEdit(prevQR)
}
resp, err := ce.Bot.SendMessageEvent(ce.Ctx, ce.RoomID, event.EventMessage, &content)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to send QR code to user")
} else if len(prevQR) == 0 {
prevQR = resp.EventID
}
content = event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("Raw linking URI: %s", code),
Format: event.FormatHTML,
FormattedBody: fmt.Sprintf("Raw linking URI: <code>%s</code>", code),
}
if len(prevMsg) != 0 {
content.SetEdit(prevMsg)
}
resp, err = ce.Bot.SendMessageEvent(ce.Ctx, ce.RoomID, event.EventMessage, &content)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to send raw code to user")
} else if len(prevMsg) == 0 {
prevMsg = resp.EventID
}
return prevQR, prevMsg
}
func (user *User) uploadQR(ce *WrappedCommandEvent, code string) (event.MessageEventContent, bool) {
const size = 512
qrCode, err := qrcode.Encode(code, qrcode.Low, size)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to encode QR code")
ce.Reply("Failed to encode QR code: %v", err)
return event.MessageEventContent{}, false
}
bot := user.bridge.AS.BotClient()
resp, err := bot.UploadBytes(ce.Ctx, qrCode, "image/png")
if err != nil {
ce.ZLog.Err(err).Msg("Failed to upload QR code")
ce.Reply("Failed to upload QR code: %v", err)
return event.MessageEventContent{}, false
}
return event.MessageEventContent{
MsgType: event.MsgImage,
Info: &event.FileInfo{
MimeType: "image/png",
Width: size,
Height: size,
Size: len(qrCode),
},
Body: "qr.png",
URL: resp.ContentURI.CUString(),
}, true
}
func canDeletePortal(ctx context.Context, portal *Portal, userID id.UserID) bool {
if len(portal.MXID) == 0 {
return false
}
members, err := portal.MainIntent().JoinedMembers(ctx, portal.MXID)
if err != nil {
portal.log.Err(err).
Stringer("user_id", userID).
Msg("Failed to get joined members to check if user can delete portal")
return false
}
for otherUser := range members.Joined {
_, isPuppet := portal.bridge.ParsePuppetMXID(otherUser)
if isPuppet || otherUser == portal.bridge.Bot.UserID || otherUser == userID {
continue
}
user := portal.bridge.GetUserByMXID(otherUser)
if user != nil && user.IsLoggedIn() {
return false
}
}
return true
}
var cmdDeletePortal = &commands.FullHandler{
Func: wrapCommand(fnDeletePortal),
Name: "delete-portal",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Delete the current portal. If the portal is used by other people, this is limited to bridge admins.",
},
RequiresPortal: true,
}
func fnDeletePortal(ce *WrappedCommandEvent) {
if !ce.User.Admin && !canDeletePortal(ce.Ctx, ce.Portal, ce.User.MXID) {
ce.Reply("Only bridge admins can delete portals with other Matrix users")
return
}
ce.Portal.log.Info().Stringer("user_id", ce.User.MXID).Msg("User requested deletion of portal")
ce.Portal.Delete()
ce.Portal.Cleanup(ce.Ctx, false)
}
var cmdDeleteAllPortals = &commands.FullHandler{
Func: wrapCommand(fnDeleteAllPortals),
Name: "delete-all-portals",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Delete all portals.",
},
}
func fnDeleteAllPortals(ce *WrappedCommandEvent) {
portals := ce.Bridge.GetAllPortalsWithMXID()
var portalsToDelete []*Portal
if ce.User.Admin {
portalsToDelete = portals
} else {
portalsToDelete = portals[:0]
for _, portal := range portals {
if canDeletePortal(ce.Ctx, portal, ce.User.MXID) {
portalsToDelete = append(portalsToDelete, portal)
}
}
}
if len(portalsToDelete) == 0 {
ce.Reply("Didn't find any portals to delete")
return
}
leave := func(portal *Portal) {
if len(portal.MXID) > 0 {
_, _ = portal.MainIntent().KickUser(ce.Ctx, portal.MXID, &mautrix.ReqKickUser{
Reason: "Deleting portal",
UserID: ce.User.MXID,
})
}
}
customPuppet := ce.Bridge.GetPuppetByCustomMXID(ce.User.MXID)
if customPuppet != nil && customPuppet.CustomIntent() != nil {
intent := customPuppet.CustomIntent()
leave = func(portal *Portal) {
if len(portal.MXID) > 0 {
_, _ = intent.LeaveRoom(ce.Ctx, portal.MXID)
_, _ = intent.ForgetRoom(ce.Ctx, portal.MXID)
}
}
}
ce.Reply("Found %d portals, deleting...", len(portalsToDelete))
for _, portal := range portalsToDelete {
portal.Delete()
leave(portal)
}
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
backgroundCtx := context.TODO()
go func() {
for _, portal := range portalsToDelete {
portal.Cleanup(backgroundCtx, false)
}
ce.Reply("Finished background cleanup of deleted portal rooms.")
}()
}
var cmdCleanupLostPortals = &commands.FullHandler{
Func: wrapCommand(fnCleanupLostPortals),
Name: "cleanup-lost-portals",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Clean up portals that were discarded due to the receiver not being logged into the bridge",
},
RequiresAdmin: true,
}
func fnCleanupLostPortals(ce *WrappedCommandEvent) {
portals, err := ce.Bridge.DB.LostPortal.GetAll(ce.Ctx)
if err != nil {
ce.Reply("Failed to get portals: %v", err)
return
} else if len(portals) == 0 {
ce.Reply("No lost portals found")
return
}
ce.Reply("Found %d lost portals, deleting...", len(portals))
for _, portal := range portals {
dmUUID, err := uuid.Parse(portal.ChatID)
intent := ce.Bot
if err == nil {
intent = ce.Bridge.GetPuppetBySignalID(dmUUID).DefaultIntent()
}
ce.Bridge.CleanupRoom(ce.Ctx, ce.ZLog, intent, portal.MXID, false)
err = portal.Delete(ce.Ctx)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to delete lost portal from database after cleanup")
}
}
ce.Reply("Finished cleaning up portals")
}
var cmdInviteLink = &commands.FullHandler{
Func: wrapCommand(fnInviteLink),
Name: "invite-link",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Get the invite link for the corresponding Signal Group",
},
RequiresLogin: true,
}
func fnInviteLink(ce *WrappedCommandEvent) {
if ce.Portal == nil {
ce.Reply("This is not a portal room")
return
}
if ce.Portal.IsPrivateChat() {
ce.Reply("Invite Links are not available for private chats")
return
}
inviteLinkPassword, err := ce.Portal.GetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error getting invite link %w", err)
return
}
ce.Reply(inviteLinkPassword)
}
var cmdResetInviteLink = &commands.FullHandler{
Func: wrapCommand(fnResetInviteLink),
Name: "reset-invite-link",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Generate a new invite link password",
},
RequiresLogin: true,
}
func fnResetInviteLink(ce *WrappedCommandEvent) {
if ce.Portal == nil {
ce.Reply("This is not a portal room")
return
}
if ce.Portal.IsPrivateChat() {
ce.Reply("Invite Links are not available for private chats")
return
}
err := ce.Portal.ResetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error setting new invite link %w", err)
}
inviteLink, err := ce.Portal.GetInviteLink(ce.Ctx, ce.User)
if err != nil {
ce.Reply("Error getting new invite link %w", err)
return
}
ce.Reply(inviteLink)
}
var cmdCreate = &commands.FullHandler{
Func: wrapCommand(fnCreate),
Name: "create",
Help: commands.HelpMeta{
Section: HelpSectionCreatingPortals,
Description: "Create a Signal group chat for the current Matrix room.",
},
RequiresLogin: true,
}
func fnCreate(ce *WrappedCommandEvent) {
if ce.Portal != nil {
ce.Reply("This is already a portal room")
return
}
roomState, err := ce.Bot.State(ce.Ctx, ce.RoomID)
if err != nil {
ce.Reply("Failed to get room state: %w", err)
return
}
members := roomState[event.StateMember]
powerLevelsRaw, ok := roomState[event.StatePowerLevels][""]
if !ok {
ce.Reply("Failed to get room power levels")
return
}
powerLevelsRaw.Content.ParseRaw(event.StatePowerLevels)
powerLevels := powerLevelsRaw.Content.AsPowerLevels()
joinRulesRaw, ok := roomState[event.StateJoinRules][""]
if !ok {
ce.Reply("Failed to get join rules")
return
}
joinRulesRaw.Content.ParseRaw(event.StateJoinRules)
joinRule := joinRulesRaw.Content.AsJoinRules().JoinRule
roomNameEventRaw, ok := roomState[event.StateRoomName][""]
if !ok {
ce.Reply("Failed to get room name")
return
}
roomNameEventRaw.Content.ParseRaw(event.StateRoomName)
roomName := roomNameEventRaw.Content.AsRoomName().Name
if len(roomName) == 0 {
ce.Reply("Please set a name for the room first")
return
}
roomTopic := ""
roomTopicEvent, ok := roomState[event.StateTopic][""]
if ok {
roomTopicEvent.Content.ParseRaw(event.StateTopic)
roomTopic = roomTopicEvent.Content.AsTopic().Topic
}
roomAvatarEvent, ok := roomState[event.StateRoomAvatar][""]
var avatarHash string
var avatarURL id.ContentURI
var avatarBytes []byte
if ok {
roomAvatarEvent.Content.ParseRaw(event.StateRoomAvatar)
avatarURL = roomAvatarEvent.Content.AsRoomAvatar().URL
if !avatarURL.IsEmpty() {
avatarBytes, err = ce.Bot.DownloadBytes(ce.Ctx, avatarURL)
if err != nil {
ce.ZLog.Err(err).Stringer("Failed to download updated avatar %s", avatarURL)
return
}
hash := sha256.Sum256(avatarBytes)
avatarHash = hex.EncodeToString(hash[:])
ce.ZLog.Debug().Stringers("%s set the group avatar to %s", []fmt.Stringer{ce.User.MXID, avatarURL})
}
}
var encryptionEvent *event.EncryptionEventContent
encryptionEventContent, ok := roomState[event.StateEncryption][""]
if ok {
encryptionEventContent.Content.ParseRaw(event.StateEncryption)
encryptionEvent = encryptionEventContent.Content.AsEncryption()
}
var participants []*signalmeow.GroupMember
var bannedMembers []*signalmeow.BannedMember
participantDedup := make(map[uuid.UUID]bool)
participantDedup[uuid.Nil] = true
for key, member := range members {
mxid := id.UserID(key)
member.Content.ParseRaw(event.StateMember)
content := member.Content.AsMember()
membership := content.Membership
var uuid uuid.UUID
puppet := ce.Bridge.GetPuppetByMXID(mxid)
if puppet != nil {
uuid = puppet.SignalID
} else {
user := ce.Bridge.GetUserByMXID(mxid)
if user != nil && user.IsLoggedIn() {
uuid = user.SignalID
}
}
role := signalmeow.GroupMember_DEFAULT
if powerLevels.GetUserLevel(mxid) >= 50 {
role = signalmeow.GroupMember_ADMINISTRATOR
}
if !participantDedup[uuid] {
participantDedup[uuid] = true
// invites should be added on signal and then auto-joined
// joined members that need to be pending-Members should have their signal invite auto-accepted
if membership == event.MembershipJoin || membership == event.MembershipInvite {
participants = append(participants, &signalmeow.GroupMember{
UserID: uuid,
Role: role,
})
} else if membership == event.MembershipBan {
bannedMembers = append(bannedMembers, &signalmeow.BannedMember{
UserID: uuid,
})
}
}
}
addFromInviteLinkAccess := signalmeow.AccessControl_UNSATISFIABLE
if joinRule == event.JoinRulePublic {
addFromInviteLinkAccess = signalmeow.AccessControl_ANY
} else if joinRule == event.JoinRuleKnock {
addFromInviteLinkAccess = signalmeow.AccessControl_ADMINISTRATOR
}
var inviteLinkPassword types.SerializedInviteLinkPassword
if addFromInviteLinkAccess != signalmeow.AccessControl_UNSATISFIABLE {
inviteLinkPassword = signalmeow.GenerateInviteLinkPassword()
}
membersAccess := signalmeow.AccessControl_MEMBER
if powerLevels.Invite() >= 50 {
membersAccess = signalmeow.AccessControl_ADMINISTRATOR
}
attributesAccess := signalmeow.AccessControl_MEMBER
if powerLevels.StateDefault() >= 50 {
attributesAccess = signalmeow.AccessControl_ADMINISTRATOR
}
announcementsOnly := false
if powerLevels.EventsDefault >= 50 {
announcementsOnly = true
}
ce.ZLog.Info().
Str("room_name", roomName).
Any("participants", participants).
Msg("Creating Signal group for Matrix room")
group, err := ce.User.Client.CreateGroupOnServer(ce.Ctx, &signalmeow.Group{
Title: roomName,
Description: roomTopic,
Members: participants,
AccessControl: &signalmeow.GroupAccessControl{
Members: membersAccess,
Attributes: attributesAccess,
AddFromInviteLink: addFromInviteLinkAccess,
},
InviteLinkPassword: &inviteLinkPassword,
BannedMembers: bannedMembers,
AnnouncementsOnly: announcementsOnly,
}, avatarBytes)
if err != nil {
ce.Reply("Failed to create group: %v", err)
return
}
gid := group.GroupIdentifier
ce.ZLog.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("group_id", gid)
})
portal := ce.User.GetPortalByChatID(gid.String())
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if len(portal.MXID) != 0 {
ce.ZLog.Warn().Msg("Detected race condition in room creation")
// TODO race condition, clean up the old room
}
portal.MXID = ce.RoomID
portal.Name = roomName
portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
if !portal.Encrypted && ce.Bridge.Config.Bridge.Encryption.Default {
_, err = portal.MainIntent().SendStateEvent(ce.Ctx, portal.MXID, event.StateEncryption, "", portal.GetEncryptionEventContent())
if err != nil {
ce.ZLog.Err(err).Msg("Failed to enable encryption in room")
if errors.Is(err, mautrix.MForbidden) {
ce.Reply("I don't seem to have permission to enable encryption in this room.")
} else {
ce.Reply("Failed to enable encryption in room: %v", err)
}
}
portal.Encrypted = true
}
revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &signalmeow.GroupChange{}, gid)
if err != nil {
ce.Reply("Failed to update Group")
return
}
portal.Revision = revision
portal.AvatarHash = avatarHash
portal.AvatarURL = avatarURL
portal.AvatarPath = group.AvatarPath
portal.AvatarSet = true
err = portal.Update(ce.Ctx)
if err != nil {
ce.ZLog.Err(err).Msg("Failed to save portal after creating group")
}
portal.UpdateBridgeInfo(ce.Ctx)
ce.Reply("Successfully created Signal group %s", gid.String())
}

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