mirror of
https://github.com/mautrix/signal.git
synced 2026-05-15 05:36:53 -04:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
650c7c243b |
216 changed files with 22875 additions and 41941 deletions
8
.envrc
Normal file
8
.envrc
Normal 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
2
.gitattributes
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*.pb.go linguist-generated=true
|
||||
*.pb.raw binary linguist-generated=true
|
||||
15
.github/ISSUE_TEMPLATE/bug.md
vendored
15
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -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: ``
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: Enhancement request
|
||||
about: Submit a feature request or other suggestion
|
||||
type: Feature
|
||||
labels: enhancement
|
||||
|
||||
---
|
||||
|
|
|
|||
32
.github/workflows/go.yml
vendored
32
.github/workflows/go.yml
vendored
|
|
@ -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.25", "1.26"]
|
||||
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
|
||||
go-version: ["1.21", "1.22"]
|
||||
name: Lint ${{ matrix.go-version == '1.22' && '(latest)' || '(old)' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
|
|
@ -33,29 +30,29 @@ jobs:
|
|||
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.25", "1.26"]
|
||||
name: Test ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
|
||||
go-version: ["1.21", "1.22"]
|
||||
name: Test ${{ matrix.go-version == '1.22' && '(latest)' || '(old)' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
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
|
||||
|
|
@ -68,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
|
||||
|
|
|
|||
29
.github/workflows/stale.yml
vendored
29
.github/workflows/stale.yml
vendored
|
|
@ -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
3
.gitignore
vendored
|
|
@ -6,7 +6,6 @@
|
|||
*.log*
|
||||
|
||||
/mautrix-signal
|
||||
/mautrix-signalgo
|
||||
/start
|
||||
/libsignal_ffi.a
|
||||
|
||||
.idea
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,20 +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
|
||||
# TODO: reenable this and fix all the problems
|
||||
|
||||
- repo: https://github.com/beeper/pre-commit-go
|
||||
rev: v0.4.2
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: zerolog-ban-msgf
|
||||
- id: zerolog-use-stringer
|
||||
|
|
|
|||
257
CHANGELOG.md
257
CHANGELOG.md
|
|
@ -1,260 +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)
|
||||
|
||||
* 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.
|
||||
|
|
|
|||
32
Dockerfile
32
Dockerfile
|
|
@ -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.23 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.23
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
ARG DOCKER_HUB="docker.io"
|
||||
|
||||
FROM ${DOCKER_HUB}/alpine:3.23
|
||||
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
28
Makefile
Normal 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)
|
||||
12
ROADMAP.md
12
ROADMAP.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "$@"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
build.sh
7
build.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,147 +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 istributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request, create bool) {
|
||||
login := m.Matrix.Provisioning.GetLoginForRequest(w, r)
|
||||
if login == nil {
|
||||
return
|
||||
}
|
||||
api := login.Client.(bridgev2.IdentifierResolvingNetworkAPI)
|
||||
phonenum := r.PathValue("phonenum")
|
||||
resp, err := api.ResolveIdentifier(r.Context(), phonenum, create)
|
||||
if err != nil {
|
||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to resolve identifier")
|
||||
JSONResponse(w, http.StatusInternalServerError, &Error{
|
||||
Error: fmt.Sprintf("Failed to resolve identifier: %v", err),
|
||||
ErrCode: "M_UNKNOWN",
|
||||
})
|
||||
return
|
||||
} else if resp == nil {
|
||||
JSONResponse(w, http.StatusNotFound, &Error{
|
||||
ErrCode: mautrix.MNotFound.ErrCode,
|
||||
Error: "User not found on Signal",
|
||||
})
|
||||
return
|
||||
}
|
||||
status := http.StatusOK
|
||||
apiResp := &ResolveIdentifierResponse{
|
||||
ChatID: ResolveIdentifierResponseChatID{
|
||||
UUID: string(resp.UserID),
|
||||
Number: phonenum,
|
||||
},
|
||||
}
|
||||
if resp.Ghost != nil {
|
||||
if resp.UserInfo != nil {
|
||||
resp.Ghost.UpdateInfo(r.Context(), resp.UserInfo)
|
||||
}
|
||||
apiResp.OtherUser = &ResolveIdentifierResponseOtherUser{
|
||||
MXID: resp.Ghost.Intent.GetMXID(),
|
||||
DisplayName: resp.Ghost.Name,
|
||||
AvatarURL: resp.Ghost.AvatarMXC.ParseOrIgnore(),
|
||||
}
|
||||
}
|
||||
if resp.Chat != nil {
|
||||
if resp.Chat.Portal == nil {
|
||||
resp.Chat.Portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
|
||||
if err != nil {
|
||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to get portal")
|
||||
JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
|
||||
Err: "Failed to get portal",
|
||||
ErrCode: "M_UNKNOWN",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if create && resp.Chat.Portal.MXID == "" {
|
||||
apiResp.JustCreated = true
|
||||
status = http.StatusCreated
|
||||
err = resp.Chat.Portal.CreateMatrixRoom(r.Context(), login, resp.Chat.PortalInfo)
|
||||
if err != nil {
|
||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to create portal room")
|
||||
JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
|
||||
Err: "Failed to create portal room",
|
||||
ErrCode: "M_UNKNOWN",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
apiResp.RoomID = resp.Chat.Portal.MXID
|
||||
}
|
||||
JSONResponse(w, status, &Response{
|
||||
Success: true,
|
||||
Status: "ok",
|
||||
ResolveIdentifierResponse: apiResp,
|
||||
})
|
||||
}
|
||||
|
||||
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
|
||||
legacyResolveIdentifierOrStartChat(w, r, false)
|
||||
}
|
||||
|
||||
func legacyProvPM(w http.ResponseWriter, r *http.Request) {
|
||||
legacyResolveIdentifierOrStartChat(w, r, true)
|
||||
}
|
||||
|
||||
func JSONResponse(w http.ResponseWriter, status int, response any) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
ErrCode string `json:"errcode"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Status string `json:"status"`
|
||||
|
||||
// For response in ResolveIdentifier
|
||||
*ResolveIdentifierResponse
|
||||
}
|
||||
|
||||
type ResolveIdentifierResponse struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
ChatID ResolveIdentifierResponseChatID `json:"chat_id"`
|
||||
JustCreated bool `json:"just_created"`
|
||||
OtherUser *ResolveIdentifierResponseOtherUser `json:"other_user,omitempty"`
|
||||
}
|
||||
|
||||
type ResolveIdentifierResponseChatID struct {
|
||||
UUID string `json:"uuid"`
|
||||
Number string `json:"number"`
|
||||
}
|
||||
|
||||
type ResolveIdentifierResponseOtherUser struct {
|
||||
MXID id.UserID `json:"mxid"`
|
||||
DisplayName string `json:"displayname"`
|
||||
AvatarURL id.ContentURI `json:"avatar_url"`
|
||||
}
|
||||
|
|
@ -1,56 +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.04",
|
||||
SemCalVer: true,
|
||||
|
||||
Connector: &connector.SignalConnector{},
|
||||
}
|
||||
|
||||
func main() {
|
||||
web.UserAgent = fmt.Sprintf("mautrix-signal/%s %s", m.Version, web.BaseUserAgent)
|
||||
m.PostStart = func() {
|
||||
if m.Matrix.Provisioning != nil {
|
||||
m.Matrix.Provisioning.Router.HandleFunc("GET /v2/resolve_identifier/{phonenum}", legacyProvResolveIdentifier)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v2/pm/{phonenum}", legacyProvPM)
|
||||
}
|
||||
}
|
||||
m.InitVersion(Tag, Commit, BuildTime)
|
||||
m.Run()
|
||||
}
|
||||
918
commands.go
Normal file
918
commands.go
Normal 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())
|
||||
}
|
||||
240
config/bridge.go
Normal file
240
config/bridge.go
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2022 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 config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
type BridgeConfig struct {
|
||||
UsernameTemplate string `yaml:"username_template"`
|
||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||
PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"`
|
||||
UseContactAvatars bool `yaml:"use_contact_avatars"`
|
||||
UseOutdatedProfiles bool `yaml:"use_outdated_profiles"`
|
||||
NumberInTopic bool `yaml:"number_in_topic"`
|
||||
|
||||
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
|
||||
|
||||
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
||||
|
||||
PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"`
|
||||
BridgeNotices bool `yaml:"bridge_notices"`
|
||||
DeliveryReceipts bool `yaml:"delivery_receipts"`
|
||||
MessageStatusEvents bool `yaml:"message_status_events"`
|
||||
MessageErrorNotices bool `yaml:"message_error_notices"`
|
||||
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
|
||||
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
|
||||
PublicPortals bool `yaml:"public_portals"`
|
||||
CaptionInMessage bool `yaml:"caption_in_message"`
|
||||
FederateRooms bool `yaml:"federate_rooms"`
|
||||
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
|
||||
|
||||
DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"`
|
||||
|
||||
MessageHandlingTimeout struct {
|
||||
ErrorAfterStr string `yaml:"error_after"`
|
||||
DeadlineStr string `yaml:"deadline"`
|
||||
|
||||
ErrorAfter time.Duration `yaml:"-"`
|
||||
Deadline time.Duration `yaml:"-"`
|
||||
} `yaml:"message_handling_timeout"`
|
||||
|
||||
CommandPrefix string `yaml:"command_prefix"`
|
||||
ManagementRoomText bridgeconfig.ManagementRoomTexts `yaml:"management_room_text"`
|
||||
|
||||
Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"`
|
||||
|
||||
Provisioning struct {
|
||||
Prefix string `yaml:"prefix"`
|
||||
SharedSecret string `yaml:"shared_secret"`
|
||||
DebugEndpoints bool `yaml:"debug_endpoints"`
|
||||
} `yaml:"provisioning"`
|
||||
|
||||
Permissions bridgeconfig.PermissionConfig `yaml:"permissions"`
|
||||
|
||||
Relay RelaybotConfig `yaml:"relay"`
|
||||
|
||||
usernameTemplate *template.Template `yaml:"-"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) GetResendBridgeInfo() bool {
|
||||
return bc.ResendBridgeInfo
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) EnableMessageStatusEvents() bool {
|
||||
return bc.MessageStatusEvents
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) EnableMessageErrorNotices() bool {
|
||||
return bc.MessageErrorNotices
|
||||
}
|
||||
|
||||
func boolToInt(val bool) int {
|
||||
if val {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) Validate() error {
|
||||
_, hasWildcard := bc.Permissions["*"]
|
||||
_, hasExampleDomain := bc.Permissions["example.com"]
|
||||
_, hasExampleUser := bc.Permissions["@admin:example.com"]
|
||||
exampleLen := boolToInt(hasWildcard) + boolToInt(hasExampleUser) + boolToInt(hasExampleDomain)
|
||||
if len(bc.Permissions) <= exampleLen {
|
||||
return errors.New("bridge.permissions not configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type umBridgeConfig BridgeConfig
|
||||
|
||||
func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
err := unmarshal((*umBridgeConfig)(bc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bc.usernameTemplate, err = template.New("username").Parse(bc.UsernameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !strings.Contains(bc.FormatUsername("1234567890"), "1234567890") {
|
||||
return fmt.Errorf("username template is missing user ID placeholder")
|
||||
}
|
||||
bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ bridgeconfig.BridgeConfig = (*BridgeConfig)(nil)
|
||||
|
||||
func (bc BridgeConfig) GetDoublePuppetConfig() bridgeconfig.DoublePuppetConfig {
|
||||
return bc.DoublePuppetConfig
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
|
||||
return bc.Encryption
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) GetCommandPrefix() string {
|
||||
return bc.CommandPrefix
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts {
|
||||
return bc.ManagementRoomText
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatUsername(userID string) string {
|
||||
var buffer strings.Builder
|
||||
_ = bc.usernameTemplate.Execute(&buffer, userID)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
type DisplaynameParams struct {
|
||||
ProfileName string
|
||||
ContactName string
|
||||
Username string
|
||||
PhoneNumber string
|
||||
UUID string
|
||||
ACI string
|
||||
PNI string
|
||||
AboutEmoji string
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatDisplayname(contact *types.Recipient) string {
|
||||
var buffer strings.Builder
|
||||
_ = bc.displaynameTemplate.Execute(&buffer, DisplaynameParams{
|
||||
ProfileName: contact.Profile.Name,
|
||||
ContactName: contact.ContactName,
|
||||
//Username: contact.Username,
|
||||
PhoneNumber: contact.E164,
|
||||
UUID: contact.ACI.String(),
|
||||
ACI: contact.ACI.String(),
|
||||
PNI: contact.PNI.String(),
|
||||
AboutEmoji: contact.Profile.AboutEmoji,
|
||||
})
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
type RelaybotConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AdminOnly bool `yaml:"admin_only"`
|
||||
MessageFormats map[event.MessageType]string `yaml:"message_formats"`
|
||||
messageTemplates *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
type umRelaybotConfig RelaybotConfig
|
||||
|
||||
func (rc *RelaybotConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
err := unmarshal((*umRelaybotConfig)(rc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc.messageTemplates = template.New("messageTemplates")
|
||||
for key, format := range rc.MessageFormats {
|
||||
_, err := rc.messageTemplates.New(string(key)).Parse(format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
UserID string
|
||||
event.MemberEventContent
|
||||
}
|
||||
|
||||
type formatData struct {
|
||||
Sender Sender
|
||||
Message string
|
||||
Content *event.MessageEventContent
|
||||
}
|
||||
|
||||
func (rc *RelaybotConfig) FormatMessage(content *event.MessageEventContent, sender id.UserID, member event.MemberEventContent) (string, error) {
|
||||
if len(member.Displayname) == 0 {
|
||||
member.Displayname = sender.String()
|
||||
}
|
||||
member.Displayname = template.HTMLEscapeString(member.Displayname)
|
||||
var output strings.Builder
|
||||
err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), formatData{
|
||||
Sender: Sender{
|
||||
UserID: template.HTMLEscapeString(sender.String()),
|
||||
MemberEventContent: member,
|
||||
},
|
||||
Content: content,
|
||||
Message: content.FormattedBody,
|
||||
})
|
||||
return output.String(), err
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-signal - A Matrix-Signal puppeting bridge.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
// Copyright (C) 2022 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
|
||||
|
|
@ -14,28 +14,31 @@
|
|||
// 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
|
||||
package config
|
||||
|
||||
import (
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (s *SignalConnector) GetDBMetaTypes() database.MetaTypes {
|
||||
return database.MetaTypes{
|
||||
Portal: func() any {
|
||||
return &signalid.PortalMetadata{}
|
||||
},
|
||||
Ghost: func() any {
|
||||
return &signalid.GhostMetadata{}
|
||||
},
|
||||
Message: func() any {
|
||||
return &signalid.MessageMetadata{}
|
||||
},
|
||||
Reaction: nil,
|
||||
UserLogin: func() any {
|
||||
return &signalid.UserLoginMetadata{}
|
||||
},
|
||||
type Config struct {
|
||||
*bridgeconfig.BaseConfig `yaml:",inline"`
|
||||
|
||||
Metrics struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Listen string `yaml:"listen"`
|
||||
} `yaml:"metrics"`
|
||||
|
||||
Signal struct {
|
||||
DeviceName string `yaml:"device_name"`
|
||||
} `yaml:"signal"`
|
||||
|
||||
Bridge BridgeConfig `yaml:"bridge"`
|
||||
}
|
||||
|
||||
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
|
||||
_, homeserver, _ := userID.Parse()
|
||||
_, hasSecret := config.Bridge.DoublePuppetConfig.SharedSecretMap[homeserver]
|
||||
|
||||
return hasSecret
|
||||
}
|
||||
166
config/upgrade.go
Normal file
166
config/upgrade.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
// mautrix-signal - A Matrix-Signal puppeting bridge.
|
||||
// Copyright (C) 2022 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 config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
up "go.mau.fi/util/configupgrade"
|
||||
"go.mau.fi/util/random"
|
||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||
)
|
||||
|
||||
func DoUpgrade(helper *up.Helper) {
|
||||
bridgeconfig.Upgrader.DoUpgrade(helper)
|
||||
|
||||
legacyDB, ok := helper.Get(up.Str, "appservice", "database")
|
||||
if ok {
|
||||
if strings.HasPrefix(legacyDB, "postgres") {
|
||||
parsedDB, err := url.Parse(legacyDB)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
q := parsedDB.Query()
|
||||
if parsedDB.Host == "" && !q.Has("host") {
|
||||
q.Set("host", "/var/run/postgresql")
|
||||
} else if !q.Has("sslmode") {
|
||||
q.Set("sslmode", "disable")
|
||||
}
|
||||
parsedDB.RawQuery = q.Encode()
|
||||
helper.Set(up.Str, parsedDB.String(), "appservice", "database", "uri")
|
||||
helper.Set(up.Str, "postgres", "appservice", "database", "type")
|
||||
} else {
|
||||
dbPath := strings.TrimPrefix(strings.TrimPrefix(legacyDB, "sqlite:"), "///")
|
||||
helper.Set(up.Str, dbPath, "appservice", "database", "uri")
|
||||
helper.Set(up.Str, "sqlite3-fk-wal", "appservice", "database", "type")
|
||||
}
|
||||
}
|
||||
if legacyDBMinSize, ok := helper.Get(up.Int, "appservice", "database_opts", "min_size"); ok {
|
||||
helper.Set(up.Int, legacyDBMinSize, "appservice", "database", "max_idle_conns")
|
||||
}
|
||||
if legacyDBMaxSize, ok := helper.Get(up.Int, "appservice", "database_opts", "max_size"); ok {
|
||||
helper.Set(up.Int, legacyDBMaxSize, "appservice", "database", "max_open_conns")
|
||||
}
|
||||
if legacyBotUsername, ok := helper.Get(up.Str, "appservice", "bot_username"); ok {
|
||||
helper.Set(up.Str, legacyBotUsername, "appservice", "bot", "username")
|
||||
}
|
||||
if legacyBotDisplayname, ok := helper.Get(up.Str, "appservice", "bot_displayname"); ok {
|
||||
helper.Set(up.Str, legacyBotDisplayname, "appservice", "bot", "displayname")
|
||||
}
|
||||
if legacyBotAvatar, ok := helper.Get(up.Str, "appservice", "bot_avatar"); ok {
|
||||
helper.Set(up.Str, legacyBotAvatar, "appservice", "bot", "avatar")
|
||||
}
|
||||
|
||||
helper.Copy(up.Bool, "metrics", "enabled")
|
||||
helper.Copy(up.Str, "metrics", "listen")
|
||||
|
||||
helper.Copy(up.Str, "signal", "device_name")
|
||||
|
||||
if usernameTemplate, ok := helper.Get(up.Str, "bridge", "username_template"); ok && strings.Contains(usernameTemplate, "{userid}") {
|
||||
helper.Set(up.Str, strings.ReplaceAll(usernameTemplate, "{userid}", "{{.}}"), "bridge", "username_template")
|
||||
} else {
|
||||
helper.Copy(up.Str, "bridge", "username_template")
|
||||
}
|
||||
if displaynameTemplate, ok := helper.Get(up.Str, "bridge", "displayname_template"); ok && strings.Contains(displaynameTemplate, "{displayname}") {
|
||||
helper.Set(up.Str, strings.ReplaceAll(displaynameTemplate, "{displayname}", `{{or .ProfileName .PhoneNumber "Unknown user"}}`), "bridge", "displayname_template")
|
||||
} else {
|
||||
helper.Copy(up.Str, "bridge", "displayname_template")
|
||||
}
|
||||
helper.Copy(up.Str, "bridge", "private_chat_portal_meta")
|
||||
helper.Copy(up.Bool, "bridge", "use_contact_avatars")
|
||||
helper.Copy(up.Bool, "bridge", "use_outdated_profiles")
|
||||
helper.Copy(up.Bool, "bridge", "number_in_topic")
|
||||
helper.Copy(up.Str, "bridge", "note_to_self_avatar")
|
||||
helper.Copy(up.Int, "bridge", "portal_message_buffer")
|
||||
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
|
||||
helper.Copy(up.Bool, "bridge", "bridge_notices")
|
||||
helper.Copy(up.Bool, "bridge", "delivery_receipts")
|
||||
helper.Copy(up.Bool, "bridge", "message_status_events")
|
||||
helper.Copy(up.Bool, "bridge", "message_error_notices")
|
||||
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
|
||||
helper.Copy(up.Bool, "bridge", "resend_bridge_info")
|
||||
helper.Copy(up.Bool, "bridge", "public_portals")
|
||||
helper.Copy(up.Bool, "bridge", "caption_in_message")
|
||||
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
||||
helper.Copy(up.Map, "bridge", "double_puppet_server_map")
|
||||
helper.Copy(up.Bool, "bridge", "double_puppet_allow_discovery")
|
||||
helper.Copy(up.Map, "bridge", "login_shared_secret_map")
|
||||
helper.Copy(up.Str, "bridge", "command_prefix")
|
||||
helper.Copy(up.Str, "bridge", "management_room_text", "welcome")
|
||||
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_connected")
|
||||
helper.Copy(up.Str, "bridge", "management_room_text", "welcome_unconnected")
|
||||
helper.Copy(up.Str|up.Null, "bridge", "management_room_text", "additional_help")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "allow")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "default")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "require")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "appservice")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "allow_key_sharing")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outbound_on_ack")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "dont_store_outbound")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "ratchet_on_decrypt")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_fully_used_on_decrypt")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_prev_on_new_session")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_on_device_delete")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "periodically_delete_expired")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outdated_inbound")
|
||||
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "receive")
|
||||
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "send")
|
||||
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "share")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "enable_custom")
|
||||
helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds")
|
||||
helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages")
|
||||
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "disable_device_change_key_rotation")
|
||||
helper.Copy(up.Bool, "bridge", "bridge_matrix_leave")
|
||||
|
||||
helper.Copy(up.Str, "bridge", "provisioning", "prefix")
|
||||
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
|
||||
sharedSecret := random.String(64)
|
||||
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
|
||||
} else {
|
||||
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")
|
||||
}
|
||||
helper.Copy(up.Bool, "bridge", "provisioning", "debug_endpoints")
|
||||
|
||||
helper.Copy(up.Map, "bridge", "permissions")
|
||||
helper.Copy(up.Bool, "bridge", "relay", "enabled")
|
||||
helper.Copy(up.Bool, "bridge", "relay", "admin_only")
|
||||
if textRelayFormat, ok := helper.Get(up.Str, "bridge", "relay", "message_formats", "m.text"); ok && strings.Contains(textRelayFormat, "$message") && !strings.Contains(textRelayFormat, ".Message") {
|
||||
// don't copy legacy message formats
|
||||
} else {
|
||||
helper.Copy(up.Map, "bridge", "relay", "message_formats")
|
||||
}
|
||||
}
|
||||
|
||||
var SpacedBlocks = [][]string{
|
||||
{"homeserver", "software"},
|
||||
{"appservice"},
|
||||
{"appservice", "hostname"},
|
||||
{"appservice", "database"},
|
||||
{"appservice", "id"},
|
||||
{"appservice", "as_token"},
|
||||
{"metrics"},
|
||||
{"signal"},
|
||||
{"bridge"},
|
||||
{"bridge", "personal_filtering_spaces"},
|
||||
{"bridge", "command_prefix"},
|
||||
{"bridge", "management_room_text"},
|
||||
{"bridge", "encryption"},
|
||||
{"bridge", "provisioning"},
|
||||
{"bridge", "permissions"},
|
||||
{"logging"},
|
||||
}
|
||||
97
custompuppet.go
Normal file
97
custompuppet.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
|
||||
puppet.CustomMXID = mxid
|
||||
puppet.AccessToken = accessToken
|
||||
err := puppet.Update(context.TODO())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save access token: %w", err)
|
||||
}
|
||||
err = puppet.StartCustomMXID(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO leave rooms with default puppet
|
||||
return nil
|
||||
}
|
||||
|
||||
func (puppet *Puppet) ClearCustomMXID() {
|
||||
save := puppet.CustomMXID != "" || puppet.AccessToken != ""
|
||||
puppet.bridge.puppetsLock.Lock()
|
||||
if puppet.CustomMXID != "" && puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] == puppet {
|
||||
delete(puppet.bridge.puppetsByCustomMXID, puppet.CustomMXID)
|
||||
}
|
||||
puppet.bridge.puppetsLock.Unlock()
|
||||
puppet.CustomMXID = ""
|
||||
puppet.AccessToken = ""
|
||||
puppet.customIntent = nil
|
||||
puppet.customUser = nil
|
||||
if save {
|
||||
err := puppet.Update(context.TODO())
|
||||
if err != nil {
|
||||
puppet.log.Err(err).Msg("Failed to clear custom MXID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
|
||||
newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(context.TODO(), puppet.CustomMXID, puppet.AccessToken, reloginOnFail)
|
||||
if err != nil {
|
||||
puppet.ClearCustomMXID()
|
||||
return err
|
||||
}
|
||||
puppet.bridge.puppetsLock.Lock()
|
||||
puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
|
||||
puppet.bridge.puppetsLock.Unlock()
|
||||
if puppet.AccessToken != newAccessToken {
|
||||
puppet.AccessToken = newAccessToken
|
||||
err = puppet.Update(context.TODO())
|
||||
}
|
||||
puppet.customIntent = newIntent
|
||||
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (user *User) tryAutomaticDoublePuppeting() {
|
||||
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
|
||||
return
|
||||
}
|
||||
user.log.Debug().Msg("Checking if double puppeting needs to be enabled")
|
||||
puppet := user.bridge.GetPuppetBySignalID(user.SignalID)
|
||||
if puppet.CustomMXID == user.MXID {
|
||||
user.log.Debug().Msg("User already has double-puppeting enabled")
|
||||
// Custom puppet already enabled
|
||||
return
|
||||
}
|
||||
puppet.CustomMXID = user.MXID
|
||||
err := puppet.StartCustomMXID(true)
|
||||
if err != nil {
|
||||
user.log.Warn().Err(err).Msg("Failed to login with shared secret")
|
||||
} else {
|
||||
// TODO leave rooms with default puppet
|
||||
user.log.Debug().Msg("Successfully automatically enabled custom puppet")
|
||||
}
|
||||
}
|
||||
53
database/database.go
Normal file
53
database/database.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.mau.fi/util/dbutil"
|
||||
|
||||
"go.mau.fi/mautrix-signal/database/upgrades"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
*dbutil.Database
|
||||
|
||||
User *UserQuery
|
||||
Portal *PortalQuery
|
||||
LostPortal *LostPortalQuery
|
||||
Puppet *PuppetQuery
|
||||
Message *MessageQuery
|
||||
Reaction *ReactionQuery
|
||||
DisappearingMessage *DisappearingMessageQuery
|
||||
}
|
||||
|
||||
func New(db *dbutil.Database) *Database {
|
||||
db.UpgradeTable = upgrades.Table
|
||||
return &Database{
|
||||
Database: db,
|
||||
User: &UserQuery{dbutil.MakeQueryHelper(db, newUser)},
|
||||
Portal: &PortalQuery{dbutil.MakeQueryHelper(db, newPortal)},
|
||||
LostPortal: &LostPortalQuery{dbutil.MakeQueryHelper(db, newLostPortal)},
|
||||
Puppet: &PuppetQuery{dbutil.MakeQueryHelper(db, newPuppet)},
|
||||
Message: &MessageQuery{dbutil.MakeQueryHelper(db, newMessage)},
|
||||
Reaction: &ReactionQuery{dbutil.MakeQueryHelper(db, newReaction)},
|
||||
DisappearingMessage: &DisappearingMessageQuery{dbutil.MakeQueryHelper(db, newDisappearingMessage)},
|
||||
}
|
||||
}
|
||||
125
database/disappearingmessage.go
Normal file
125
database/disappearingmessage.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
getUnscheduledDisappearingMessagesForRoomQuery = `
|
||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
||||
FROM disappearing_message WHERE expiration_ts IS NULL AND room_id = $1
|
||||
`
|
||||
getExpiredDisappearingMessagesQuery = `
|
||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
||||
FROM disappearing_message WHERE expiration_ts IS NOT NULL AND expiration_ts <= $1
|
||||
`
|
||||
getNextDisappearingMessageQuery = `
|
||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
||||
FROM disappearing_message WHERE expiration_ts IS NOT NULL ORDER BY expiration_ts ASC LIMIT 1
|
||||
`
|
||||
insertDisappearingMessageQuery = `
|
||||
INSERT INTO disappearing_message (room_id, mxid, expiration_seconds, expiration_ts) VALUES ($1, $2, $3, $4)
|
||||
`
|
||||
updateDisappearingMessageQuery = `
|
||||
UPDATE disappearing_message SET expiration_ts=$2 WHERE mxid=$1
|
||||
`
|
||||
deleteDisappearingMessageQuery = `
|
||||
DELETE FROM disappearing_message WHERE mxid=$1
|
||||
`
|
||||
)
|
||||
|
||||
type DisappearingMessageQuery struct {
|
||||
*dbutil.QueryHelper[*DisappearingMessage]
|
||||
}
|
||||
|
||||
type DisappearingMessage struct {
|
||||
qh *dbutil.QueryHelper[*DisappearingMessage]
|
||||
|
||||
RoomID id.RoomID
|
||||
EventID id.EventID
|
||||
ExpireIn time.Duration
|
||||
ExpireAt time.Time
|
||||
}
|
||||
|
||||
func newDisappearingMessage(qh *dbutil.QueryHelper[*DisappearingMessage]) *DisappearingMessage {
|
||||
return &DisappearingMessage{qh: qh}
|
||||
}
|
||||
|
||||
func (dmq *DisappearingMessageQuery) NewWithValues(roomID id.RoomID, eventID id.EventID, expireIn time.Duration, expireAt time.Time) *DisappearingMessage {
|
||||
return &DisappearingMessage{
|
||||
qh: dmq.QueryHelper,
|
||||
RoomID: roomID,
|
||||
EventID: eventID,
|
||||
ExpireIn: expireIn,
|
||||
ExpireAt: expireAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (dmq *DisappearingMessageQuery) GetUnscheduledForRoom(ctx context.Context, roomID id.RoomID) ([]*DisappearingMessage, error) {
|
||||
return dmq.QueryMany(ctx, getUnscheduledDisappearingMessagesForRoomQuery, roomID)
|
||||
}
|
||||
|
||||
func (dmq *DisappearingMessageQuery) GetExpiredMessages(ctx context.Context) ([]*DisappearingMessage, error) {
|
||||
return dmq.QueryMany(ctx, getExpiredDisappearingMessagesQuery, time.Now().Unix()+1)
|
||||
}
|
||||
|
||||
func (dmq *DisappearingMessageQuery) GetNextScheduledMessage(ctx context.Context) (*DisappearingMessage, error) {
|
||||
return dmq.QueryOne(ctx, getNextDisappearingMessageQuery)
|
||||
}
|
||||
|
||||
func (msg *DisappearingMessage) Scan(row dbutil.Scannable) (*DisappearingMessage, error) {
|
||||
var expireIn int64
|
||||
var expireAt sql.NullInt64
|
||||
err := row.Scan(&msg.RoomID, &msg.EventID, &expireIn, &expireAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.ExpireIn = time.Duration(expireIn) * time.Second
|
||||
if expireAt.Valid {
|
||||
msg.ExpireAt = time.Unix(expireAt.Int64, 0)
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (msg *DisappearingMessage) sqlVariables() []any {
|
||||
var expireAt sql.NullInt64
|
||||
if !msg.ExpireAt.IsZero() {
|
||||
expireAt.Valid = true
|
||||
expireAt.Int64 = msg.ExpireAt.Unix()
|
||||
}
|
||||
return []any{msg.RoomID, msg.EventID, int64(msg.ExpireIn.Seconds()), expireAt}
|
||||
}
|
||||
|
||||
func (msg *DisappearingMessage) Insert(ctx context.Context) error {
|
||||
return msg.qh.Exec(ctx, insertDisappearingMessageQuery, msg.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (msg *DisappearingMessage) StartExpirationTimer(ctx context.Context) error {
|
||||
msg.ExpireAt = time.Now().Add(msg.ExpireIn)
|
||||
return msg.qh.Exec(ctx, updateDisappearingMessageQuery, msg.EventID, msg.ExpireAt.Unix())
|
||||
}
|
||||
|
||||
func (msg *DisappearingMessage) Delete(ctx context.Context) error {
|
||||
return msg.qh.Exec(ctx, deleteDisappearingMessageQuery, msg.EventID)
|
||||
}
|
||||
58
database/lostportal.go
Normal file
58
database/lostportal.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
getLostPortalsQuery = `SELECT chat_id, receiver, mxid FROM lost_portals`
|
||||
deleteLostPortalQuery = `DELETE FROM lost_portals WHERE mxid=$1`
|
||||
)
|
||||
|
||||
type LostPortalQuery struct {
|
||||
*dbutil.QueryHelper[*LostPortal]
|
||||
}
|
||||
|
||||
func (lpq *LostPortalQuery) GetAll(ctx context.Context) ([]*LostPortal, error) {
|
||||
return lpq.QueryMany(ctx, getLostPortalsQuery)
|
||||
}
|
||||
|
||||
type LostPortal struct {
|
||||
qh *dbutil.QueryHelper[*LostPortal]
|
||||
|
||||
ChatID string
|
||||
Receiver string
|
||||
MXID id.RoomID
|
||||
}
|
||||
|
||||
func newLostPortal(qh *dbutil.QueryHelper[*LostPortal]) *LostPortal {
|
||||
return &LostPortal{qh: qh}
|
||||
}
|
||||
|
||||
func (l *LostPortal) Scan(row dbutil.Scannable) (*LostPortal, error) {
|
||||
err := row.Scan(&l.ChatID, &l.Receiver, &l.MXID)
|
||||
return l, err
|
||||
}
|
||||
|
||||
func (l *LostPortal) Delete(ctx context.Context) error {
|
||||
return l.qh.Exec(ctx, deleteLostPortalQuery, l.MXID)
|
||||
}
|
||||
179
database/message.go
Normal file
179
database/message.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
getMessageByMXIDQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE mxid=$1
|
||||
`
|
||||
getMessagePartBySignalIDQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=$1 AND timestamp=$2 AND part_index=$3 AND signal_receiver=$4
|
||||
`
|
||||
getLastMessagePartBySignalIDQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
||||
ORDER BY part_index DESC LIMIT 1
|
||||
`
|
||||
getAllMessagePartsBySignalIDQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
||||
`
|
||||
getMessageLastPartBySignalIDWithUnknownReceiverQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=$1 AND timestamp=$2 AND (signal_receiver=$3 OR signal_receiver='00000000-0000-0000-0000-000000000000')
|
||||
ORDER BY part_index DESC LIMIT 1
|
||||
`
|
||||
getManyMessagesBySignalIDQueryPostgres = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=$1 AND (signal_receiver=$2 OR signal_receiver=$3) AND timestamp=ANY($4)
|
||||
ORDER BY timestamp DESC, part_index DESC
|
||||
`
|
||||
getManyMessagesBySignalIDQuerySQLite = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE sender=?1 AND (signal_receiver=?2 OR signal_receiver=?3) AND timestamp IN (?4)
|
||||
ORDER BY timestamp DESC, part_index DESC
|
||||
`
|
||||
getFirstBeforeQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE mx_room=$1 AND timestamp <= $2
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
`
|
||||
getMessagesBetweenTimeQuery = `
|
||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
||||
WHERE signal_chat_id=$1 AND signal_receiver=$2 AND timestamp>$3 AND timestamp<=$4 AND part_index=0
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
insertMessageQuery = `
|
||||
INSERT INTO message (sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`
|
||||
deleteMessageQuery = `
|
||||
DELETE FROM message
|
||||
WHERE sender=$1 AND timestamp=$2 AND part_index=$3 AND signal_receiver=$4
|
||||
`
|
||||
updateMessageTimestampQuery = `
|
||||
UPDATE message SET timestamp=$4 WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
||||
`
|
||||
)
|
||||
|
||||
type MessageQuery struct {
|
||||
*dbutil.QueryHelper[*Message]
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
qh *dbutil.QueryHelper[*Message]
|
||||
|
||||
Sender uuid.UUID
|
||||
Timestamp uint64
|
||||
PartIndex int
|
||||
|
||||
SignalChatID string
|
||||
SignalReceiver uuid.UUID
|
||||
|
||||
MXID id.EventID
|
||||
RoomID id.RoomID
|
||||
}
|
||||
|
||||
func newMessage(qh *dbutil.QueryHelper[*Message]) *Message {
|
||||
return &Message{qh: qh}
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Message, error) {
|
||||
return mq.QueryOne(ctx, getMessageByMXIDQuery, mxid)
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, partIndex int, receiver uuid.UUID) (*Message, error) {
|
||||
return mq.QueryOne(ctx, getMessagePartBySignalIDQuery, sender, timestamp, partIndex, receiver)
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetLastPartBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) (*Message, error) {
|
||||
return mq.QueryOne(ctx, getLastMessagePartBySignalIDQuery, sender, timestamp, receiver)
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetAllPartsBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) ([]*Message, error) {
|
||||
return mq.QueryMany(ctx, getAllMessagePartsBySignalIDQuery, sender, timestamp, receiver)
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetAllBetweenTimestamps(ctx context.Context, key PortalKey, min, max uint64) ([]*Message, error) {
|
||||
return mq.QueryMany(ctx, getMessagesBetweenTimeQuery, key.ChatID, key.Receiver, int64(min), int64(max))
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetLastPartBySignalIDWithUnknownReceiver(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) (*Message, error) {
|
||||
return mq.QueryOne(ctx, getMessageLastPartBySignalIDWithUnknownReceiverQuery, sender, timestamp, receiver)
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetManyBySignalID(ctx context.Context, sender uuid.UUID, timestamps []uint64, receiver uuid.UUID, strictReceiver bool) ([]*Message, error) {
|
||||
receiver2 := uuid.Nil
|
||||
if strictReceiver {
|
||||
receiver2 = receiver
|
||||
}
|
||||
if mq.GetDB().Dialect == dbutil.Postgres {
|
||||
int64Array := make([]int64, len(timestamps))
|
||||
for i, timestamp := range timestamps {
|
||||
int64Array[i] = int64(timestamp)
|
||||
}
|
||||
return mq.QueryMany(ctx, getManyMessagesBySignalIDQueryPostgres, sender, receiver, receiver2, pq.Array(int64Array))
|
||||
} else {
|
||||
const varargIndex = 3
|
||||
arguments := make([]any, len(timestamps)+varargIndex)
|
||||
placeholders := make([]string, len(timestamps))
|
||||
arguments[0] = sender
|
||||
arguments[1] = receiver
|
||||
arguments[2] = receiver2
|
||||
for i, timestamp := range timestamps {
|
||||
arguments[i+varargIndex] = timestamp
|
||||
placeholders[i] = fmt.Sprintf("?%d", i+varargIndex+1)
|
||||
}
|
||||
return mq.QueryMany(ctx, strings.Replace(getManyMessagesBySignalIDQuerySQLite, fmt.Sprintf("?%d", varargIndex+1), strings.Join(placeholders, ", "), 1), arguments...)
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *Message) Scan(row dbutil.Scannable) (*Message, error) {
|
||||
return dbutil.ValueOrErr(msg, row.Scan(
|
||||
&msg.Sender, &msg.Timestamp, &msg.PartIndex, &msg.SignalChatID, &msg.SignalReceiver, &msg.MXID, &msg.RoomID,
|
||||
))
|
||||
}
|
||||
|
||||
func (msg *Message) sqlVariables() []any {
|
||||
return []any{msg.Sender, msg.Timestamp, msg.PartIndex, msg.SignalChatID, msg.SignalReceiver, msg.MXID, msg.RoomID}
|
||||
}
|
||||
|
||||
func (msg *Message) Insert(ctx context.Context) error {
|
||||
return msg.qh.Exec(ctx, insertMessageQuery, msg.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (msg *Message) Delete(ctx context.Context) error {
|
||||
return msg.qh.Exec(ctx, deleteMessageQuery, msg.Sender, msg.Timestamp, msg.PartIndex, msg.SignalReceiver)
|
||||
}
|
||||
|
||||
func (msg *Message) SetTimestamp(ctx context.Context, editTime uint64) error {
|
||||
return msg.qh.Exec(ctx, updateMessageTimestampQuery, msg.Sender, msg.Timestamp, msg.SignalReceiver, editTime)
|
||||
}
|
||||
206
database/portal.go
Normal file
206
database/portal.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
const (
|
||||
portalBaseSelect = `
|
||||
SELECT chat_id, receiver, mxid, name, topic, avatar_path, avatar_hash, avatar_url,
|
||||
name_set, avatar_set, topic_set, revision, encrypted, relay_user_id, expiration_time
|
||||
FROM portal
|
||||
`
|
||||
getPortalByMXIDQuery = portalBaseSelect + `WHERE mxid=$1`
|
||||
getPortalByChatIDQuery = portalBaseSelect + `WHERE chat_id=$1 AND receiver=$2`
|
||||
getPortalsByReceiver = portalBaseSelect + `WHERE receiver=$1`
|
||||
getPortalsByUser = portalBaseSelect + `WHERE chat_id=$1`
|
||||
getAllPortalsWithMXIDQuery = portalBaseSelect + `WHERE mxid IS NOT NULL`
|
||||
getChatsNotInSpaceQuery = `
|
||||
SELECT chat_id FROM portal
|
||||
LEFT JOIN user_portal ON portal.chat_id=user_portal.portal_chat_id AND portal.receiver=user_portal.portal_receiver
|
||||
WHERE mxid<>'' AND receiver=$1 AND (user_portal.in_space=false OR user_portal.in_space IS NULL)
|
||||
`
|
||||
insertPortalQuery = `
|
||||
INSERT INTO portal (
|
||||
chat_id, receiver, mxid, name, topic, avatar_path, avatar_hash, avatar_url,
|
||||
name_set, avatar_set, topic_set, revision, encrypted, relay_user_id, expiration_time
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
`
|
||||
updatePortalQuery = `
|
||||
UPDATE portal SET
|
||||
mxid=$3, name=$4, topic=$5, avatar_path=$6, avatar_hash=$7, avatar_url=$8,
|
||||
name_set=$9, avatar_set=$10, topic_set=$11, revision=$12, encrypted=$13, relay_user_id=$14, expiration_time=$15
|
||||
WHERE chat_id=$1 AND receiver=$2
|
||||
`
|
||||
deletePortalQuery = `DELETE FROM portal WHERE chat_id=$1 AND receiver=$2`
|
||||
reIDPortalQuery = `UPDATE portal SET chat_id=$2 WHERE chat_id=$1 AND receiver=$3`
|
||||
)
|
||||
|
||||
type PortalQuery struct {
|
||||
*dbutil.QueryHelper[*Portal]
|
||||
}
|
||||
|
||||
type PortalKey struct {
|
||||
ChatID string
|
||||
Receiver uuid.UUID
|
||||
}
|
||||
|
||||
func (pk *PortalKey) UserID() libsignalgo.ServiceID {
|
||||
parsed, _ := libsignalgo.ServiceIDFromString(pk.ChatID)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (pk *PortalKey) GroupID() types.GroupIdentifier {
|
||||
if len(pk.ChatID) == 44 {
|
||||
return types.GroupIdentifier(pk.ChatID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func NewPortalKey(chatID string, receiver uuid.UUID) PortalKey {
|
||||
return PortalKey{
|
||||
ChatID: chatID,
|
||||
Receiver: receiver,
|
||||
}
|
||||
}
|
||||
|
||||
type Portal struct {
|
||||
qh *dbutil.QueryHelper[*Portal]
|
||||
|
||||
PortalKey
|
||||
MXID id.RoomID
|
||||
Name string
|
||||
Topic string
|
||||
AvatarPath string
|
||||
AvatarHash string
|
||||
AvatarURL id.ContentURI
|
||||
NameSet bool
|
||||
AvatarSet bool
|
||||
TopicSet bool
|
||||
Revision uint32
|
||||
Encrypted bool
|
||||
RelayUserID id.UserID
|
||||
ExpirationTime uint32
|
||||
}
|
||||
|
||||
func newPortal(qh *dbutil.QueryHelper[*Portal]) *Portal {
|
||||
return &Portal{qh: qh}
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetByMXID(ctx context.Context, mxid id.RoomID) (*Portal, error) {
|
||||
return pq.QueryOne(ctx, getPortalByMXIDQuery, mxid)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetByChatID(ctx context.Context, pk PortalKey) (*Portal, error) {
|
||||
return pq.QueryOne(ctx, getPortalByChatIDQuery, pk.ChatID, pk.Receiver)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) FindPrivateChatsWith(ctx context.Context, userID uuid.UUID) ([]*Portal, error) {
|
||||
return pq.QueryMany(ctx, getPortalsByUser, userID.String())
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) FindPrivateChatsOf(ctx context.Context, receiver uuid.UUID) ([]*Portal, error) {
|
||||
return pq.QueryMany(ctx, getPortalsByReceiver, receiver)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetAllWithMXID(ctx context.Context) ([]*Portal, error) {
|
||||
return pq.QueryMany(ctx, getAllPortalsWithMXIDQuery)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) FindPrivateChatsNotInSpace(ctx context.Context, receiver uuid.UUID) ([]PortalKey, error) {
|
||||
rows, err := pq.GetDB().Query(ctx, getChatsNotInSpaceQuery, receiver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbutil.NewRowIter(rows, func(rows dbutil.Scannable) (key PortalKey, err error) {
|
||||
err = rows.Scan(&key.ChatID)
|
||||
key.Receiver = receiver
|
||||
return
|
||||
}).AsList()
|
||||
}
|
||||
|
||||
func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
|
||||
var mxid sql.NullString
|
||||
err := row.Scan(
|
||||
&p.ChatID,
|
||||
&p.Receiver,
|
||||
&mxid,
|
||||
&p.Name,
|
||||
&p.Topic,
|
||||
&p.AvatarPath,
|
||||
&p.AvatarHash,
|
||||
&p.AvatarURL,
|
||||
&p.NameSet,
|
||||
&p.AvatarSet,
|
||||
&p.TopicSet,
|
||||
&p.Revision,
|
||||
&p.Encrypted,
|
||||
&p.RelayUserID,
|
||||
&p.ExpirationTime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.MXID = id.RoomID(mxid.String)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Portal) sqlVariables() []any {
|
||||
return []any{
|
||||
p.ChatID,
|
||||
p.Receiver,
|
||||
dbutil.StrPtr(p.MXID),
|
||||
p.Name,
|
||||
p.Topic,
|
||||
p.AvatarPath,
|
||||
p.AvatarHash,
|
||||
&p.AvatarURL,
|
||||
p.NameSet,
|
||||
p.AvatarSet,
|
||||
p.TopicSet,
|
||||
p.Revision,
|
||||
p.Encrypted,
|
||||
p.RelayUserID,
|
||||
p.ExpirationTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Portal) Insert(ctx context.Context) error {
|
||||
return p.qh.Exec(ctx, insertPortalQuery, p.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (p *Portal) Update(ctx context.Context) error {
|
||||
return p.qh.Exec(ctx, updatePortalQuery, p.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (p *Portal) Delete(ctx context.Context) error {
|
||||
return p.qh.Exec(ctx, deletePortalQuery, p.ChatID, p.Receiver)
|
||||
}
|
||||
|
||||
func (p *Portal) ReID(ctx context.Context, newID string) error {
|
||||
return p.qh.Exec(ctx, reIDPortalQuery, p.ChatID, newID, p.Receiver)
|
||||
}
|
||||
158
database/puppet.go
Normal file
158
database/puppet.go
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
puppetBaseSelect = `
|
||||
SELECT uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url, name_set, avatar_set,
|
||||
contact_info_set, is_registered, profile_fetched_at, custom_mxid, access_token
|
||||
FROM puppet
|
||||
`
|
||||
getPuppetBySignalIDQuery = puppetBaseSelect + `WHERE uuid=$1`
|
||||
getPuppetByNumberQuery = puppetBaseSelect + `WHERE number=$1`
|
||||
getPuppetByCustomMXIDQuery = puppetBaseSelect + `WHERE custom_mxid=$1`
|
||||
getPuppetsWithCustomMXID = puppetBaseSelect + `WHERE custom_mxid<>''`
|
||||
updatePuppetQuery = `
|
||||
UPDATE puppet SET
|
||||
number=$2, name=$3, name_quality=$4, avatar_path=$5, avatar_hash=$6, avatar_url=$7,
|
||||
name_set=$8, avatar_set=$9, contact_info_set=$10, is_registered=$11, profile_fetched_at=$12,
|
||||
custom_mxid=$13, access_token=$14
|
||||
WHERE uuid=$1
|
||||
`
|
||||
insertPuppetQuery = `
|
||||
INSERT INTO puppet (
|
||||
uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url,
|
||||
name_set, avatar_set, contact_info_set, is_registered, profile_fetched_at,
|
||||
custom_mxid, access_token
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
||||
)
|
||||
`
|
||||
)
|
||||
|
||||
type PuppetQuery struct {
|
||||
*dbutil.QueryHelper[*Puppet]
|
||||
}
|
||||
|
||||
type Puppet struct {
|
||||
qh *dbutil.QueryHelper[*Puppet]
|
||||
|
||||
SignalID uuid.UUID
|
||||
Number string
|
||||
Name string
|
||||
NameQuality int
|
||||
AvatarPath string
|
||||
AvatarHash string
|
||||
AvatarURL id.ContentURI
|
||||
NameSet bool
|
||||
AvatarSet bool
|
||||
|
||||
IsRegistered bool
|
||||
ContactInfoSet bool
|
||||
ProfileFetchedAt time.Time
|
||||
|
||||
CustomMXID id.UserID
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
func newPuppet(qh *dbutil.QueryHelper[*Puppet]) *Puppet {
|
||||
return &Puppet{qh: qh}
|
||||
}
|
||||
|
||||
func (pq *PuppetQuery) GetBySignalID(ctx context.Context, signalID uuid.UUID) (*Puppet, error) {
|
||||
return pq.QueryOne(ctx, getPuppetBySignalIDQuery, signalID)
|
||||
}
|
||||
|
||||
func (pq *PuppetQuery) GetByNumber(ctx context.Context, number string) (*Puppet, error) {
|
||||
return pq.QueryOne(ctx, getPuppetByNumberQuery, number)
|
||||
}
|
||||
|
||||
func (pq *PuppetQuery) GetByCustomMXID(ctx context.Context, mxid id.UserID) (*Puppet, error) {
|
||||
return pq.QueryOne(ctx, getPuppetByCustomMXIDQuery, mxid)
|
||||
}
|
||||
|
||||
func (pq *PuppetQuery) GetAllWithCustomMXID(ctx context.Context) ([]*Puppet, error) {
|
||||
return pq.QueryMany(ctx, getPuppetsWithCustomMXID)
|
||||
}
|
||||
|
||||
func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
|
||||
var number, customMXID sql.NullString
|
||||
var profileFetchedAt sql.NullInt64
|
||||
err := row.Scan(
|
||||
&p.SignalID,
|
||||
&number,
|
||||
&p.Name,
|
||||
&p.NameQuality,
|
||||
&p.AvatarPath,
|
||||
&p.AvatarHash,
|
||||
&p.AvatarURL,
|
||||
&p.NameSet,
|
||||
&p.AvatarSet,
|
||||
&p.ContactInfoSet,
|
||||
&p.IsRegistered,
|
||||
&profileFetchedAt,
|
||||
&customMXID,
|
||||
&p.AccessToken,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Number = number.String
|
||||
p.CustomMXID = id.UserID(customMXID.String)
|
||||
if profileFetchedAt.Valid {
|
||||
p.ProfileFetchedAt = time.UnixMilli(profileFetchedAt.Int64)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Puppet) sqlVariables() []any {
|
||||
return []any{
|
||||
p.SignalID,
|
||||
dbutil.StrPtr(p.Number),
|
||||
p.Name,
|
||||
p.NameQuality,
|
||||
p.AvatarPath,
|
||||
p.AvatarHash,
|
||||
&p.AvatarURL,
|
||||
p.NameSet,
|
||||
p.AvatarSet,
|
||||
p.ContactInfoSet,
|
||||
p.IsRegistered,
|
||||
dbutil.UnixMilliPtr(p.ProfileFetchedAt),
|
||||
dbutil.StrPtr(p.CustomMXID),
|
||||
p.AccessToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Puppet) Insert(ctx context.Context) error {
|
||||
return p.qh.Exec(ctx, insertPuppetQuery, p.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (p *Puppet) Update(ctx context.Context) error {
|
||||
return p.qh.Exec(ctx, updatePuppetQuery, p.sqlVariables()...)
|
||||
}
|
||||
97
database/reaction.go
Normal file
97
database/reaction.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
getReactionByMXIDQuery = `SELECT msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room FROM reaction WHERE mxid=$1`
|
||||
getReactionBySignalIDQuery = `SELECT msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room FROM reaction WHERE msg_author=$1 AND msg_timestamp=$2 AND author=$3 AND signal_receiver=$4`
|
||||
insertReactionQuery = `
|
||||
INSERT INTO reaction (msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`
|
||||
updateReactionQuery = `
|
||||
UPDATE reaction
|
||||
SET mxid=$1, emoji=$2
|
||||
WHERE msg_author=$3 AND msg_timestamp=$4 AND author=$5 AND signal_receiver=$6
|
||||
`
|
||||
deleteReactionQuery = `
|
||||
DELETE FROM reaction WHERE msg_author=$1 AND msg_timestamp=$2 AND author=$3 AND signal_receiver=$4
|
||||
`
|
||||
)
|
||||
|
||||
type ReactionQuery struct {
|
||||
*dbutil.QueryHelper[*Reaction]
|
||||
}
|
||||
|
||||
func newReaction(qh *dbutil.QueryHelper[*Reaction]) *Reaction {
|
||||
return &Reaction{qh: qh}
|
||||
}
|
||||
|
||||
type Reaction struct {
|
||||
qh *dbutil.QueryHelper[*Reaction]
|
||||
|
||||
MsgAuthor uuid.UUID
|
||||
MsgTimestamp uint64
|
||||
Author uuid.UUID
|
||||
Emoji string
|
||||
|
||||
SignalChatID string
|
||||
SignalReceiver uuid.UUID
|
||||
|
||||
MXID id.EventID
|
||||
RoomID id.RoomID
|
||||
}
|
||||
|
||||
func (rq *ReactionQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Reaction, error) {
|
||||
return rq.QueryOne(ctx, getReactionByMXIDQuery, mxid)
|
||||
}
|
||||
|
||||
func (rq *ReactionQuery) GetBySignalID(ctx context.Context, msgAuthor uuid.UUID, msgTimestamp uint64, author, signalReceiver uuid.UUID) (*Reaction, error) {
|
||||
return rq.QueryOne(ctx, getReactionBySignalIDQuery, msgAuthor, msgTimestamp, author, signalReceiver)
|
||||
}
|
||||
|
||||
func (r *Reaction) Scan(row dbutil.Scannable) (*Reaction, error) {
|
||||
return dbutil.ValueOrErr(r, row.Scan(
|
||||
&r.MsgAuthor, &r.MsgTimestamp, &r.Author, &r.Emoji, &r.SignalChatID, &r.SignalReceiver, &r.MXID, &r.RoomID,
|
||||
))
|
||||
}
|
||||
|
||||
func (r *Reaction) sqlVariables() []any {
|
||||
return []any{
|
||||
r.MsgAuthor, r.MsgTimestamp, r.Author, r.Emoji, r.SignalChatID, r.SignalReceiver, r.MXID, r.RoomID,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reaction) Insert(ctx context.Context) error {
|
||||
return r.qh.Exec(ctx, insertReactionQuery, r.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (r *Reaction) Update(ctx context.Context) error {
|
||||
return r.qh.Exec(ctx, updateReactionQuery, r.MXID, r.Emoji, r.MsgAuthor, r.MsgTimestamp, r.Author, r.SignalReceiver)
|
||||
}
|
||||
|
||||
func (r *Reaction) Delete(ctx context.Context) error {
|
||||
return r.qh.Exec(ctx, deleteReactionQuery, r.MsgAuthor, r.MsgTimestamp, r.Author, r.SignalReceiver)
|
||||
}
|
||||
116
database/upgrades/00-latest.sql
Normal file
116
database/upgrades/00-latest.sql
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
-- v0 -> v20 (compatible with v17+): Latest revision
|
||||
|
||||
CREATE TABLE portal (
|
||||
chat_id TEXT NOT NULL,
|
||||
receiver uuid NOT NULL,
|
||||
mxid TEXT,
|
||||
name TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
encrypted BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_path TEXT NOT NULL DEFAULT '',
|
||||
avatar_hash TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||
topic_set BOOLEAN NOT NULL DEFAULT false,
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
expiration_time BIGINT NOT NULL,
|
||||
relay_user_id TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (chat_id, receiver),
|
||||
CONSTRAINT portal_mxid_unique UNIQUE(mxid)
|
||||
);
|
||||
|
||||
CREATE TABLE puppet (
|
||||
uuid uuid PRIMARY KEY,
|
||||
number TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
name_quality INTEGER NOT NULL,
|
||||
avatar_path TEXT NOT NULL,
|
||||
avatar_hash TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
is_registered BOOLEAN NOT NULL DEFAULT false,
|
||||
contact_info_set BOOLEAN NOT NULL DEFAULT false,
|
||||
profile_fetched_at BIGINT,
|
||||
|
||||
custom_mxid TEXT,
|
||||
access_token TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT puppet_custom_mxid_unique UNIQUE(custom_mxid)
|
||||
);
|
||||
|
||||
CREATE TABLE "user" (
|
||||
mxid TEXT PRIMARY KEY,
|
||||
uuid uuid,
|
||||
phone TEXT,
|
||||
|
||||
management_room TEXT,
|
||||
space_room TEXT,
|
||||
|
||||
CONSTRAINT user_uuid_unique UNIQUE(uuid)
|
||||
);
|
||||
|
||||
CREATE TABLE user_portal (
|
||||
user_mxid TEXT,
|
||||
portal_chat_id TEXT,
|
||||
portal_receiver uuid,
|
||||
last_read_ts BIGINT NOT NULL DEFAULT 0,
|
||||
in_space BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (user_mxid, portal_chat_id, portal_receiver),
|
||||
CONSTRAINT user_portal_user_fkey FOREIGN KEY (user_mxid)
|
||||
REFERENCES "user"(mxid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT user_portal_portal_fkey FOREIGN KEY (portal_chat_id, portal_receiver)
|
||||
REFERENCES portal(chat_id, receiver) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE message (
|
||||
sender uuid NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
part_index INTEGER NOT NULL,
|
||||
|
||||
signal_chat_id TEXT NOT NULL,
|
||||
signal_receiver uuid NOT NULL,
|
||||
|
||||
mxid TEXT NOT NULL,
|
||||
mx_room TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (sender, timestamp, part_index, signal_receiver),
|
||||
CONSTRAINT message_portal_fkey FOREIGN KEY (signal_chat_id, signal_receiver)
|
||||
REFERENCES portal(chat_id, receiver) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (sender) REFERENCES puppet(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT message_mxid_unique UNIQUE (mxid)
|
||||
);
|
||||
|
||||
CREATE TABLE reaction (
|
||||
msg_author uuid NOT NULL,
|
||||
msg_timestamp BIGINT NOT NULL,
|
||||
-- part_index is not used in reactions, but is required for the foreign key.
|
||||
_part_index INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
author uuid NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
|
||||
signal_chat_id TEXT NOT NULL,
|
||||
signal_receiver uuid NOT NULL,
|
||||
|
||||
mxid TEXT NOT NULL,
|
||||
mx_room TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (msg_author, msg_timestamp, author, signal_receiver),
|
||||
CONSTRAINT reaction_message_fkey FOREIGN KEY (msg_author, msg_timestamp, _part_index, signal_receiver)
|
||||
REFERENCES message (sender, timestamp, part_index, signal_receiver) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (author) REFERENCES puppet(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT reaction_mxid_unique UNIQUE (mxid)
|
||||
);
|
||||
|
||||
CREATE TABLE disappearing_message (
|
||||
mxid TEXT NOT NULL PRIMARY KEY,
|
||||
room_id TEXT NOT NULL,
|
||||
expiration_seconds BIGINT NOT NULL,
|
||||
expiration_ts BIGINT
|
||||
);
|
||||
18
database/upgrades/13-upgrade-mx-state-store.sql
Normal file
18
database/upgrades/13-upgrade-mx-state-store.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- v13: Switch mx_room_state from Python to Go format
|
||||
ALTER TABLE mx_room_state DROP COLUMN is_encrypted;
|
||||
ALTER TABLE mx_room_state DROP COLUMN has_full_member_list;
|
||||
|
||||
-- only: postgres for next 2 lines
|
||||
ALTER TABLE mx_room_state ALTER COLUMN power_levels TYPE jsonb USING power_levels::jsonb;
|
||||
ALTER TABLE mx_room_state ALTER COLUMN encryption TYPE jsonb USING encryption::jsonb;
|
||||
|
||||
ALTER TABLE "user" ADD COLUMN management_room TEXT;
|
||||
|
||||
UPDATE mx_user_profile SET displayname='' WHERE displayname IS NULL;
|
||||
UPDATE mx_user_profile SET avatar_url='' WHERE avatar_url IS NULL;
|
||||
|
||||
CREATE TABLE mx_registrations (
|
||||
user_id TEXT PRIMARY KEY
|
||||
);
|
||||
|
||||
UPDATE mx_version SET version=5;
|
||||
3
database/upgrades/14-remove-notice-room.sql
Normal file
3
database/upgrades/14-remove-notice-room.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- v14: Remove redundant notice_room column from users
|
||||
UPDATE "user" SET management_room = COALESCE(management_room, notice_room);
|
||||
ALTER TABLE "user" DROP COLUMN notice_room;
|
||||
3
database/upgrades/15-remove-unused-puppet-columns.sql
Normal file
3
database/upgrades/15-remove-unused-puppet-columns.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- v15: Remove unused columns in puppet table
|
||||
ALTER TABLE puppet DROP COLUMN next_batch;
|
||||
ALTER TABLE puppet DROP COLUMN base_url;
|
||||
123
database/upgrades/16-refactor-postgres.sql
Normal file
123
database/upgrades/16-refactor-postgres.sql
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
-- v16: Refactor types (Postgres)
|
||||
-- only: postgres
|
||||
|
||||
DROP TABLE IF EXISTS user_portal;
|
||||
|
||||
-- Drop constraints so we can fix timestamps.
|
||||
ALTER TABLE reaction DROP CONSTRAINT reaction_message_fkey;
|
||||
ALTER TABLE message DROP CONSTRAINT message_pkey;
|
||||
|
||||
-- Add part index to message and fix the hacky timestamps
|
||||
ALTER TABLE message ADD COLUMN part_index INTEGER;
|
||||
UPDATE message
|
||||
SET timestamp=CASE WHEN timestamp > 1500000000000000 THEN timestamp / 1000 ELSE timestamp END,
|
||||
part_index=CASE WHEN timestamp > 1500000000000000 THEN timestamp % 1000 ELSE 0 END;
|
||||
-- If the bridge users have reacted to message parts, forget about those, not worth trying to deal with potential conflicts.
|
||||
DELETE FROM reaction WHERE msg_timestamp > 1500000000000000;
|
||||
ALTER TABLE message ALTER COLUMN part_index SET NOT NULL;
|
||||
ALTER TABLE reaction ADD COLUMN _part_index INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Re-add the dropped constraints (but with part index and no chat)
|
||||
DELETE FROM message
|
||||
WHERE (sender, timestamp, part_index, signal_receiver)
|
||||
IN (SELECT DISTINCT sender, timestamp, part_index, signal_receiver FROM message GROUP BY (sender, timestamp, part_index, signal_receiver) HAVING COUNT(*)>1);
|
||||
ALTER TABLE message ADD PRIMARY KEY (sender, timestamp, part_index, signal_receiver);
|
||||
ALTER TABLE message DROP CONSTRAINT IF EXISTS message_signal_chat_id_signal_receiver_fkey;
|
||||
ALTER TABLE message DROP CONSTRAINT IF EXISTS message_signal_chat_id_fkey;
|
||||
-- Also update the reaction primary key
|
||||
ALTER TABLE reaction DROP CONSTRAINT reaction_pkey;
|
||||
ALTER TABLE reaction ADD PRIMARY KEY (author, msg_author, msg_timestamp, signal_receiver);
|
||||
|
||||
-- Change unique constraint from (mxid, mx_room) to just mxid.
|
||||
ALTER TABLE message DROP CONSTRAINT message_mxid_mx_room_key;
|
||||
ALTER TABLE message ADD CONSTRAINT message_mxid_unique UNIQUE (mxid);
|
||||
ALTER TABLE reaction DROP CONSTRAINT reaction_mxid_mx_room_key;
|
||||
ALTER TABLE reaction ADD CONSTRAINT reaction_mxid_unique UNIQUE (mxid);
|
||||
|
||||
CREATE TABLE lost_portals (
|
||||
mxid TEXT PRIMARY KEY,
|
||||
chat_id TEXT,
|
||||
receiver TEXT
|
||||
);
|
||||
INSERT INTO lost_portals SELECT mxid, chat_id, receiver FROM portal WHERE mxid<>'';
|
||||
|
||||
-- Make mxid column unique (requires using nulls for missing values)
|
||||
UPDATE portal SET mxid=NULL WHERE mxid='';
|
||||
ALTER TABLE portal ADD CONSTRAINT portal_mxid_unique UNIQUE(mxid);
|
||||
-- Delete any portals that aren't associated with logged-in users.
|
||||
DELETE FROM portal WHERE receiver<>'' AND receiver NOT IN (SELECT username FROM "user" WHERE username IS NOT NULL AND uuid IS NOT NULL);
|
||||
-- CASCADE manually
|
||||
DELETE FROM message
|
||||
WHERE (signal_chat_id, signal_receiver)
|
||||
NOT IN (SELECT DISTINCT signal_chat_id, signal_receiver FROM message WHERE (signal_chat_id, signal_receiver) IN (SELECT DISTINCT chat_id, receiver FROM portal));
|
||||
DELETE FROM reaction
|
||||
WHERE (author, msg_author, msg_timestamp, signal_receiver)
|
||||
NOT IN (SELECT DISTINCT author, msg_author, msg_timestamp, signal_receiver FROM reaction WHERE (msg_author, msg_timestamp, _part_index, signal_receiver) IN (SELECT DISTINCT sender, timestamp, part_index, signal_receiver FROM message));
|
||||
-- Change receiver to uuid instead of phone number, also add nil uuid for groups.
|
||||
UPDATE portal SET receiver=(SELECT uuid FROM "user" WHERE username=receiver AND uuid IS NOT NULL LIMIT 1) WHERE receiver<>'';
|
||||
UPDATE portal SET receiver='00000000-0000-0000-0000-000000000000' WHERE receiver='';
|
||||
-- CASCADE manually
|
||||
UPDATE message SET signal_receiver=(SELECT uuid FROM "user" WHERE username=signal_receiver AND uuid IS NOT NULL LIMIT 1) WHERE signal_receiver<>'';
|
||||
UPDATE message SET signal_receiver='00000000-0000-0000-0000-000000000000' WHERE signal_receiver='';
|
||||
UPDATE reaction SET signal_receiver=(SELECT uuid FROM "user" WHERE username=signal_receiver AND uuid IS NOT NULL LIMIT 1) WHERE signal_receiver<>'';
|
||||
UPDATE reaction SET signal_receiver='00000000-0000-0000-0000-000000000000' WHERE signal_receiver='';
|
||||
-- Change column types
|
||||
ALTER TABLE portal ALTER COLUMN receiver TYPE uuid USING receiver::uuid;
|
||||
ALTER TABLE message ALTER COLUMN signal_receiver TYPE uuid USING signal_receiver::uuid;
|
||||
ALTER TABLE reaction ALTER COLUMN signal_receiver TYPE uuid USING signal_receiver::uuid;
|
||||
-- Re-add the dropped constraints again
|
||||
ALTER TABLE message ADD CONSTRAINT message_portal_fkey
|
||||
FOREIGN KEY (signal_chat_id, signal_receiver)
|
||||
REFERENCES portal (chat_id, receiver)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE reaction ADD CONSTRAINT reaction_message_fkey FOREIGN KEY (msg_author, msg_timestamp, _part_index, signal_receiver)
|
||||
REFERENCES message (sender, timestamp, part_index, signal_receiver) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
-- Delete group v1 portal entries
|
||||
DELETE FROM portal WHERE chat_id NOT LIKE '________-____-____-____-____________' AND LENGTH(chat_id) <> 44;
|
||||
DELETE FROM lost_portals WHERE mxid IN (SELECT mxid FROM portal WHERE mxid<>'');
|
||||
|
||||
-- Remove unnecessary nullables in portal
|
||||
UPDATE portal SET name='' WHERE name IS NULL;
|
||||
UPDATE portal SET topic='' WHERE topic IS NULL;
|
||||
UPDATE portal SET avatar_hash='' WHERE avatar_hash IS NULL;
|
||||
UPDATE portal SET avatar_url='' WHERE avatar_url IS NULL;
|
||||
UPDATE portal SET expiration_time=0 WHERE expiration_time IS NULL;
|
||||
UPDATE portal SET relay_user_id='' WHERE relay_user_id IS NULL;
|
||||
ALTER TABLE portal ALTER COLUMN name SET NOT NULL;
|
||||
ALTER TABLE portal ALTER COLUMN topic SET NOT NULL;
|
||||
ALTER TABLE portal ALTER COLUMN avatar_hash SET NOT NULL;
|
||||
ALTER TABLE portal ALTER COLUMN avatar_url SET NOT NULL;
|
||||
ALTER TABLE portal ALTER COLUMN expiration_time SET NOT NULL;
|
||||
ALTER TABLE portal ALTER COLUMN relay_user_id SET NOT NULL;
|
||||
|
||||
-- Add unique constraint to custom_mxid
|
||||
UPDATE puppet
|
||||
SET custom_mxid=NULL, access_token=''
|
||||
WHERE custom_mxid<>''
|
||||
AND uuid<>COALESCE((SELECT uuid FROM "user" WHERE mxid=custom_mxid), '00000000-0000-0000-0000-000000000000');
|
||||
UPDATE puppet SET custom_mxid=NULL WHERE custom_mxid='';
|
||||
ALTER TABLE puppet ADD CONSTRAINT puppet_custom_mxid_unique UNIQUE(custom_mxid);
|
||||
-- Remove unnecessary nullables in puppet
|
||||
UPDATE puppet SET name='' WHERE name IS NULL;
|
||||
UPDATE puppet SET avatar_hash='' WHERE avatar_hash IS NULL;
|
||||
UPDATE puppet SET avatar_url='' WHERE avatar_url IS NULL;
|
||||
UPDATE puppet SET access_token='' WHERE access_token IS NULL;
|
||||
ALTER TABLE puppet ALTER COLUMN name SET NOT NULL;
|
||||
ALTER TABLE puppet ALTER COLUMN avatar_hash SET NOT NULL;
|
||||
ALTER TABLE puppet ALTER COLUMN avatar_url SET NOT NULL;
|
||||
ALTER TABLE puppet ALTER COLUMN access_token SET NOT NULL;
|
||||
ALTER TABLE puppet ALTER COLUMN name_quality DROP DEFAULT;
|
||||
|
||||
UPDATE "user"
|
||||
SET uuid=NULL
|
||||
WHERE uuid IN (SELECT DISTINCT uuid FROM "user" WHERE uuid IS NOT NULL GROUP BY uuid HAVING COUNT(*)>1);
|
||||
ALTER TABLE "user" ADD CONSTRAINT user_uuid_unique UNIQUE(uuid);
|
||||
ALTER TABLE "user" RENAME COLUMN username TO phone;
|
||||
|
||||
-- Drop room_id from disappearing message primary key
|
||||
ALTER TABLE disappearing_message DROP CONSTRAINT disappearing_message_pkey;
|
||||
ALTER TABLE disappearing_message ADD PRIMARY KEY (mxid);
|
||||
-- Remove unnecessary nullables in disappearing_message
|
||||
ALTER TABLE disappearing_message ALTER COLUMN room_id SET NOT NULL;
|
||||
UPDATE disappearing_message SET expiration_seconds=0 WHERE expiration_seconds IS NULL;
|
||||
ALTER TABLE disappearing_message ALTER COLUMN expiration_seconds SET NOT NULL;
|
||||
198
database/upgrades/17-refactor-sqlite.sql
Normal file
198
database/upgrades/17-refactor-sqlite.sql
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
-- v17: Refactor types (SQLite)
|
||||
-- transaction: off
|
||||
-- only: sqlite
|
||||
|
||||
-- This is separate from v16 so that postgres can run with transaction: on
|
||||
-- (split upgrades by dialect don't currently allow disabling transaction in only one dialect)
|
||||
|
||||
DROP TABLE IF EXISTS user_portal;
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE message_new (
|
||||
sender uuid NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
part_index INTEGER NOT NULL,
|
||||
|
||||
signal_chat_id TEXT NOT NULL,
|
||||
signal_receiver TEXT NOT NULL,
|
||||
|
||||
mxid TEXT NOT NULL,
|
||||
mx_room TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (sender, timestamp, part_index, signal_receiver),
|
||||
CONSTRAINT message_portal_fkey FOREIGN KEY (signal_chat_id, signal_receiver) REFERENCES portal(chat_id, receiver) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (sender) REFERENCES puppet(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT message_mxid_unique UNIQUE (mxid)
|
||||
);
|
||||
|
||||
CREATE TABLE reaction_new (
|
||||
msg_author uuid NOT NULL,
|
||||
msg_timestamp BIGINT NOT NULL,
|
||||
-- part_index is not used in reactions, but is required for the foreign key.
|
||||
_part_index INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
author uuid NOT NULL,
|
||||
emoji TEXT NOT NULL,
|
||||
|
||||
signal_chat_id TEXT NOT NULL,
|
||||
signal_receiver TEXT NOT NULL,
|
||||
|
||||
mxid TEXT NOT NULL,
|
||||
mx_room TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (msg_author, msg_timestamp, author, signal_receiver),
|
||||
CONSTRAINT reaction_message_fkey FOREIGN KEY (msg_author, msg_timestamp, _part_index, signal_receiver)
|
||||
REFERENCES message (sender, timestamp, part_index, signal_receiver) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (author) REFERENCES puppet(uuid) ON DELETE CASCADE,
|
||||
CONSTRAINT reaction_mxid_unique UNIQUE (mxid)
|
||||
);
|
||||
|
||||
|
||||
DELETE FROM message
|
||||
WHERE (sender, timestamp, signal_receiver)
|
||||
IN (SELECT sender, timestamp, signal_receiver FROM message GROUP BY sender, timestamp, signal_receiver HAVING COUNT(*)>1);
|
||||
|
||||
INSERT INTO message_new
|
||||
SELECT sender,
|
||||
CASE WHEN timestamp > 1500000000000000 THEN timestamp / 1000 ELSE timestamp END,
|
||||
CASE WHEN timestamp > 1500000000000000 THEN timestamp % 1000 ELSE 0 END,
|
||||
COALESCE(signal_chat_id, ''),
|
||||
COALESCE(signal_receiver, ''),
|
||||
mxid,
|
||||
mx_room
|
||||
FROM message;
|
||||
|
||||
INSERT INTO reaction_new
|
||||
SELECT msg_author,
|
||||
msg_timestamp,
|
||||
0, -- _part_index
|
||||
author,
|
||||
emoji,
|
||||
COALESCE(signal_chat_id, ''),
|
||||
COALESCE(signal_receiver, ''),
|
||||
mxid,
|
||||
mx_room
|
||||
FROM reaction
|
||||
WHERE msg_timestamp<1500000000000000;
|
||||
|
||||
DROP TABLE message;
|
||||
DROP TABLE reaction;
|
||||
ALTER TABLE message_new RENAME TO message;
|
||||
ALTER TABLE reaction_new RENAME TO reaction;
|
||||
|
||||
PRAGMA foreign_key_check;
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
BEGIN;
|
||||
CREATE TABLE lost_portals (
|
||||
mxid TEXT PRIMARY KEY,
|
||||
chat_id TEXT,
|
||||
receiver TEXT
|
||||
);
|
||||
INSERT INTO lost_portals SELECT mxid, chat_id, receiver FROM portal WHERE mxid<>'';
|
||||
DELETE FROM portal WHERE receiver<>'' AND receiver NOT IN (SELECT username FROM "user" WHERE username IS NOT NULL AND uuid<>'');
|
||||
UPDATE portal SET receiver=(SELECT uuid FROM "user" WHERE username=receiver AND uuid<>'' LIMIT 1) WHERE receiver<>'';
|
||||
UPDATE portal SET receiver='00000000-0000-0000-0000-000000000000' WHERE receiver='';
|
||||
DELETE FROM portal WHERE chat_id NOT LIKE '________-____-____-____-____________' AND LENGTH(chat_id) <> 44;
|
||||
DELETE FROM lost_portals WHERE mxid IN (SELECT mxid FROM portal WHERE mxid<>'');
|
||||
COMMIT;
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE portal_new (
|
||||
chat_id TEXT NOT NULL,
|
||||
receiver uuid NOT NULL,
|
||||
mxid TEXT,
|
||||
name TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
encrypted BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_hash TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
expiration_time BIGINT NOT NULL,
|
||||
relay_user_id TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (chat_id, receiver),
|
||||
CONSTRAINT portal_mxid_unique UNIQUE(mxid)
|
||||
);
|
||||
|
||||
INSERT INTO portal_new
|
||||
SELECT chat_id, receiver, CASE WHEN mxid='' THEN NULL ELSE mxid END,
|
||||
COALESCE(name, ''), COALESCE(topic, ''), encrypted, COALESCE(avatar_hash, ''), COALESCE(avatar_url, ''),
|
||||
name_set, avatar_set, revision, COALESCE(expiration_time, 0), COALESCE(relay_user_id, '')
|
||||
FROM portal;
|
||||
DROP TABLE portal;
|
||||
ALTER TABLE portal_new RENAME TO portal;
|
||||
|
||||
CREATE TABLE puppet_new (
|
||||
uuid uuid PRIMARY KEY,
|
||||
number TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
name_quality INTEGER NOT NULL,
|
||||
avatar_hash TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
is_registered BOOLEAN NOT NULL DEFAULT false,
|
||||
contact_info_set BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
custom_mxid TEXT,
|
||||
access_token TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT puppet_custom_mxid_unique UNIQUE(custom_mxid)
|
||||
);
|
||||
|
||||
UPDATE puppet
|
||||
SET custom_mxid=NULL, access_token=''
|
||||
WHERE custom_mxid<>''
|
||||
AND uuid<>COALESCE((SELECT uuid FROM "user" WHERE mxid=custom_mxid), '00000000-0000-0000-0000-000000000000');
|
||||
INSERT INTO puppet_new
|
||||
SELECT uuid, number, COALESCE(name, ''), COALESCE(name_quality, 0), COALESCE(avatar_hash, ''),
|
||||
COALESCE(avatar_url, ''), name_set, avatar_set, is_registered, contact_info_set,
|
||||
CASE WHEN custom_mxid='' THEN NULL ELSE custom_mxid END, COALESCE(access_token, '')
|
||||
FROM puppet;
|
||||
DROP TABLE puppet;
|
||||
ALTER TABLE puppet_new RENAME TO puppet;
|
||||
|
||||
CREATE TABLE user_new (
|
||||
mxid TEXT PRIMARY KEY,
|
||||
uuid uuid,
|
||||
phone TEXT,
|
||||
|
||||
management_room TEXT,
|
||||
|
||||
CONSTRAINT user_uuid_unique UNIQUE(uuid)
|
||||
);
|
||||
|
||||
INSERT INTO user_new
|
||||
SELECT mxid, uuid, username, management_room
|
||||
FROM user;
|
||||
DROP TABLE user;
|
||||
ALTER TABLE user_new RENAME TO user;
|
||||
|
||||
CREATE TABLE disappearing_message_new (
|
||||
mxid TEXT NOT NULL PRIMARY KEY,
|
||||
room_id TEXT NOT NULL,
|
||||
expiration_seconds BIGINT NOT NULL,
|
||||
expiration_ts BIGINT
|
||||
);
|
||||
|
||||
INSERT INTO disappearing_message_new
|
||||
SELECT mxid, room_id, COALESCE(expiration_seconds, 0), expiration_ts
|
||||
FROM disappearing_message;
|
||||
DROP TABLE disappearing_message;
|
||||
ALTER TABLE disappearing_message_new RENAME TO disappearing_message;
|
||||
|
||||
PRAGMA foreign_key_check;
|
||||
COMMIT;
|
||||
PRAGMA foreign_keys = ON;
|
||||
17
database/upgrades/18-spaces.sql
Normal file
17
database/upgrades/18-spaces.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- v18 (compatible with v17+): Add columns for personal filtering space info
|
||||
ALTER TABLE "user" ADD COLUMN space_room TEXT;
|
||||
|
||||
DROP TABLE IF EXISTS user_portal;
|
||||
CREATE TABLE user_portal (
|
||||
user_mxid TEXT,
|
||||
portal_chat_id TEXT,
|
||||
portal_receiver uuid,
|
||||
last_read_ts BIGINT NOT NULL DEFAULT 0,
|
||||
in_space BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (user_mxid, portal_chat_id, portal_receiver),
|
||||
CONSTRAINT user_portal_user_fkey FOREIGN KEY (user_mxid)
|
||||
REFERENCES "user"(mxid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT user_portal_portal_fkey FOREIGN KEY (portal_chat_id, portal_receiver)
|
||||
REFERENCES portal(chat_id, receiver) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
5
database/upgrades/19-more-portal-metadata.sql
Normal file
5
database/upgrades/19-more-portal-metadata.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- v19 (compatible with v17+): Add more metadata for portals
|
||||
ALTER TABLE portal ADD COLUMN topic_set BOOLEAN NOT NULL DEFAULT false;
|
||||
UPDATE portal SET topic_set=true WHERE topic<>'';
|
||||
ALTER TABLE portal ADD COLUMN avatar_path TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE puppet ADD COLUMN avatar_path TEXT NOT NULL DEFAULT '';
|
||||
2
database/upgrades/20-puppet-profile-fetch-ts.sql
Normal file
2
database/upgrades/20-puppet-profile-fetch-ts.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- v20 (compatible with v17+): Add profile fetch timestamp for puppets
|
||||
ALTER TABLE puppet ADD profile_fetched_at BIGINT;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2025 Tulir Asokan
|
||||
// mautrix-signal - A Matrix-Signal puppeting bridge.
|
||||
// Copyright (C) 2023 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
|
||||
|
|
@ -18,19 +18,23 @@ package upgrades
|
|||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
)
|
||||
|
||||
var Table dbutil.UpgradeTable
|
||||
|
||||
//go:embed *.sql
|
||||
var rawUpgrades embed.FS
|
||||
|
||||
func init() {
|
||||
Table.Register(-1, 20, 13, "Add missing columns for backup chat table", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) (err error) {
|
||||
var exists bool
|
||||
if exists, err = db.ColumnExists(ctx, "signalmeow_backup_chat", "latest_message_id"); err == nil && !exists {
|
||||
_, err = db.Exec(ctx, `
|
||||
ALTER TABLE signalmeow_backup_chat ADD COLUMN latest_message_id BIGINT;
|
||||
ALTER TABLE signalmeow_backup_chat ADD COLUMN total_message_count INTEGER;
|
||||
`)
|
||||
}
|
||||
return
|
||||
Table.Register(-1, 12, 0, "Unsupported version", false, func(ctx context.Context, database *dbutil.Database) error {
|
||||
return errors.New("please upgrade to mautrix-signal v0.4.3 before upgrading to a newer version")
|
||||
})
|
||||
Table.Register(1, 13, 0, "Jump to version 13", false, func(ctx context.Context, database *dbutil.Database) error {
|
||||
return nil
|
||||
})
|
||||
Table.RegisterFS(rawUpgrades)
|
||||
}
|
||||
115
database/user.go
Normal file
115
database/user.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
const (
|
||||
getUserByMXIDQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE mxid=$1`
|
||||
getUserByPhoneQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE phone=$1`
|
||||
getUserByUUIDQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE uuid=$1`
|
||||
getAllLoggedInUsersQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE phone IS NOT NULL`
|
||||
insertUserQuery = `INSERT INTO "user" (mxid, phone, uuid, management_room, space_room) VALUES ($1, $2, $3, $4, $5)`
|
||||
updateUserQuery = `UPDATE "user" SET phone=$2, uuid=$3, management_room=$4, space_room=$5 WHERE mxid=$1`
|
||||
)
|
||||
|
||||
type UserQuery struct {
|
||||
*dbutil.QueryHelper[*User]
|
||||
}
|
||||
|
||||
type User struct {
|
||||
qh *dbutil.QueryHelper[*User]
|
||||
|
||||
MXID id.UserID
|
||||
SignalUsername string
|
||||
SignalID uuid.UUID
|
||||
ManagementRoom id.RoomID
|
||||
SpaceRoom id.RoomID
|
||||
|
||||
lastReadCache map[PortalKey]uint64
|
||||
lastReadCacheLock sync.Mutex
|
||||
inSpaceCache map[PortalKey]bool
|
||||
inSpaceCacheLock sync.Mutex
|
||||
}
|
||||
|
||||
func newUser(qh *dbutil.QueryHelper[*User]) *User {
|
||||
return &User{
|
||||
qh: qh,
|
||||
|
||||
lastReadCache: make(map[PortalKey]uint64),
|
||||
inSpaceCache: make(map[PortalKey]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (uq *UserQuery) GetByMXID(ctx context.Context, mxid id.UserID) (*User, error) {
|
||||
return uq.QueryOne(ctx, getUserByMXIDQuery, mxid)
|
||||
}
|
||||
|
||||
func (uq *UserQuery) GetByPhone(ctx context.Context, phone string) (*User, error) {
|
||||
return uq.QueryOne(ctx, getUserByPhoneQuery, phone)
|
||||
}
|
||||
|
||||
func (uq *UserQuery) GetBySignalID(ctx context.Context, uuid uuid.UUID) (*User, error) {
|
||||
return uq.QueryOne(ctx, getUserByUUIDQuery, uuid)
|
||||
}
|
||||
|
||||
func (uq *UserQuery) GetAllLoggedIn(ctx context.Context) ([]*User, error) {
|
||||
return uq.QueryMany(ctx, getAllLoggedInUsersQuery)
|
||||
}
|
||||
|
||||
func (u *User) sqlVariables() []any {
|
||||
var nu uuid.NullUUID
|
||||
nu.UUID = u.SignalID
|
||||
nu.Valid = u.SignalID != uuid.Nil
|
||||
return []any{u.MXID, dbutil.StrPtr(u.SignalUsername), nu, dbutil.StrPtr(u.ManagementRoom), dbutil.StrPtr(u.SpaceRoom)}
|
||||
}
|
||||
|
||||
func (u *User) Insert(ctx context.Context) error {
|
||||
return u.qh.Exec(ctx, insertUserQuery, u.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (u *User) Update(ctx context.Context) error {
|
||||
return u.qh.Exec(ctx, updateUserQuery, u.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (u *User) Scan(row dbutil.Scannable) (*User, error) {
|
||||
var phone, managementRoom, spaceRoom sql.NullString
|
||||
var signalID uuid.NullUUID
|
||||
err := row.Scan(
|
||||
&u.MXID,
|
||||
&phone,
|
||||
&signalID,
|
||||
&managementRoom,
|
||||
&spaceRoom,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.SignalUsername = phone.String
|
||||
u.SignalID = signalID.UUID
|
||||
u.ManagementRoom = id.RoomID(managementRoom.String)
|
||||
u.SpaceRoom = id.RoomID(spaceRoom.String)
|
||||
return u, nil
|
||||
}
|
||||
116
database/userportal.go
Normal file
116
database/userportal.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber, 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 database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
getLastReadTSQuery = `SELECT last_read_ts FROM user_portal WHERE user_mxid=$1 AND portal_chat_id=$2 AND portal_receiver=$3`
|
||||
setLastReadTSQuery = `
|
||||
INSERT INTO user_portal (user_mxid, portal_chat_id, portal_receiver, last_read_ts) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_mxid, portal_chat_id, portal_receiver) DO UPDATE
|
||||
SET last_read_ts=excluded.last_read_ts WHERE user_portal.last_read_ts<excluded.last_read_ts
|
||||
`
|
||||
getIsInSpaceQuery = `SELECT in_space FROM user_portal WHERE user_mxid=$1 AND portal_chat_id=$2 AND portal_receiver=$3`
|
||||
setIsInSpaceQuery = `
|
||||
INSERT INTO user_portal (user_mxid, portal_chat_id, portal_receiver, in_space) VALUES ($1, $2, $3, true)
|
||||
ON CONFLICT (user_mxid, portal_chat_id, portal_receiver) DO UPDATE SET in_space=true
|
||||
`
|
||||
)
|
||||
|
||||
func (u *User) GetLastReadTS(ctx context.Context, portal PortalKey) uint64 {
|
||||
u.lastReadCacheLock.Lock()
|
||||
defer u.lastReadCacheLock.Unlock()
|
||||
if cached, ok := u.lastReadCache[portal]; ok {
|
||||
return cached
|
||||
}
|
||||
var ts int64
|
||||
err := u.qh.GetDB().QueryRow(ctx, getLastReadTSQuery, u.MXID, portal.ChatID, portal.Receiver).Scan(&ts)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("user_id", u.MXID).
|
||||
Any("portal_key", portal).
|
||||
Msg("Failed to query last read timestamp")
|
||||
return 0
|
||||
}
|
||||
u.lastReadCache[portal] = uint64(ts)
|
||||
return uint64(ts)
|
||||
}
|
||||
|
||||
func (u *User) SetLastReadTS(ctx context.Context, portal PortalKey, ts uint64) {
|
||||
u.lastReadCacheLock.Lock()
|
||||
defer u.lastReadCacheLock.Unlock()
|
||||
err := u.qh.Exec(ctx, setLastReadTSQuery, u.MXID, portal.ChatID, portal.Receiver, int64(ts))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("user_id", u.MXID).
|
||||
Any("portal_key", portal).
|
||||
Msg("Failed to update last read timestamp")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("user_id", u.MXID).
|
||||
Any("portal_key", portal).
|
||||
Uint64("last_read_ts", ts).
|
||||
Msg("Updated last read timestamp of portal")
|
||||
u.lastReadCache[portal] = ts
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsInSpace(ctx context.Context, portal PortalKey) bool {
|
||||
u.inSpaceCacheLock.Lock()
|
||||
defer u.inSpaceCacheLock.Unlock()
|
||||
if cached, ok := u.inSpaceCache[portal]; ok {
|
||||
return cached
|
||||
}
|
||||
var inSpace bool
|
||||
err := u.qh.GetDB().QueryRow(ctx, getIsInSpaceQuery, u.MXID, portal.ChatID, portal.Receiver).Scan(&inSpace)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("user_id", u.MXID).
|
||||
Any("portal_key", portal).
|
||||
Msg("Failed to query in space status")
|
||||
return false
|
||||
}
|
||||
u.inSpaceCache[portal] = inSpace
|
||||
return inSpace
|
||||
}
|
||||
|
||||
func (u *User) MarkInSpace(ctx context.Context, portal PortalKey) {
|
||||
u.inSpaceCacheLock.Lock()
|
||||
defer u.inSpaceCacheLock.Unlock()
|
||||
err := u.qh.Exec(ctx, setIsInSpaceQuery, u.MXID, portal.ChatID, portal.Receiver)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("user_id", u.MXID).
|
||||
Any("portal_key", portal).
|
||||
Msg("Failed to update in space status")
|
||||
} else {
|
||||
u.inSpaceCache[portal] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) RemoveInSpaceCache(key PortalKey) {
|
||||
u.inSpaceCacheLock.Lock()
|
||||
defer u.inSpaceCacheLock.Unlock()
|
||||
delete(u.inSpaceCache, key)
|
||||
}
|
||||
156
disappearing.go
Normal file
156
disappearing.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/database"
|
||||
)
|
||||
|
||||
type DisappearingMessagesManager struct {
|
||||
DB *database.Database
|
||||
Log zerolog.Logger
|
||||
Bridge *SignalBridge
|
||||
checkMessagesChan chan struct{}
|
||||
}
|
||||
|
||||
func (dmm *DisappearingMessagesManager) ScheduleDisappearingForRoom(ctx context.Context, roomID id.RoomID) {
|
||||
log := dmm.Log.With().Stringer("room_id", roomID).Logger()
|
||||
disappearingMessages, err := dmm.DB.DisappearingMessage.GetUnscheduledForRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get unscheduled disappearing messages")
|
||||
return
|
||||
}
|
||||
for _, disappearingMessage := range disappearingMessages {
|
||||
err = disappearingMessage.StartExpirationTimer(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to schedule disappearing message")
|
||||
} else {
|
||||
log.Debug().
|
||||
Stringer("event_id", disappearingMessage.EventID).
|
||||
Time("expire_at", disappearingMessage.ExpireAt).
|
||||
Msg("Scheduling disappearing message")
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the disappearing messages loop to check again
|
||||
dmm.checkMessagesChan <- struct{}{}
|
||||
}
|
||||
|
||||
func (dmm *DisappearingMessagesManager) StartDisappearingLoop(ctx context.Context) {
|
||||
dmm.checkMessagesChan = make(chan struct{}, 1)
|
||||
go func() {
|
||||
log := dmm.Log.With().Str("action", "loop").Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
for {
|
||||
dmm.redactExpiredMessages(ctx)
|
||||
|
||||
duration := 10 * time.Minute // Check again in 10 minutes just in case
|
||||
nextMsg, err := dmm.DB.DisappearingMessage.GetNextScheduledMessage(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Err(err).Msg("Failed to get next disappearing message")
|
||||
continue
|
||||
} else if nextMsg != nil {
|
||||
duration = time.Until(nextMsg.ExpireAt)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
case <-dmm.checkMessagesChan:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (dmm *DisappearingMessagesManager) redactExpiredMessages(ctx context.Context) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
expiredMessages, err := dmm.DB.DisappearingMessage.GetExpiredMessages(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get expired disappearing messages")
|
||||
return
|
||||
}
|
||||
|
||||
for _, msg := range expiredMessages {
|
||||
portal := dmm.Bridge.GetPortalByMXID(msg.RoomID)
|
||||
if portal == nil {
|
||||
log.Warn().Stringer("event_id", msg.EventID).Stringer("room_id", msg.RoomID).Msg("Failed to redact message: portal not found")
|
||||
err = msg.Delete(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Stringer("event_id", msg.EventID).
|
||||
Msg("Failed to delete disappearing message row in database")
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err = portal.MainIntent().RedactEvent(ctx, msg.RoomID, msg.EventID, mautrix.ReqRedact{
|
||||
Reason: "Message expired",
|
||||
TxnID: fmt.Sprintf("mxsg_disappear_%s", msg.EventID),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Stringer("event_id", msg.EventID).
|
||||
Stringer("room_id", msg.RoomID).
|
||||
Msg("Failed to redact message")
|
||||
} else {
|
||||
log.Err(err).
|
||||
Stringer("event_id", msg.EventID).
|
||||
Stringer("room_id", msg.RoomID).
|
||||
Msg("Redacted message")
|
||||
}
|
||||
err = msg.Delete(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Stringer("event_id", msg.EventID).
|
||||
Msg("Failed to delete disappearing message row in database")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dmm *DisappearingMessagesManager) AddDisappearingMessage(ctx context.Context, eventID id.EventID, roomID id.RoomID, expireIn time.Duration, startTimerNow bool) {
|
||||
if expireIn == 0 {
|
||||
return
|
||||
}
|
||||
var expireAt time.Time
|
||||
if startTimerNow {
|
||||
expireAt = time.Now().Add(expireIn)
|
||||
}
|
||||
disappearingMessage := dmm.DB.DisappearingMessage.NewWithValues(roomID, eventID, expireIn, expireAt)
|
||||
err := disappearingMessage.Insert(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("event_id", eventID).
|
||||
Msg("Failed to add disappearing message to database")
|
||||
return
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().Stringer("event_id", eventID).
|
||||
Msg("Added disappearing message row to database")
|
||||
if startTimerNow {
|
||||
// Tell the disappearing messages loop to check again
|
||||
dmm.checkMessagesChan <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ if [[ -z "$GID" ]]; then
|
|||
GID="$UID"
|
||||
fi
|
||||
|
||||
BINARY_NAME=/usr/bin/mautrix-signal
|
||||
|
||||
# Define functions.
|
||||
function fixperms {
|
||||
chown -R $UID:$GID /data
|
||||
|
|
@ -17,7 +15,7 @@ function fixperms {
|
|||
}
|
||||
|
||||
if [[ ! -f /data/config.yaml ]]; then
|
||||
$BINARY_NAME -c /data/config.yaml -e
|
||||
cp /opt/mautrix-signal/example-config.yaml /data/config.yaml
|
||||
echo "Didn't find a config file."
|
||||
echo "Copied default config file to /data/config.yaml"
|
||||
echo "Modify that config file to your liking."
|
||||
|
|
@ -26,7 +24,7 @@ if [[ ! -f /data/config.yaml ]]; then
|
|||
fi
|
||||
|
||||
if [[ ! -f /data/registration.yaml ]]; then
|
||||
$BINARY_NAME -g -c /data/config.yaml -r /data/registration.yaml || exit $?
|
||||
/usr/bin/mautrix-signal -g -c /data/config.yaml -r /data/registration.yaml || exit $?
|
||||
echo "Didn't find a registration file."
|
||||
echo "Generated one for you."
|
||||
echo "See https://docs.mau.fi/bridges/general/registering-appservices.html on how to use it."
|
||||
|
|
@ -36,12 +34,13 @@ fi
|
|||
cd /data
|
||||
fixperms
|
||||
|
||||
EXE=/usr/bin/mautrix-signal
|
||||
DLV=/usr/bin/dlv
|
||||
if [ -x "$DLV" ]; then
|
||||
if [ "$DBGWAIT" != 1 ]; then
|
||||
NOWAIT=1
|
||||
fi
|
||||
BINARY_NAME="${DLV} exec ${BINARY_NAME} ${NOWAIT:+--continue --accept-multiclient} --api-version 2 --headless -l :4040"
|
||||
EXE="${DLV} exec ${EXE} ${NOWAIT:+--continue --accept-multiclient} --api-version 2 --headless -l :4040"
|
||||
fi
|
||||
|
||||
exec su-exec $UID:$GID $BINARY_NAME
|
||||
exec su-exec $UID:$GID $EXE
|
||||
|
|
|
|||
313
example-config.yaml
Normal file
313
example-config.yaml
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# Homeserver details.
|
||||
homeserver:
|
||||
# The address that this appservice can use to connect to the homeserver.
|
||||
address: https://matrix.example.com
|
||||
# The domain of the homeserver (also known as server_name, used for MXIDs, etc).
|
||||
domain: example.com
|
||||
|
||||
# What software is the homeserver running?
|
||||
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
|
||||
software: standard
|
||||
# The URL to push real-time bridge status to.
|
||||
# If set, the bridge will make POST requests to this URL whenever a user's Signal connection state changes.
|
||||
# The bridge will use the appservice as_token to authorize requests.
|
||||
status_endpoint: null
|
||||
# Endpoint for reporting per-message status.
|
||||
message_send_checkpoint_endpoint: null
|
||||
# Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246?
|
||||
async_media: false
|
||||
|
||||
# Should the bridge use a websocket for connecting to the homeserver?
|
||||
# The server side is currently not documented anywhere and is only implemented by mautrix-wsproxy,
|
||||
# mautrix-asmux (deprecated), and hungryserv (proprietary).
|
||||
websocket: false
|
||||
# How often should the websocket be pinged? Pinging will be disabled if this is zero.
|
||||
ping_interval_seconds: 0
|
||||
|
||||
# Application service host/registration related details.
|
||||
# Changing these values requires regeneration of the registration.
|
||||
appservice:
|
||||
# The address that the homeserver can use to connect to this appservice.
|
||||
address: http://localhost:29328
|
||||
|
||||
# The hostname and port where this appservice should listen.
|
||||
hostname: 0.0.0.0
|
||||
port: 29328
|
||||
|
||||
# Database config.
|
||||
database:
|
||||
# The database type. "sqlite3-fk-wal" and "postgres" are supported.
|
||||
type: postgres
|
||||
# The database URI.
|
||||
# SQLite: A raw file path is supported, but `file:<path>?_txlock=immediate` is recommended.
|
||||
# https://github.com/mattn/go-sqlite3#connection-string
|
||||
# Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
|
||||
# To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
|
||||
uri: postgres://user:password@host/database?sslmode=disable
|
||||
# Maximum number of connections. Mostly relevant for Postgres.
|
||||
max_open_conns: 20
|
||||
max_idle_conns: 2
|
||||
# Maximum connection idle time and lifetime before they're closed. Disabled if null.
|
||||
# Parsed with https://pkg.go.dev/time#ParseDuration
|
||||
max_conn_idle_time: null
|
||||
max_conn_lifetime: null
|
||||
|
||||
# The unique ID of this appservice.
|
||||
id: signal
|
||||
# Appservice bot details.
|
||||
bot:
|
||||
# Username of the appservice bot.
|
||||
username: signalbot
|
||||
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
||||
# to leave display name/avatar as-is.
|
||||
displayname: Signal bridge bot
|
||||
avatar: mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp
|
||||
|
||||
# Whether or not to receive ephemeral events via appservice transactions.
|
||||
# Requires MSC2409 support (i.e. Synapse 1.22+).
|
||||
ephemeral_events: true
|
||||
|
||||
# Should incoming events be handled asynchronously?
|
||||
# This may be necessary for large public instances with lots of messages going through.
|
||||
# However, messages will not be guaranteed to be bridged in the same order they were sent in.
|
||||
async_transactions: false
|
||||
|
||||
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
|
||||
as_token: "This value is generated when generating the registration"
|
||||
hs_token: "This value is generated when generating the registration"
|
||||
|
||||
# Prometheus config.
|
||||
metrics:
|
||||
# Enable prometheus metrics?
|
||||
enabled: false
|
||||
# IP and port where the metrics listener should be. The path is always /metrics
|
||||
listen: 127.0.0.1:8000
|
||||
|
||||
signal:
|
||||
# Default device name that shows up in the Signal app.
|
||||
device_name: mautrix-signal
|
||||
|
||||
# Bridge config
|
||||
bridge:
|
||||
# Localpart template of MXIDs for Signal users.
|
||||
# {{.}} is replaced with the internal ID of the Signal user.
|
||||
username_template: signal_{{.}}
|
||||
# Displayname template for Signal users. This is also used as the room name in DMs if private_chat_portal_meta is enabled.
|
||||
# {{.ProfileName}} - The Signal profile name set by the user.
|
||||
# {{.ContactName}} - The name for the user from your phone's contact list. This is not safe on multi-user instances.
|
||||
# {{.PhoneNumber}} - The phone number of the user.
|
||||
# {{.UUID}} - The UUID of the Signal user.
|
||||
# {{.AboutEmoji}} - The emoji set by the user in their profile.
|
||||
displayname_template: '{{or .ProfileName .PhoneNumber "Unknown user"}}'
|
||||
# Whether to explicitly set the avatar and room name for private chat portal rooms.
|
||||
# If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.
|
||||
# If set to `always`, all DM rooms will have explicit names and avatars set.
|
||||
# If set to `never`, DM rooms will never have names and avatars set.
|
||||
private_chat_portal_meta: default
|
||||
# Should avatars from the user's contact list be used? This is not safe on multi-user instances.
|
||||
use_contact_avatars: false
|
||||
# Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.
|
||||
use_outdated_profiles: false
|
||||
# Should the Signal user's phone number be included in the room topic in private chat portal rooms?
|
||||
number_in_topic: true
|
||||
# Avatar image for the Note to Self room.
|
||||
note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
|
||||
|
||||
portal_message_buffer: 128
|
||||
|
||||
# Should the bridge create a space for each logged-in user and add bridged rooms to it?
|
||||
# Users who logged in before turning this on should run `!signal sync-space` to create and fill the space for the first time.
|
||||
personal_filtering_spaces: false
|
||||
# Should Matrix m.notice-type messages be bridged?
|
||||
bridge_notices: true
|
||||
# Should the bridge send a read receipt from the bridge bot when a message has been sent to Signal?
|
||||
delivery_receipts: false
|
||||
# Whether the bridge should send the message status as a custom com.beeper.message_send_status event.
|
||||
message_status_events: false
|
||||
# Whether the bridge should send error notices via m.notice events when a message fails to bridge.
|
||||
message_error_notices: true
|
||||
# Should the bridge update the m.direct account data event when double puppeting is enabled.
|
||||
# Note that updating the m.direct event is not atomic (except with mautrix-asmux)
|
||||
# and is therefore prone to race conditions.
|
||||
sync_direct_chat_list: false
|
||||
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
|
||||
# This field will automatically be changed back to false after it, except if the config file is not writable.
|
||||
resend_bridge_info: false
|
||||
# Whether or not to make portals of groups that don't need approval of an admin to join by invite
|
||||
# link publicly joinable on Matrix.
|
||||
public_portals: false
|
||||
# Send captions in the same message as images. This will send data compatible with both MSC2530.
|
||||
# This is currently not supported in most clients.
|
||||
caption_in_message: false
|
||||
# Whether or not created rooms should have federation enabled.
|
||||
# If false, created portal rooms will never be federated.
|
||||
federate_rooms: true
|
||||
# Servers to always allow double puppeting from
|
||||
double_puppet_server_map:
|
||||
example.com: https://example.com
|
||||
# Allow using double puppeting from any server with a valid client .well-known file.
|
||||
double_puppet_allow_discovery: false
|
||||
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
|
||||
#
|
||||
# If set, double puppeting will be enabled automatically for local users
|
||||
# instead of users having to find an access token and run `login-matrix`
|
||||
# manually.
|
||||
login_shared_secret_map:
|
||||
example.com: foobar
|
||||
|
||||
# Maximum time for handling Matrix events. Duration strings formatted for https://pkg.go.dev/time#ParseDuration
|
||||
# Null means there's no enforced timeout.
|
||||
message_handling_timeout:
|
||||
# Send an error message after this timeout, but keep waiting for the response until the deadline.
|
||||
# This is counted from the origin_server_ts, so the warning time is consistent regardless of the source of delay.
|
||||
# If the message is older than this when it reaches the bridge, the message won't be handled at all.
|
||||
error_after: null
|
||||
# Drop messages after this timeout. They may still go through if the message got sent to the servers.
|
||||
# This is counted from the time the bridge starts handling the message.
|
||||
deadline: 120s
|
||||
|
||||
# The prefix for commands. Only required in non-management rooms.
|
||||
command_prefix: '!signal'
|
||||
# Messages sent upon joining a management room.
|
||||
# Markdown is supported. The defaults are listed below.
|
||||
management_room_text:
|
||||
# Sent when joining a room.
|
||||
welcome: "Hello, I'm a Signal bridge bot."
|
||||
# Sent when joining a management room and the user is already logged in.
|
||||
welcome_connected: "Use `help` for help."
|
||||
# Sent when joining a management room and the user is not logged in.
|
||||
welcome_unconnected: "Use `help` for help or `login` to log in."
|
||||
# Optional extra text sent when joining a management room.
|
||||
additional_help: ""
|
||||
|
||||
# End-to-bridge encryption support options.
|
||||
#
|
||||
# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.
|
||||
encryption:
|
||||
# Allow encryption, work in group chat rooms with e2ee enabled
|
||||
allow: false
|
||||
# Default to encryption, force-enable encryption in all portals the bridge creates
|
||||
# This will cause the bridge bot to be in private chats for the encryption to work properly.
|
||||
default: false
|
||||
# Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.
|
||||
appservice: false
|
||||
# Require encryption, drop any unencrypted messages.
|
||||
require: false
|
||||
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.
|
||||
# You must use a client that supports requesting keys from other users to use this feature.
|
||||
allow_key_sharing: false
|
||||
# Options for deleting megolm sessions from the bridge.
|
||||
delete_keys:
|
||||
# Beeper-specific: delete outbound sessions when hungryserv confirms
|
||||
# that the user has uploaded the key to key backup.
|
||||
delete_outbound_on_ack: false
|
||||
# Don't store outbound sessions in the inbound table.
|
||||
dont_store_outbound: false
|
||||
# Ratchet megolm sessions forward after decrypting messages.
|
||||
ratchet_on_decrypt: false
|
||||
# Delete fully used keys (index >= max_messages) after decrypting messages.
|
||||
delete_fully_used_on_decrypt: false
|
||||
# Delete previous megolm sessions from same device when receiving a new one.
|
||||
delete_prev_on_new_session: false
|
||||
# Delete megolm sessions received from a device when the device is deleted.
|
||||
delete_on_device_delete: false
|
||||
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
|
||||
periodically_delete_expired: false
|
||||
# Delete inbound megolm sessions that don't have the received_at field used for
|
||||
# automatic ratcheting and expired session deletion. This is meant as a migration
|
||||
# to delete old keys prior to the bridge update.
|
||||
delete_outdated_inbound: false
|
||||
# What level of device verification should be required from users?
|
||||
#
|
||||
# Valid levels:
|
||||
# unverified - Send keys to all device in the room.
|
||||
# cross-signed-untrusted - Require valid cross-signing, but trust all cross-signing keys.
|
||||
# cross-signed-tofu - Require valid cross-signing, trust cross-signing keys on first use (and reject changes).
|
||||
# cross-signed-verified - Require valid cross-signing, plus a valid user signature from the bridge bot.
|
||||
# Note that creating user signatures from the bridge bot is not currently possible.
|
||||
# verified - Require manual per-device verification
|
||||
# (currently only possible by modifying the `trust` column in the `crypto_device` database table).
|
||||
verification_levels:
|
||||
# Minimum level for which the bridge should send keys to when bridging messages from Signal to Matrix.
|
||||
receive: unverified
|
||||
# Minimum level that the bridge should accept for incoming Matrix messages.
|
||||
send: unverified
|
||||
# Minimum level that the bridge should require for accepting key requests.
|
||||
share: cross-signed-tofu
|
||||
# Options for Megolm room key rotation. These options allow you to
|
||||
# configure the m.room.encryption event content. See:
|
||||
# https://spec.matrix.org/v1.3/client-server-api/#mroomencryption for
|
||||
# more information about that event.
|
||||
rotation:
|
||||
# Enable custom Megolm room key rotation settings. Note that these
|
||||
# settings will only apply to rooms created after this option is
|
||||
# set.
|
||||
enable_custom: false
|
||||
# The maximum number of milliseconds a session should be used
|
||||
# before changing it. The Matrix spec recommends 604800000 (a week)
|
||||
# as the default.
|
||||
milliseconds: 604800000
|
||||
# The maximum number of messages that should be sent with a given a
|
||||
# session before changing it. The Matrix spec recommends 100 as the
|
||||
# default.
|
||||
messages: 100
|
||||
|
||||
# Disable rotating keys when a user's devices change?
|
||||
# You should not enable this option unless you understand all the implications.
|
||||
disable_device_change_key_rotation: false
|
||||
# Should leaving the room on Matrix make the user leave on Signal?
|
||||
bridge_matrix_leave: true
|
||||
# Settings for provisioning API
|
||||
provisioning:
|
||||
# Prefix for the provisioning API paths.
|
||||
prefix: /_matrix/provision
|
||||
# Shared secret for authentication. If set to "generate", a random secret will be generated,
|
||||
# or if set to "disable", the provisioning API will be disabled.
|
||||
shared_secret: generate
|
||||
# Enable debug API at /debug with provisioning authentication.
|
||||
debug_endpoints: false
|
||||
|
||||
# Permissions for using the bridge.
|
||||
# Permitted values:
|
||||
# relay - Talk through the relaybot (if enabled), no access otherwise
|
||||
# user - Access to use the bridge to chat with a Signal account.
|
||||
# admin - User level and some additional administration tools
|
||||
# Permitted keys:
|
||||
# * - All Matrix users
|
||||
# domain - All users on that homeserver
|
||||
# mxid - Specific user
|
||||
permissions:
|
||||
"*": relay
|
||||
"example.com": user
|
||||
"@admin:example.com": admin
|
||||
|
||||
# Settings for relay mode
|
||||
relay:
|
||||
# Whether relay mode should be allowed. If allowed, `!signal set-relay` can be used to turn any
|
||||
# authenticated user into a relaybot for that chat.
|
||||
enabled: false
|
||||
# Should only admins be allowed to set themselves as relay users?
|
||||
admin_only: true
|
||||
# The formats to use when sending messages to Signal via the relaybot.
|
||||
message_formats:
|
||||
m.text: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
|
||||
m.notice: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
|
||||
m.emote: "* <b>{{ .Sender.Displayname }}</b> {{ .Message }}"
|
||||
m.file: "<b>{{ .Sender.Displayname }}</b> sent a file"
|
||||
m.image: "<b>{{ .Sender.Displayname }}</b> sent an image"
|
||||
m.audio: "<b>{{ .Sender.Displayname }}</b> sent an audio file"
|
||||
m.video: "<b>{{ .Sender.Displayname }}</b> sent a video"
|
||||
m.location: "<b>{{ .Sender.Displayname }}</b> sent a location"
|
||||
|
||||
# Logging config. See https://github.com/tulir/zeroconfig for details.
|
||||
logging:
|
||||
min_level: debug
|
||||
writers:
|
||||
- type: stdout
|
||||
format: pretty-colored
|
||||
- type: file
|
||||
format: json
|
||||
filename: ./logs/mautrix-signal.log
|
||||
max_size: 100
|
||||
max_backups: 10
|
||||
compress: true
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1703255338,
|
||||
"narHash": "sha256-Z6wfYJQKmDN9xciTwU3cOiOk+NElxdZwy/FiHctCzjU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6df37dc6a77654682fe9f071c62b4242b5342e04",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
33
flake.nix
Normal file
33
flake.nix
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
description = "mautrix-signal development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
(flake-utils.lib.eachDefaultSystem (system:
|
||||
let pkgs = import nixpkgs { inherit system; };
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages_11.libclang.lib}/lib";
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
clang
|
||||
cmake
|
||||
gnumake
|
||||
protobuf
|
||||
rust-cbindgen
|
||||
rustup
|
||||
olm
|
||||
|
||||
go_1_20
|
||||
go-tools
|
||||
gotools
|
||||
|
||||
pre-commit
|
||||
];
|
||||
};
|
||||
}));
|
||||
}
|
||||
72
go.mod
72
go.mod
|
|
@ -1,52 +1,50 @@
|
|||
module go.mau.fi/mautrix-signal
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.2
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
|
||||
github.com/beeper/libserv v0.0.0-20231231202820-c7303abfc32c
|
||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-pointer v0.0.1
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
go.mau.fi/util v0.4.1
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f
|
||||
golang.org/x/net v0.22.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9
|
||||
nhooyr.io/websocket v1.8.10
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/yuin/goldmark v1.7.0 // indirect
|
||||
go.mau.fi/zeroconfig v0.1.2 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
)
|
||||
|
|
|
|||
128
go.sum
128
go.sum
|
|
@ -1,87 +1,91 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
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.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/beeper/libserv v0.0.0-20231231202820-c7303abfc32c h1:WqjRVgUO039eiISCjsZC4F9onOEV93DJAk6v33rsZzY=
|
||||
github.com/beeper/libserv v0.0.0-20231231202820-c7303abfc32c/go.mod h1:b9FFm9y4mEm36G8ytVmS1vkNzJa0KepmcdVY+qf7qRU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
|
||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
|
||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
|
||||
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/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.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/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.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
|
||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
|
||||
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
|
||||
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
|
||||
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -91,5 +95,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
|
||||
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9 h1:Xl741d8hAFdBPJPT/ydc6zTQM4R4L+5/d1X+AevLdXY=
|
||||
maunium.net/go/mautrix v0.18.1-0.20240322180408-ade00e8603f9/go.mod h1:STwJZ+6CAeiEQs7fYCkd5aC12XR5DXANE6Swy/PBKGo=
|
||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
|
|
|
|||
352
main.go
Normal file
352
main.go
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
// 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"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/configupgrade"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge"
|
||||
"maunium.net/go/mautrix/bridge/commands"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/config"
|
||||
"go.mau.fi/mautrix-signal/database"
|
||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
)
|
||||
|
||||
//go:embed example-config.yaml
|
||||
var ExampleConfig string
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
||||
type SignalBridge struct {
|
||||
bridge.Bridge
|
||||
|
||||
Config *config.Config
|
||||
DB *database.Database
|
||||
Metrics *MetricsHandler
|
||||
MeowStore *store.Container
|
||||
|
||||
provisioning *ProvisioningAPI
|
||||
|
||||
usersByMXID map[id.UserID]*User
|
||||
usersBySignalID map[uuid.UUID]*User
|
||||
usersLock sync.Mutex
|
||||
|
||||
managementRooms map[id.RoomID]*User
|
||||
managementRoomsLock sync.Mutex
|
||||
|
||||
portalsByMXID map[id.RoomID]*Portal
|
||||
portalsByID map[database.PortalKey]*Portal
|
||||
portalsLock sync.Mutex
|
||||
|
||||
puppets map[uuid.UUID]*Puppet
|
||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
||||
puppetsLock sync.Mutex
|
||||
|
||||
disappearingMessagesManager *DisappearingMessagesManager
|
||||
}
|
||||
|
||||
var _ bridge.ChildOverride = (*SignalBridge)(nil)
|
||||
|
||||
func (br *SignalBridge) GetExampleConfig() string {
|
||||
return ExampleConfig
|
||||
}
|
||||
|
||||
func (br *SignalBridge) GetConfigPtr() interface{} {
|
||||
br.Config = &config.Config{
|
||||
BaseConfig: &br.Bridge.Config,
|
||||
}
|
||||
br.Config.BaseConfig.Bridge = &br.Config.Bridge
|
||||
return br.Config
|
||||
}
|
||||
|
||||
func (br *SignalBridge) Init() {
|
||||
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
||||
br.RegisterCommands()
|
||||
|
||||
signalmeow.SetLogger(br.ZLog.With().Str("component", "signalmeow").Logger())
|
||||
|
||||
br.DB = database.New(br.Bridge.DB)
|
||||
br.MeowStore = store.NewStore(br.Bridge.DB, dbutil.ZeroLogger(br.ZLog.With().Str("db_section", "signalmeow").Logger()))
|
||||
|
||||
ss := br.Config.Bridge.Provisioning.SharedSecret
|
||||
if len(ss) > 0 && ss != "disable" {
|
||||
br.provisioning = &ProvisioningAPI{bridge: br, log: br.ZLog.With().Str("component", "provisioning").Logger()}
|
||||
}
|
||||
br.disappearingMessagesManager = &DisappearingMessagesManager{
|
||||
DB: br.DB,
|
||||
Log: br.ZLog.With().Str("component", "disappearing messages").Logger(),
|
||||
Bridge: br,
|
||||
}
|
||||
|
||||
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.ZLog.With().Str("component", "metrics").Logger(), br.DB)
|
||||
br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent
|
||||
|
||||
signalFormatParams = &signalfmt.FormatParams{
|
||||
GetUserInfo: func(ctx context.Context, u uuid.UUID) signalfmt.UserInfo {
|
||||
puppet := br.GetPuppetBySignalID(u)
|
||||
if puppet == nil {
|
||||
return signalfmt.UserInfo{}
|
||||
}
|
||||
user := br.GetUserBySignalID(u)
|
||||
if user != nil {
|
||||
return signalfmt.UserInfo{
|
||||
MXID: user.MXID,
|
||||
Name: puppet.Name,
|
||||
}
|
||||
}
|
||||
return signalfmt.UserInfo{
|
||||
MXID: puppet.MXID,
|
||||
Name: puppet.Name,
|
||||
}
|
||||
},
|
||||
}
|
||||
matrixFormatParams = &matrixfmt.HTMLParser{
|
||||
GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
|
||||
parsed, ok := br.ParsePuppetMXID(userID)
|
||||
if ok {
|
||||
return parsed
|
||||
}
|
||||
user := br.GetUserByMXIDIfExists(userID)
|
||||
if user != nil && user.SignalID != uuid.Nil {
|
||||
return user.SignalID
|
||||
}
|
||||
return uuid.Nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (br *SignalBridge) logLostPortals(ctx context.Context) {
|
||||
exists, err := br.DB.TableExists(ctx, "lost_portals")
|
||||
if err != nil {
|
||||
br.ZLog.Err(err).Msg("Failed to check if lost_portals table exists")
|
||||
} else if !exists {
|
||||
return
|
||||
}
|
||||
lostPortals, err := br.DB.LostPortal.GetAll(ctx)
|
||||
if err != nil {
|
||||
br.ZLog.Err(err).Msg("Failed to get lost portals")
|
||||
return
|
||||
} else if len(lostPortals) == 0 {
|
||||
return
|
||||
}
|
||||
lostCountByReceiver := make(map[string]int)
|
||||
for _, lost := range lostPortals {
|
||||
lostCountByReceiver[lost.Receiver]++
|
||||
}
|
||||
br.ZLog.Warn().
|
||||
Any("count_by_receiver", lostCountByReceiver).
|
||||
Msg("Some portals were discarded due to the receiver not being logged into the bridge anymore. " +
|
||||
"Use `!signal cleanup-lost-portals` to remove them from the database. " +
|
||||
"Alternatively, you can re-insert the data into the portal table with the appropriate receiver column to restore the portals.")
|
||||
}
|
||||
|
||||
func (br *SignalBridge) Start() {
|
||||
go br.logLostPortals(context.TODO())
|
||||
err := br.MeowStore.Upgrade(context.TODO())
|
||||
if err != nil {
|
||||
br.ZLog.Fatal().Err(err).Msg("Failed to upgrade signalmeow database")
|
||||
os.Exit(15)
|
||||
}
|
||||
if br.provisioning != nil {
|
||||
br.ZLog.Debug().Msg("Initializing provisioning API")
|
||||
br.provisioning.Init()
|
||||
}
|
||||
go br.StartUsers()
|
||||
if br.Config.Metrics.Enabled {
|
||||
go br.Metrics.Start()
|
||||
}
|
||||
go br.disappearingMessagesManager.StartDisappearingLoop(context.TODO())
|
||||
}
|
||||
|
||||
func (br *SignalBridge) Stop() {
|
||||
br.Metrics.Stop()
|
||||
for _, user := range br.usersByMXID {
|
||||
br.ZLog.Debug().Stringer("user_id", user.MXID).Msg("Disconnecting user")
|
||||
user.Disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func (br *SignalBridge) GetIPortal(mxid id.RoomID) bridge.Portal {
|
||||
p := br.GetPortalByMXID(mxid)
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (br *SignalBridge) GetIUser(mxid id.UserID, create bool) bridge.User {
|
||||
p := br.GetUserByMXID(mxid)
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (br *SignalBridge) IsGhost(mxid id.UserID) bool {
|
||||
_, isGhost := br.ParsePuppetMXID(mxid)
|
||||
return isGhost
|
||||
}
|
||||
|
||||
func (br *SignalBridge) GetIGhost(mxid id.UserID) bridge.Ghost {
|
||||
p := br.GetPuppetByMXID(mxid)
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (br *SignalBridge) CreatePrivatePortal(roomID id.RoomID, brInviter bridge.User, brGhost bridge.Ghost) {
|
||||
inviter := brInviter.(*User)
|
||||
puppet := brGhost.(*Puppet)
|
||||
|
||||
log := br.ZLog.With().
|
||||
Str("action", "create private portal").
|
||||
Stringer("target_room_id", roomID).
|
||||
Stringer("inviter_mxid", brInviter.GetMXID()).
|
||||
Stringer("invitee_uuid", puppet.SignalID).
|
||||
Logger()
|
||||
log.Debug().Msg("Creating private chat portal")
|
||||
|
||||
key := database.NewPortalKey(puppet.SignalID.String(), inviter.SignalID)
|
||||
portal := br.GetPortalByChatID(key)
|
||||
ctx := log.WithContext(context.TODO())
|
||||
|
||||
if len(portal.MXID) == 0 {
|
||||
br.createPrivatePortalFromInvite(ctx, roomID, inviter, puppet, portal)
|
||||
return
|
||||
}
|
||||
log.Debug().
|
||||
Stringer("existing_room_id", portal.MXID).
|
||||
Msg("Existing private chat portal found, trying to invite user")
|
||||
|
||||
ok := portal.ensureUserInvited(ctx, inviter)
|
||||
if !ok {
|
||||
log.Warn().Msg("Failed to invite user to existing private chat portal. Redirecting portal to new room")
|
||||
br.createPrivatePortalFromInvite(ctx, roomID, inviter, puppet, portal)
|
||||
return
|
||||
}
|
||||
intent := puppet.DefaultIntent()
|
||||
errorMessage := fmt.Sprintf("You already have a private chat portal with me at [%[1]s](https://matrix.to/#/%[1]s)", portal.MXID)
|
||||
errorContent := format.RenderMarkdown(errorMessage, true, false)
|
||||
_, _ = intent.SendMessageEvent(ctx, roomID, event.EventMessage, errorContent)
|
||||
log.Debug().Msg("Leaving ghost from private chat room after accepting invite because we already have a chat with the user")
|
||||
_, _ = intent.LeaveRoom(ctx, roomID)
|
||||
}
|
||||
|
||||
func (br *SignalBridge) createPrivatePortalFromInvite(ctx context.Context, roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
log.Debug().Msg("Creating private portal from invite")
|
||||
|
||||
// Check if room is already encrypted
|
||||
var existingEncryption event.EncryptionEventContent
|
||||
var encryptionEnabled bool
|
||||
err := portal.MainIntent().StateEvent(ctx, roomID, event.StateEncryption, "", &existingEncryption)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to check if encryption is enabled in private chat room")
|
||||
} else {
|
||||
encryptionEnabled = existingEncryption.Algorithm == id.AlgorithmMegolmV1
|
||||
}
|
||||
portal.MXID = roomID
|
||||
br.portalsLock.Lock()
|
||||
br.portalsByMXID[portal.MXID] = portal
|
||||
br.portalsLock.Unlock()
|
||||
intent := puppet.DefaultIntent()
|
||||
|
||||
if br.Config.Bridge.Encryption.Default || encryptionEnabled {
|
||||
log.Debug().Msg("Adding bridge bot to new private chat portal as encryption is enabled")
|
||||
_, err = intent.InviteUser(ctx, roomID, &mautrix.ReqInviteUser{UserID: br.Bot.UserID})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to invite bridge bot to enable e2be")
|
||||
}
|
||||
err = br.Bot.EnsureJoined(ctx, roomID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to join as bridge bot to enable e2be")
|
||||
}
|
||||
if !encryptionEnabled {
|
||||
_, err = intent.SendStateEvent(ctx, roomID, event.StateEncryption, "", portal.getEncryptionEventContent())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to enable e2be")
|
||||
}
|
||||
}
|
||||
br.AS.StateStore.SetMembership(ctx, roomID, inviter.MXID, event.MembershipJoin)
|
||||
br.AS.StateStore.SetMembership(ctx, roomID, puppet.MXID, event.MembershipJoin)
|
||||
br.AS.StateStore.SetMembership(ctx, roomID, br.Bot.UserID, event.MembershipJoin)
|
||||
portal.Encrypted = true
|
||||
}
|
||||
portal.UpdateDMInfo(ctx, true)
|
||||
_, _ = intent.SendNotice(ctx, roomID, "Private chat portal created")
|
||||
log.Info().Msg("Created private chat portal after invite")
|
||||
}
|
||||
|
||||
func main() {
|
||||
br := &SignalBridge{
|
||||
usersByMXID: make(map[id.UserID]*User),
|
||||
usersBySignalID: make(map[uuid.UUID]*User),
|
||||
|
||||
managementRooms: make(map[id.RoomID]*User),
|
||||
|
||||
portalsByMXID: make(map[id.RoomID]*Portal),
|
||||
portalsByID: make(map[database.PortalKey]*Portal),
|
||||
|
||||
puppets: make(map[uuid.UUID]*Puppet),
|
||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
||||
}
|
||||
br.Bridge = bridge.Bridge{
|
||||
Name: "mautrix-signal",
|
||||
URL: "https://github.com/mautrix/signal",
|
||||
Description: "A Matrix-Signal puppeting bridge.",
|
||||
Version: "0.5.1",
|
||||
ProtocolName: "Signal",
|
||||
BeeperServiceName: "signal",
|
||||
BeeperNetworkName: "signal",
|
||||
|
||||
CryptoPickleKey: "mautrix.bridge.e2ee",
|
||||
|
||||
ConfigUpgrader: &configupgrade.StructUpgrader{
|
||||
SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
|
||||
Blocks: config.SpacedBlocks,
|
||||
Base: ExampleConfig,
|
||||
},
|
||||
|
||||
Child: br,
|
||||
}
|
||||
br.InitVersion(Tag, Commit, BuildTime)
|
||||
|
||||
br.Main()
|
||||
}
|
||||
311
messagetracking.go
Normal file
311
messagetracking.go
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/msgconv"
|
||||
)
|
||||
|
||||
var (
|
||||
errUserNotConnected = errors.New("you are not connected to Signal")
|
||||
errDifferentUser = errors.New("user is not the recipient of this private chat portal")
|
||||
errUserNotLoggedIn = errors.New("user is not logged in and chat has no relay bot")
|
||||
errRelaybotNotLoggedIn = errors.New("neither user nor relay bot of chat are logged in")
|
||||
errCantRelayReactions = errors.New("user is not logged in and reactions can't be relayed")
|
||||
errMNoticeDisabled = errors.New("bridging m.notice messages is disabled")
|
||||
errUnexpectedParsedContentType = errors.New("unexpected parsed content type")
|
||||
|
||||
errRedactionTargetNotFound = errors.New("redaction target message was not found")
|
||||
errRedactionTargetSentBySomeoneElse = errors.New("redaction target message was sent by someone else")
|
||||
errUnreactTargetSentBySomeoneElse = errors.New("redaction target reaction was sent by someone else")
|
||||
errReactionTargetNotFound = errors.New("reaction target message not found")
|
||||
errEditUnknownTarget = errors.New("unknown edit target message")
|
||||
errFailedToGetEditTarget = errors.New("failed to get edit target message")
|
||||
errEditDifferentSender = errors.New("can't edit message sent by another user")
|
||||
errEditTooOld = errors.New("message is too old to be edited")
|
||||
|
||||
errMessageTakingLong = errors.New("bridging the message is taking longer than usual")
|
||||
errTimeoutBeforeHandling = errors.New("message timed out before handling was started")
|
||||
)
|
||||
|
||||
func errorToStatusReason(err error) (reason event.MessageStatusReason, status event.MessageStatus, isCertain, sendNotice bool, humanMessage string) {
|
||||
switch {
|
||||
case errors.Is(err, errUnexpectedParsedContentType),
|
||||
errors.Is(err, msgconv.ErrUnsupportedMsgType),
|
||||
errors.Is(err, msgconv.ErrInvalidGeoURI):
|
||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, ""
|
||||
case errors.Is(err, errMNoticeDisabled):
|
||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, false, ""
|
||||
case errors.Is(err, errEditDifferentSender),
|
||||
errors.Is(err, errEditTooOld),
|
||||
errors.Is(err, errEditUnknownTarget):
|
||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, err.Error()
|
||||
case errors.Is(err, errTimeoutBeforeHandling):
|
||||
return event.MessageStatusTooOld, event.MessageStatusRetriable, true, true, "the message was too old when it reached the bridge, so it was not handled"
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return event.MessageStatusTooOld, event.MessageStatusRetriable, false, true, "handling the message took too long and was cancelled"
|
||||
case errors.Is(err, errMessageTakingLong):
|
||||
return event.MessageStatusTooOld, event.MessageStatusPending, false, true, err.Error()
|
||||
case errors.Is(err, errRedactionTargetNotFound),
|
||||
errors.Is(err, errReactionTargetNotFound),
|
||||
errors.Is(err, errRedactionTargetSentBySomeoneElse),
|
||||
errors.Is(err, errUnreactTargetSentBySomeoneElse):
|
||||
return event.MessageStatusGenericError, event.MessageStatusFail, true, false, ""
|
||||
case errors.Is(err, errUserNotConnected):
|
||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, true, ""
|
||||
case errors.Is(err, errUserNotLoggedIn),
|
||||
errors.Is(err, errDifferentUser),
|
||||
errors.Is(err, errRelaybotNotLoggedIn):
|
||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, false, ""
|
||||
default:
|
||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, false, true, ""
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) sendErrorMessage(ctx context.Context, evt *event.Event, err error, confirmed bool, editID id.EventID) id.EventID {
|
||||
if !portal.bridge.Config.Bridge.MessageErrorNotices {
|
||||
return ""
|
||||
}
|
||||
certainty := "may not have been"
|
||||
if confirmed {
|
||||
certainty = "was not"
|
||||
}
|
||||
var msgType string
|
||||
switch evt.Type {
|
||||
case event.EventMessage:
|
||||
msgType = "message"
|
||||
case event.EventReaction:
|
||||
msgType = "reaction"
|
||||
case event.EventRedaction:
|
||||
msgType = "redaction"
|
||||
//case TypeMSC3381PollResponse, TypeMSC3381V2PollResponse:
|
||||
// msgType = "poll response"
|
||||
//case TypeMSC3381PollStart:
|
||||
// msgType = "poll start"
|
||||
default:
|
||||
msgType = "unknown event"
|
||||
}
|
||||
msg := fmt.Sprintf("\u26a0 Your %s %s bridged: %v", msgType, certainty, err)
|
||||
if errors.Is(err, errMessageTakingLong) {
|
||||
msg = fmt.Sprintf("\u26a0 Bridging your %s is taking longer than usual", msgType)
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: msg,
|
||||
}
|
||||
if editID != "" {
|
||||
content.SetEdit(editID)
|
||||
} else {
|
||||
content.SetReply(evt)
|
||||
}
|
||||
resp, err := portal.sendMainIntentMessage(ctx, content)
|
||||
if err != nil {
|
||||
portal.log.Err(err).Msg("Failed to send bridging error message")
|
||||
return ""
|
||||
}
|
||||
return resp.EventID
|
||||
}
|
||||
|
||||
func (portal *Portal) sendStatusEvent(ctx context.Context, evtID, lastRetry id.EventID, err error, deliveredTo *[]id.UserID) {
|
||||
if !portal.bridge.Config.Bridge.MessageStatusEvents {
|
||||
return
|
||||
}
|
||||
if lastRetry == evtID {
|
||||
lastRetry = ""
|
||||
}
|
||||
intent := portal.bridge.Bot
|
||||
if !portal.Encrypted {
|
||||
// Bridge bot isn't present in unencrypted DMs
|
||||
intent = portal.MainIntent()
|
||||
}
|
||||
content := event.BeeperMessageStatusEventContent{
|
||||
Network: portal.getBridgeInfoStateKey(),
|
||||
RelatesTo: event.RelatesTo{
|
||||
Type: event.RelReference,
|
||||
EventID: evtID,
|
||||
},
|
||||
DeliveredToUsers: deliveredTo,
|
||||
LastRetry: lastRetry,
|
||||
}
|
||||
if err == nil {
|
||||
content.Status = event.MessageStatusSuccess
|
||||
} else {
|
||||
content.Reason, content.Status, _, _, content.Message = errorToStatusReason(err)
|
||||
content.Error = err.Error()
|
||||
}
|
||||
_, err = intent.SendMessageEvent(ctx, portal.MXID, event.BeeperMessageStatus, &content)
|
||||
if err != nil {
|
||||
portal.log.Err(err).Msg("Failed to send message status event")
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) sendDeliveryReceipt(ctx context.Context, eventID id.EventID) {
|
||||
if portal.bridge.Config.Bridge.DeliveryReceipts {
|
||||
err := portal.bridge.Bot.SendReceipt(ctx, portal.MXID, eventID, event.ReceiptTypeRead, nil)
|
||||
if err != nil {
|
||||
portal.log.Debug().Err(err).Stringer("event_id", eventID).Msg("Failed to send delivery receipt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) sendMessageMetrics(ctx context.Context, evt *event.Event, err error, part string, ms *metricSender) {
|
||||
log := portal.log.With().
|
||||
Str("handling_step", part).
|
||||
Str("event_type", evt.Type.String()).
|
||||
Stringer("event_id", evt.ID).
|
||||
Stringer("sender", evt.Sender).
|
||||
Logger()
|
||||
if evt.Type == event.EventRedaction {
|
||||
log = log.With().Stringer("redacts", evt.Redacts).Logger()
|
||||
}
|
||||
ctx = log.WithContext(ctx)
|
||||
|
||||
origEvtID := evt.ID
|
||||
if retryMeta := evt.Content.AsMessage().MessageSendRetry; retryMeta != nil {
|
||||
origEvtID = retryMeta.OriginalEventID
|
||||
}
|
||||
if err != nil {
|
||||
logEvt := log.Error()
|
||||
if part == "Ignoring" {
|
||||
logEvt = log.Debug()
|
||||
}
|
||||
logEvt.Err(err).Msg("Sending message metrics for event")
|
||||
reason, statusCode, isCertain, sendNotice, _ := errorToStatusReason(err)
|
||||
checkpointStatus := status.ReasonToCheckpointStatus(reason, statusCode)
|
||||
portal.bridge.SendMessageCheckpoint(evt, status.MsgStepRemote, err, checkpointStatus, ms.getRetryNum())
|
||||
if sendNotice {
|
||||
ms.setNoticeID(portal.sendErrorMessage(ctx, evt, err, isCertain, ms.getNoticeID()))
|
||||
}
|
||||
portal.sendStatusEvent(ctx, origEvtID, evt.ID, err, nil)
|
||||
} else {
|
||||
log.Debug().Msg("Sending metrics for successfully handled Matrix event")
|
||||
portal.sendDeliveryReceipt(ctx, evt.ID)
|
||||
portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, ms.getRetryNum())
|
||||
var deliveredTo *[]id.UserID
|
||||
if portal.IsPrivateChat() {
|
||||
deliveredTo = &[]id.UserID{}
|
||||
}
|
||||
portal.sendStatusEvent(ctx, origEvtID, evt.ID, nil, deliveredTo)
|
||||
if prevNotice := ms.popNoticeID(); prevNotice != "" {
|
||||
_, _ = portal.MainIntent().RedactEvent(ctx, portal.MXID, prevNotice, mautrix.ReqRedact{
|
||||
Reason: "error resolved",
|
||||
})
|
||||
}
|
||||
}
|
||||
if ms != nil {
|
||||
log.Debug().Object("timings", ms.timings).Msg("Timings for event")
|
||||
}
|
||||
}
|
||||
|
||||
type messageTimings struct {
|
||||
initReceive time.Duration
|
||||
decrypt time.Duration
|
||||
implicitRR time.Duration
|
||||
portalQueue time.Duration
|
||||
totalReceive time.Duration
|
||||
|
||||
preproc time.Duration
|
||||
convert time.Duration
|
||||
totalSend time.Duration
|
||||
}
|
||||
|
||||
func niceRound(dur time.Duration) time.Duration {
|
||||
switch {
|
||||
case dur < time.Millisecond:
|
||||
return dur
|
||||
case dur < time.Second:
|
||||
return dur.Round(100 * time.Microsecond)
|
||||
default:
|
||||
return dur.Round(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (mt *messageTimings) MarshalZerologObject(evt *zerolog.Event) {
|
||||
evt.
|
||||
Dict("bridge", zerolog.Dict().
|
||||
Stringer("init_receive", niceRound(mt.initReceive)).
|
||||
Stringer("decrypt", niceRound(mt.decrypt)).
|
||||
Stringer("queue", niceRound(mt.portalQueue)).
|
||||
Stringer("total_hs_to_portal", niceRound(mt.totalReceive))).
|
||||
Dict("portal", zerolog.Dict().
|
||||
Stringer("implicit_rr", niceRound(mt.implicitRR)).
|
||||
Stringer("preproc", niceRound(mt.preproc)).
|
||||
Stringer("convert", niceRound(mt.convert)).
|
||||
Stringer("total_send", niceRound(mt.totalSend)))
|
||||
}
|
||||
|
||||
type metricSender struct {
|
||||
portal *Portal
|
||||
previousNotice id.EventID
|
||||
lock sync.Mutex
|
||||
completed bool
|
||||
retryNum int
|
||||
timings *messageTimings
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ms *metricSender) getRetryNum() int {
|
||||
if ms != nil {
|
||||
return ms.retryNum
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ms *metricSender) getNoticeID() id.EventID {
|
||||
if ms == nil {
|
||||
return ""
|
||||
}
|
||||
return ms.previousNotice
|
||||
}
|
||||
|
||||
func (ms *metricSender) popNoticeID() id.EventID {
|
||||
if ms == nil {
|
||||
return ""
|
||||
}
|
||||
evtID := ms.previousNotice
|
||||
ms.previousNotice = ""
|
||||
return evtID
|
||||
}
|
||||
|
||||
func (ms *metricSender) setNoticeID(evtID id.EventID) {
|
||||
if ms != nil && ms.previousNotice == "" {
|
||||
ms.previousNotice = evtID
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *metricSender) sendMessageMetrics(evt *event.Event, err error, part string, completed bool) {
|
||||
ms.lock.Lock()
|
||||
defer ms.lock.Unlock()
|
||||
if !completed && ms.completed {
|
||||
return
|
||||
}
|
||||
ms.portal.sendMessageMetrics(ms.ctx, evt, err, part, ms)
|
||||
ms.retryNum++
|
||||
ms.completed = completed
|
||||
}
|
||||
281
metrics.go
Normal file
281
metrics.go
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Element
|
||||
//
|
||||
// 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"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/database"
|
||||
)
|
||||
|
||||
type MetricsHandler struct {
|
||||
db *database.Database
|
||||
server *http.Server
|
||||
log zerolog.Logger
|
||||
|
||||
running bool
|
||||
ctx context.Context
|
||||
stopRecorder func()
|
||||
|
||||
matrixEventHandling *prometheus.HistogramVec
|
||||
signalMessageAge prometheus.Histogram
|
||||
signalMessageHandling *prometheus.HistogramVec
|
||||
countCollection prometheus.Histogram
|
||||
puppetCount prometheus.Gauge
|
||||
userCount prometheus.Gauge
|
||||
messageCount prometheus.Gauge
|
||||
portalCount *prometheus.GaugeVec
|
||||
encryptedGroupCount prometheus.Gauge
|
||||
encryptedPrivateCount prometheus.Gauge
|
||||
unencryptedGroupCount prometheus.Gauge
|
||||
unencryptedPrivateCount prometheus.Gauge
|
||||
|
||||
connected prometheus.Gauge
|
||||
connectedState map[uuid.UUID]bool
|
||||
connectedStateLock sync.Mutex
|
||||
loggedIn prometheus.Gauge
|
||||
loggedInState map[uuid.UUID]bool
|
||||
loggedInStateLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewMetricsHandler(address string, log zerolog.Logger, db *database.Database) *MetricsHandler {
|
||||
portalCount := promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "signal_portals_total",
|
||||
Help: "Number of portal rooms on Matrix",
|
||||
}, []string{"type", "encrypted"})
|
||||
return &MetricsHandler{
|
||||
db: db,
|
||||
server: &http.Server{Addr: address, Handler: promhttp.Handler()},
|
||||
log: log,
|
||||
running: false,
|
||||
|
||||
matrixEventHandling: promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "matrix_event",
|
||||
Help: "Time spent processing Matrix events",
|
||||
}, []string{"event_type"}),
|
||||
signalMessageAge: promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "remote_event_age",
|
||||
Help: "Age of messages received from Signal",
|
||||
Buckets: []float64{1, 2, 3, 5, 7.5, 10, 20, 30, 60},
|
||||
}),
|
||||
signalMessageHandling: promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "remote_event",
|
||||
Help: "Time spent processing Signal messages",
|
||||
}, []string{"message_type"}),
|
||||
countCollection: promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "signal_count_collection",
|
||||
Help: "Time spent collecting the bridge_*_total metrics",
|
||||
}),
|
||||
puppetCount: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "signal_puppets_total",
|
||||
Help: "Number of Signal users bridged into Matrix",
|
||||
}),
|
||||
userCount: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "signal_users_total",
|
||||
Help: "Number of Matrix users using the bridge",
|
||||
}),
|
||||
messageCount: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "signal_messages_total",
|
||||
Help: "Number of messages bridged",
|
||||
}),
|
||||
portalCount: portalCount,
|
||||
encryptedGroupCount: portalCount.With(prometheus.Labels{"type": "group", "encrypted": "true"}),
|
||||
encryptedPrivateCount: portalCount.With(prometheus.Labels{"type": "private", "encrypted": "true"}),
|
||||
unencryptedGroupCount: portalCount.With(prometheus.Labels{"type": "group", "encrypted": "false"}),
|
||||
unencryptedPrivateCount: portalCount.With(prometheus.Labels{"type": "private", "encrypted": "false"}),
|
||||
|
||||
loggedIn: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "bridge_logged_in",
|
||||
Help: "Bridge users logged into Signal",
|
||||
}),
|
||||
loggedInState: make(map[uuid.UUID]bool),
|
||||
connected: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "bridge_connected",
|
||||
Help: "Bridge users connected to Signal",
|
||||
}),
|
||||
connectedState: make(map[uuid.UUID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func noop() {}
|
||||
|
||||
func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
|
||||
if !mh.running {
|
||||
return noop
|
||||
}
|
||||
start := time.Now()
|
||||
return func() {
|
||||
duration := time.Since(start)
|
||||
mh.matrixEventHandling.
|
||||
With(prometheus.Labels{"event_type": eventType.Type}).
|
||||
Observe(duration.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackSignalMessage(timestamp time.Time, messageType string) func() {
|
||||
if !mh.running {
|
||||
return noop
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
return func() {
|
||||
duration := time.Since(start)
|
||||
mh.signalMessageHandling.
|
||||
With(prometheus.Labels{"message_type": messageType}).
|
||||
Observe(duration.Seconds())
|
||||
mh.signalMessageAge.Observe(time.Since(timestamp).Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackLoginState(signalID uuid.UUID, loggedIn bool) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.loggedInStateLock.Lock()
|
||||
defer mh.loggedInStateLock.Unlock()
|
||||
currentVal, ok := mh.loggedInState[signalID]
|
||||
if !ok || currentVal != loggedIn {
|
||||
mh.loggedInState[signalID] = loggedIn
|
||||
if loggedIn {
|
||||
mh.loggedIn.Inc()
|
||||
} else if ok {
|
||||
mh.loggedIn.Dec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackConnectionState(signalID uuid.UUID, connected bool) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.connectedStateLock.Lock()
|
||||
defer mh.connectedStateLock.Unlock()
|
||||
currentVal, ok := mh.connectedState[signalID]
|
||||
if !ok || currentVal != connected {
|
||||
mh.connectedState[signalID] = connected
|
||||
if connected {
|
||||
mh.connected.Inc()
|
||||
} else if ok {
|
||||
mh.connected.Dec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) updateStats() {
|
||||
start := time.Now()
|
||||
var puppetCount int
|
||||
err := mh.db.QueryRow(mh.ctx, "SELECT COUNT(*) FROM puppet").Scan(&puppetCount)
|
||||
if err != nil {
|
||||
mh.log.Warn().Err(err).Msg("Failed to scan number of puppets")
|
||||
} else {
|
||||
mh.puppetCount.Set(float64(puppetCount))
|
||||
}
|
||||
|
||||
var userCount int
|
||||
err = mh.db.QueryRow(mh.ctx, `SELECT COUNT(*) FROM "user"`).Scan(&userCount)
|
||||
if err != nil {
|
||||
mh.log.Warn().Err(err).Msg("Failed to scan number of users:")
|
||||
} else {
|
||||
mh.userCount.Set(float64(userCount))
|
||||
}
|
||||
|
||||
var messageCount int
|
||||
err = mh.db.QueryRow(mh.ctx, "SELECT COUNT(*) FROM message").Scan(&messageCount)
|
||||
if err != nil {
|
||||
mh.log.Warn().Err(err).Msg("Failed to scan number of messages")
|
||||
} else {
|
||||
mh.messageCount.Set(float64(messageCount))
|
||||
}
|
||||
|
||||
var encryptedGroupCount, encryptedPrivateCount, unencryptedGroupCount, unencryptedPrivateCount int
|
||||
// TODO Use a more precise way to check if a chat_id is a UUID.
|
||||
// It should also be compatible with both SQLite & Postgres.
|
||||
err = mh.db.QueryRow(mh.ctx, `
|
||||
SELECT
|
||||
COUNT(CASE WHEN chat_id NOT LIKE '%-%-%-%-%' AND encrypted THEN 1 END) AS encrypted_group_portals,
|
||||
COUNT(CASE WHEN chat_id LIKE '%-%-%-%-%' AND encrypted THEN 1 END) AS encrypted_private_portals,
|
||||
COUNT(CASE WHEN chat_id NOT LIKE '%-%-%-%-%' AND NOT encrypted THEN 1 END) AS unencrypted_group_portals,
|
||||
COUNT(CASE WHEN chat_id LIKE '%-%-%-%-%' AND NOT encrypted THEN 1 END) AS unencrypted_private_portals
|
||||
FROM portal WHERE mxid<>''
|
||||
`).Scan(&encryptedGroupCount, &encryptedPrivateCount, &unencryptedGroupCount, &unencryptedPrivateCount)
|
||||
if err != nil {
|
||||
mh.log.Warn().Err(err).Msg("Failed to scan number of portals")
|
||||
} else {
|
||||
mh.encryptedGroupCount.Set(float64(encryptedGroupCount))
|
||||
mh.encryptedPrivateCount.Set(float64(encryptedPrivateCount))
|
||||
mh.unencryptedGroupCount.Set(float64(unencryptedGroupCount))
|
||||
mh.unencryptedPrivateCount.Set(float64(encryptedPrivateCount))
|
||||
}
|
||||
mh.countCollection.Observe(time.Since(start).Seconds())
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) startUpdatingStats() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
evt := mh.log.Fatal().Str("stack", string(debug.Stack()))
|
||||
if err, ok := r.(error); ok {
|
||||
evt = evt.Err(err)
|
||||
} else {
|
||||
evt = evt.Any("error", r)
|
||||
}
|
||||
evt.Msg("Panic in metric updater")
|
||||
}
|
||||
}()
|
||||
ticker := time.Tick(10 * time.Second)
|
||||
for {
|
||||
mh.updateStats()
|
||||
select {
|
||||
case <-mh.ctx.Done():
|
||||
return
|
||||
case <-ticker:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) Start() {
|
||||
mh.running = true
|
||||
mh.ctx, mh.stopRecorder = context.WithCancel(context.Background())
|
||||
go mh.startUpdatingStats()
|
||||
err := mh.server.ListenAndServe()
|
||||
mh.running = false
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
mh.log.Fatal().Err(err).Msg("Error in metrics listener")
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) Stop() {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.stopRecorder()
|
||||
err := mh.server.Close()
|
||||
if err != nil {
|
||||
mh.log.Err(err).Msg("Error closing metrics listener")
|
||||
}
|
||||
}
|
||||
|
|
@ -18,67 +18,50 @@ package msgconv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exerrors"
|
||||
"go.mau.fi/util/exmime"
|
||||
"go.mau.fi/util/ffmpeg"
|
||||
"go.mau.fi/util/variationselector"
|
||||
"golang.org/x/exp/constraints"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) ToSignal(
|
||||
ctx context.Context,
|
||||
client *signalmeow.Client,
|
||||
portal *bridgev2.Portal,
|
||||
evt *event.Event,
|
||||
content *event.MessageEventContent,
|
||||
relaybotFormatted bool,
|
||||
replyTo *database.Message,
|
||||
) (*signalpb.DataMessage, error) {
|
||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||
ctx = context.WithValue(ctx, contextKeyPortal, portal)
|
||||
var (
|
||||
ErrUnsupportedMsgType = errors.New("unsupported msgtype")
|
||||
ErrMediaDownloadFailed = errors.New("failed to download media")
|
||||
ErrMediaDecryptFailed = errors.New("failed to decrypt media")
|
||||
ErrMediaConvertFailed = errors.New("failed to convert")
|
||||
ErrMediaUploadFailed = errors.New("failed to upload media")
|
||||
ErrInvalidGeoURI = errors.New("invalid `geo:` URI in message")
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) ToSignal(ctx context.Context, evt *event.Event, content *event.MessageEventContent, relaybotFormatted bool) (*signalpb.DataMessage, error) {
|
||||
if evt.Type == event.EventSticker {
|
||||
content.MsgType = event.MessageType(event.EventSticker.Type)
|
||||
}
|
||||
|
||||
// Matrix timestamps can be faked, but if the user is using their own Signal account, faking timestamps is their problem.
|
||||
ts := uint64(evt.Timestamp)
|
||||
// However, when relaying, timestamps shouldn't be trusted because anyone can send a message with any timestamp.
|
||||
if relaybotFormatted {
|
||||
ts = uint64(time.Now().UnixMilli())
|
||||
}
|
||||
dm := &signalpb.DataMessage{
|
||||
Preview: mc.convertURLPreviewToSignal(ctx, content),
|
||||
}
|
||||
if replyTo != nil {
|
||||
authorACI, messageID, err := signalid.ParseMessageID(replyTo.ID)
|
||||
if err == nil {
|
||||
dm.Quote = &signalpb.DataMessage_Quote{
|
||||
Id: proto.Uint64(messageID),
|
||||
AuthorAciBinary: authorACI[:],
|
||||
Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
|
||||
}
|
||||
if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
|
||||
dm.Quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if content.BeeperDisappearingTimer != nil {
|
||||
dm.ExpireTimer = proto.Uint32(uint32(content.BeeperDisappearingTimer.Timer.Seconds()))
|
||||
} else if portal.Disappear.Timer > 0 {
|
||||
dm.ExpireTimer = proto.Uint32(uint32(portal.Disappear.Timer.Seconds()))
|
||||
}
|
||||
if dm.ExpireTimer != nil && *dm.ExpireTimer != 0 {
|
||||
timerVersion := portal.Metadata.(*signalid.PortalMetadata).ExpirationTimerVersion
|
||||
if timerVersion > 0 {
|
||||
dm.ExpireTimerVersion = &timerVersion
|
||||
Timestamp: &ts,
|
||||
Quote: mc.GetSignalReply(ctx, content),
|
||||
Preview: mc.convertURLPreviewToSignal(ctx, evt),
|
||||
}
|
||||
if expirationTime := mc.GetData(ctx).ExpirationTime; expirationTime != 0 {
|
||||
dm.ExpireTimer = proto.Uint32(uint32(expirationTime))
|
||||
}
|
||||
if content.MsgType == event.MsgEmote && !relaybotFormatted {
|
||||
content.Body = "/me " + content.Body
|
||||
|
|
@ -110,9 +93,6 @@ func (mc *MessageConverter) ToSignal(
|
|||
return nil, fmt.Errorf("failed to convert sticker: %w", err)
|
||||
}
|
||||
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
|
||||
|
||||
dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker)
|
||||
if dm.Sticker == nil {
|
||||
var emoji *string
|
||||
// TODO check for single grapheme cluster?
|
||||
if len([]rune(content.Body)) == 1 {
|
||||
|
|
@ -124,20 +104,15 @@ func (mc *MessageConverter) ToSignal(
|
|||
PackId: make([]byte, 16),
|
||||
PackKey: make([]byte, 32),
|
||||
StickerId: proto.Uint32(0),
|
||||
|
||||
Data: att,
|
||||
Emoji: emoji,
|
||||
}
|
||||
}
|
||||
dm.Sticker.Data = att
|
||||
case event.MsgLocation:
|
||||
lat, lon, err := parseGeoURI(content.GeoURI)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Invalid geo URI")
|
||||
return nil, err
|
||||
}
|
||||
locationString := fmt.Sprintf(mc.LocationFormat, lat, lon)
|
||||
dm.Body = &locationString
|
||||
// TODO implement
|
||||
fallthrough
|
||||
default:
|
||||
return nil, fmt.Errorf("%w %s", bridgev2.ErrUnsupportedMessageType, content.MsgType)
|
||||
return nil, fmt.Errorf("%w %s", ErrUnsupportedMsgType, content.MsgType)
|
||||
}
|
||||
return dm, nil
|
||||
}
|
||||
|
|
@ -151,36 +126,44 @@ func maybeInt[T constraints.Integer](v T) *T {
|
|||
|
||||
func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event.Event, content *event.MessageEventContent) (*signalpb.AttachmentPointer, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
mxc := content.URL
|
||||
if content.File != nil {
|
||||
mxc = content.File.URL
|
||||
}
|
||||
data, err := mc.DownloadMatrixMedia(ctx, mxc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||
return nil, exerrors.NewDualError(ErrMediaDownloadFailed, err)
|
||||
}
|
||||
if content.File != nil {
|
||||
err = content.File.DecryptInPlace(data)
|
||||
if err != nil {
|
||||
return nil, exerrors.NewDualError(ErrMediaDecryptFailed, err)
|
||||
}
|
||||
}
|
||||
fileName := content.Body
|
||||
if content.FileName != "" {
|
||||
fileName = content.FileName
|
||||
}
|
||||
_, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"]
|
||||
mime := content.GetInfo().MimeType
|
||||
if mime == "" {
|
||||
mime = http.DetectContentType(data)
|
||||
}
|
||||
if content.MSC3245Voice != nil && mime != "audio/aac" && ffmpeg.Supported() {
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".aac", []string{}, []string{"-c:a", "aac"}, mime)
|
||||
if isVoice {
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".m4a", []string{}, []string{"-c:a", "aac"}, mime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mime = "audio/aac"
|
||||
fileName += ".aac"
|
||||
} else if evt.Type == event.EventSticker {
|
||||
fileName += ".m4a"
|
||||
} else if evt.Type == event.EventSticker && mime != "image/webp" && mime != "image/png" && mime != "image/apng" {
|
||||
switch mime {
|
||||
case "image/webp", "image/png", "image/apng":
|
||||
// allowed
|
||||
case "image/gif":
|
||||
if !ffmpeg.Supported() {
|
||||
if !mc.ConvertGIFToAPNG {
|
||||
return nil, fmt.Errorf("converting gif stickers is not supported")
|
||||
}
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".apng", []string{}, []string{}, mime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w (gif to apng): %w", bridgev2.ErrMediaConvertFailed, err)
|
||||
return nil, fmt.Errorf("%w gif to apng: %w", ErrMediaConvertFailed, err)
|
||||
}
|
||||
fileName += ".apng"
|
||||
mime = "image/apng"
|
||||
|
|
@ -188,17 +171,14 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event.
|
|||
return nil, fmt.Errorf("unsupported content type for sticker %s", mime)
|
||||
}
|
||||
}
|
||||
att, err := getClient(ctx).UploadAttachment(ctx, data)
|
||||
att, err := mc.GetClient(ctx).UploadAttachment(ctx, data)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to upload file")
|
||||
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
||||
return nil, exerrors.NewDualError(ErrMediaUploadFailed, err)
|
||||
}
|
||||
if content.MSC3245Voice != nil && mime == "audio/aac" {
|
||||
if isVoice {
|
||||
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE))
|
||||
}
|
||||
if content.Info.MauGIF {
|
||||
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_GIF))
|
||||
}
|
||||
att.ContentType = proto.String(mime)
|
||||
att.FileName = &fileName
|
||||
att.Height = maybeInt(uint32(content.Info.Height))
|
||||
|
|
@ -210,20 +190,3 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event.
|
|||
}
|
||||
return att, nil
|
||||
}
|
||||
|
||||
func parseGeoURI(uri string) (lat, long string, err error) {
|
||||
if !strings.HasPrefix(uri, "geo:") {
|
||||
err = fmt.Errorf("uri doesn't have geo: prefix")
|
||||
return
|
||||
}
|
||||
// Remove geo: prefix and anything after ;
|
||||
coordinates := strings.Split(strings.TrimPrefix(uri, "geo:"), ";")[0]
|
||||
splitCoordinates := strings.Split(coordinates, ",")
|
||||
if len(splitCoordinates) != 2 {
|
||||
err = fmt.Errorf("didn't find exactly two numbers separated by a comma")
|
||||
} else {
|
||||
lat = splitCoordinates[0]
|
||||
long = splitCoordinates[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
492
msgconv/from-signal.go
Normal file
492
msgconv/from-signal.go
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
// 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 msgconv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-vcard"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exfmt"
|
||||
"go.mau.fi/util/exmime"
|
||||
"go.mau.fi/util/ffmpeg"
|
||||
"golang.org/x/exp/slices"
|
||||
"maunium.net/go/mautrix/crypto/attachment"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
type ConvertedMessage struct {
|
||||
Parts []*ConvertedMessagePart
|
||||
Timestamp uint64
|
||||
DisappearIn uint32
|
||||
}
|
||||
|
||||
func (cm *ConvertedMessage) MergeCaption() {
|
||||
if len(cm.Parts) != 2 || cm.Parts[1].Content.MsgType != event.MsgText {
|
||||
return
|
||||
}
|
||||
switch cm.Parts[0].Content.MsgType {
|
||||
case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
|
||||
default:
|
||||
return
|
||||
}
|
||||
mediaContent := cm.Parts[0].Content
|
||||
textContent := cm.Parts[1].Content
|
||||
mediaContent.FileName = mediaContent.Body
|
||||
mediaContent.Body = textContent.Body
|
||||
mediaContent.Format = textContent.Format
|
||||
mediaContent.FormattedBody = textContent.FormattedBody
|
||||
cm.Parts = cm.Parts[:1]
|
||||
}
|
||||
|
||||
type ConvertedMessagePart struct {
|
||||
Type event.Type
|
||||
Content *event.MessageEventContent
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
func calculateLength(dm *signalpb.DataMessage) int {
|
||||
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
||||
return 1
|
||||
}
|
||||
if dm.Sticker != nil {
|
||||
return 1
|
||||
}
|
||||
length := len(dm.Attachments) + len(dm.Contact)
|
||||
if dm.Body != nil {
|
||||
length++
|
||||
}
|
||||
if dm.Payment != nil {
|
||||
length++
|
||||
}
|
||||
if dm.GiftBadge != nil {
|
||||
length++
|
||||
}
|
||||
if length == 0 && dm.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT) {
|
||||
length = 1
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func CanConvertSignal(dm *signalpb.DataMessage) bool {
|
||||
return calculateLength(dm) > 0
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) ToMatrix(ctx context.Context, dm *signalpb.DataMessage) *ConvertedMessage {
|
||||
cm := &ConvertedMessage{
|
||||
Timestamp: dm.GetTimestamp(),
|
||||
DisappearIn: dm.GetExpireTimer(),
|
||||
Parts: make([]*ConvertedMessagePart, 0, calculateLength(dm)),
|
||||
}
|
||||
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
||||
cm.Parts = append(cm.Parts, mc.ConvertDisappearingTimerChangeToMatrix(ctx, dm.GetExpireTimer(), true))
|
||||
// Don't disappear disappearing timer changes
|
||||
cm.DisappearIn = 0
|
||||
// Don't allow any other parts in a disappearing timer change message
|
||||
return cm
|
||||
}
|
||||
if dm.Sticker != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertStickerToMatrix(ctx, dm.Sticker))
|
||||
// Don't allow any other parts in a sticker message
|
||||
return cm
|
||||
}
|
||||
for i, att := range dm.GetAttachments() {
|
||||
cm.Parts = append(cm.Parts, mc.convertAttachmentToMatrix(ctx, i, att))
|
||||
}
|
||||
for _, contact := range dm.GetContact() {
|
||||
cm.Parts = append(cm.Parts, mc.convertContactToMatrix(ctx, contact))
|
||||
}
|
||||
if dm.Payment != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertPaymentToMatrix(ctx, dm.Payment))
|
||||
}
|
||||
if dm.GiftBadge != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertGiftBadgeToMatrix(ctx, dm.GiftBadge))
|
||||
}
|
||||
if dm.Body != nil {
|
||||
cm.Parts = append(cm.Parts, mc.convertTextToMatrix(ctx, dm))
|
||||
}
|
||||
if len(cm.Parts) == 0 && dm.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT) {
|
||||
cm.Parts = append(cm.Parts, &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "The bridge does not support this message type yet.",
|
||||
},
|
||||
})
|
||||
}
|
||||
replyTo, sender := mc.GetMatrixReply(ctx, dm.Quote)
|
||||
for _, part := range cm.Parts {
|
||||
if part.Content.Mentions == nil {
|
||||
part.Content.Mentions = &event.Mentions{}
|
||||
}
|
||||
if replyTo != "" {
|
||||
part.Content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(replyTo)
|
||||
if !slices.Contains(part.Content.Mentions.UserIDs, sender) {
|
||||
part.Content.Mentions.UserIDs = append(part.Content.Mentions.UserIDs, sender)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.Context, timer uint32, updatePortal bool) *ConvertedMessagePart {
|
||||
part := &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: fmt.Sprintf("Disappearing messages set to %s", exfmt.Duration(time.Duration(timer)*time.Second)),
|
||||
},
|
||||
}
|
||||
if timer == 0 {
|
||||
part.Content.Body = "Disappearing messages disabled"
|
||||
}
|
||||
if updatePortal {
|
||||
portal := mc.GetData(ctx)
|
||||
portal.ExpirationTime = timer
|
||||
err := portal.Update(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update portal disappearing timer in database")
|
||||
}
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertTextToMatrix(ctx context.Context, dm *signalpb.DataMessage) *ConvertedMessagePart {
|
||||
content := signalfmt.Parse(ctx, dm.GetBody(), dm.GetBodyRanges(), mc.SignalFmtParams)
|
||||
extra := map[string]any{}
|
||||
if len(dm.Preview) > 0 {
|
||||
extra["com.beeper.linkpreviews"] = mc.convertURLPreviewsToBeeper(ctx, dm.Preview)
|
||||
}
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: content,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *signalpb.DataMessage_Payment) *ConvertedMessagePart {
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "Payments are not yet supported",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
"fi.mau.signal.payment": payment,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertGiftBadgeToMatrix(_ context.Context, giftBadge *signalpb.DataMessage_GiftBadge) *ConvertedMessagePart {
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "Gift badges are not yet supported",
|
||||
},
|
||||
Extra: map[string]any{
|
||||
"fi.mau.signal.gift_badge": giftBadge,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *signalpb.DataMessage_Contact) vcard.Card {
|
||||
card := make(vcard.Card)
|
||||
card.SetValue(vcard.FieldVersion, "4.0")
|
||||
name := contact.GetName()
|
||||
if name.GetFamilyName() != "" || name.GetGivenName() != "" {
|
||||
card.SetName(&vcard.Name{
|
||||
FamilyName: name.GetFamilyName(),
|
||||
GivenName: name.GetGivenName(),
|
||||
AdditionalName: name.GetMiddleName(),
|
||||
HonorificPrefix: name.GetPrefix(),
|
||||
HonorificSuffix: name.GetSuffix(),
|
||||
})
|
||||
}
|
||||
if name.GetDisplayName() != "" {
|
||||
card.SetValue(vcard.FieldFormattedName, name.GetDisplayName())
|
||||
}
|
||||
if contact.GetOrganization() != "" {
|
||||
card.SetValue(vcard.FieldOrganization, contact.GetOrganization())
|
||||
}
|
||||
for _, addr := range contact.GetAddress() {
|
||||
field := vcard.Field{
|
||||
Value: strings.Join([]string{
|
||||
addr.GetPobox(),
|
||||
"", // extended address,
|
||||
addr.GetStreet(),
|
||||
addr.GetCity(),
|
||||
addr.GetRegion(),
|
||||
addr.GetPostcode(),
|
||||
addr.GetCountry(),
|
||||
// TODO put neighborhood somewhere?
|
||||
}, ";"),
|
||||
Params: make(vcard.Params),
|
||||
}
|
||||
if addr.GetLabel() != "" {
|
||||
field.Params.Set("LABEL", addr.GetLabel())
|
||||
}
|
||||
field.Params.Set(vcard.ParamType, strings.ToLower(addr.GetType().String()))
|
||||
card.Add(vcard.FieldAddress, &field)
|
||||
}
|
||||
for _, email := range contact.GetEmail() {
|
||||
field := vcard.Field{
|
||||
Value: email.GetValue(),
|
||||
Params: make(vcard.Params),
|
||||
}
|
||||
field.Params.Set(vcard.ParamType, strings.ToLower(email.GetType().String()))
|
||||
if email.GetLabel() != "" {
|
||||
field.Params.Set("LABEL", email.GetLabel())
|
||||
}
|
||||
card.Add(vcard.FieldEmail, &field)
|
||||
}
|
||||
for _, phone := range contact.GetNumber() {
|
||||
field := vcard.Field{
|
||||
Value: phone.GetValue(),
|
||||
Params: make(vcard.Params),
|
||||
}
|
||||
field.Params.Set(vcard.ParamType, strings.ToLower(phone.GetType().String()))
|
||||
if phone.GetLabel() != "" {
|
||||
field.Params.Set("LABEL", phone.GetLabel())
|
||||
}
|
||||
card.Add(vcard.FieldTelephone, &field)
|
||||
}
|
||||
if contact.GetAvatar().GetAvatar() != nil {
|
||||
avatarData, err := signalmeow.DownloadAttachment(ctx, contact.GetAvatar().GetAvatar())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to download contact avatar")
|
||||
} else {
|
||||
mimeType := contact.GetAvatar().GetAvatar().GetContentType()
|
||||
if mimeType == "" {
|
||||
mimeType = http.DetectContentType(avatarData)
|
||||
}
|
||||
card.SetValue(vcard.FieldPhoto, fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(avatarData)))
|
||||
}
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact *signalpb.DataMessage_Contact) *ConvertedMessagePart {
|
||||
card := mc.convertContactToVCard(ctx, contact)
|
||||
contact.Avatar = nil
|
||||
extraData := map[string]any{
|
||||
"fi.mau.signal.contact": contact,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := vcard.NewEncoder(&buf).Encode(card)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to encode vCard")
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "Failed to encode vCard",
|
||||
},
|
||||
Extra: extraData,
|
||||
}
|
||||
}
|
||||
data := buf.Bytes()
|
||||
var file *event.EncryptedFileInfo
|
||||
uploadMime := "text/vcard"
|
||||
uploadFileName := "contact.vcf"
|
||||
if mc.GetData(ctx).Encrypted {
|
||||
file = &event.EncryptedFileInfo{
|
||||
EncryptedFile: *attachment.NewEncryptedFile(),
|
||||
URL: "",
|
||||
}
|
||||
file.EncryptInPlace(data)
|
||||
uploadMime = "application/octet-stream"
|
||||
uploadFileName = ""
|
||||
}
|
||||
mxc, err := mc.UploadMatrixMedia(ctx, data, uploadFileName, uploadMime)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to upload vCard")
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "Failed to upload vCard",
|
||||
},
|
||||
Extra: extraData,
|
||||
}
|
||||
}
|
||||
displayName := contact.GetName().GetDisplayName()
|
||||
if displayName == "" {
|
||||
displayName = contact.GetName().GetGivenName()
|
||||
if contact.GetName().GetFamilyName() != "" {
|
||||
if displayName != "" {
|
||||
displayName += " "
|
||||
}
|
||||
displayName += contact.GetName().GetFamilyName()
|
||||
}
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = "contact"
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgFile,
|
||||
Body: displayName + ".vcf",
|
||||
Info: &event.FileInfo{
|
||||
MimeType: "text/vcf",
|
||||
Size: len(data),
|
||||
},
|
||||
}
|
||||
if file != nil {
|
||||
file.URL = mxc
|
||||
content.File = file
|
||||
} else {
|
||||
content.URL = mxc
|
||||
}
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: content,
|
||||
Extra: extraData,
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index int, att *signalpb.AttachmentPointer) *ConvertedMessagePart {
|
||||
part, err := mc.reuploadAttachment(ctx, att)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Int("attachment_index", index).Msg("Failed to handle attachment")
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: fmt.Sprintf("Failed to handle attachment %s: %v", att.GetFileName(), err),
|
||||
},
|
||||
}
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker) *ConvertedMessagePart {
|
||||
converted, err := mc.reuploadAttachment(ctx, sticker.GetData())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker")
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: fmt.Sprintf("Failed to handle sticker: %v", err),
|
||||
},
|
||||
}
|
||||
}
|
||||
// Signal stickers are 512x512, so tell Matrix clients to render them as 256x256
|
||||
if converted.Content.Info.Width == 512 && converted.Content.Info.Height == 512 {
|
||||
converted.Content.Info.Width = 256
|
||||
converted.Content.Info.Height = 256
|
||||
}
|
||||
converted.Content.Body = sticker.GetEmoji()
|
||||
converted.Type = event.EventSticker
|
||||
converted.Content.MsgType = ""
|
||||
// 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
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer) (*ConvertedMessagePart, error) {
|
||||
data, err := signalmeow.DownloadAttachment(ctx, att)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download attachment: %w", err)
|
||||
}
|
||||
mimeType := att.GetContentType()
|
||||
if mimeType == "" {
|
||||
mimeType = http.DetectContentType(data)
|
||||
}
|
||||
fileName := att.GetFileName()
|
||||
extra := map[string]any{}
|
||||
if mc.ConvertVoiceMessages && att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 {
|
||||
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"
|
||||
extra["org.matrix.msc3245.voice"] = map[string]any{}
|
||||
extra["org.matrix.msc1767.audio"] = map[string]any{}
|
||||
}
|
||||
var file *event.EncryptedFileInfo
|
||||
uploadMime := mimeType
|
||||
uploadFileName := fileName
|
||||
if mc.GetData(ctx).Encrypted {
|
||||
file = &event.EncryptedFileInfo{
|
||||
EncryptedFile: *attachment.NewEncryptedFile(),
|
||||
URL: "",
|
||||
}
|
||||
file.EncryptInPlace(data)
|
||||
uploadMime = "application/octet-stream"
|
||||
uploadFileName = ""
|
||||
}
|
||||
mxc, err := mc.UploadMatrixMedia(ctx, data, uploadFileName, uploadMime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := &event.MessageEventContent{
|
||||
Body: fileName,
|
||||
Info: &event.FileInfo{
|
||||
MimeType: mimeType,
|
||||
Width: int(att.GetWidth()),
|
||||
Height: int(att.GetHeight()),
|
||||
Size: len(data),
|
||||
},
|
||||
}
|
||||
if att.GetBlurHash() != "" {
|
||||
content.Info.Blurhash = att.GetBlurHash()
|
||||
content.Info.AnoaBlurhash = att.GetBlurHash()
|
||||
}
|
||||
switch strings.Split(mimeType, "/")[0] {
|
||||
case "image":
|
||||
content.MsgType = event.MsgImage
|
||||
case "video":
|
||||
content.MsgType = event.MsgVideo
|
||||
case "audio":
|
||||
content.MsgType = event.MsgAudio
|
||||
default:
|
||||
content.MsgType = event.MsgFile
|
||||
}
|
||||
if content.Body == "" {
|
||||
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
|
||||
}
|
||||
if file != nil {
|
||||
file.URL = mxc
|
||||
content.File = file
|
||||
} else {
|
||||
content.URL = mxc
|
||||
}
|
||||
return &ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: content,
|
||||
Extra: extra,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import (
|
|||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
)
|
||||
|
||||
var formatParams = &matrixfmt.HTMLParser{
|
||||
|
|
@ -65,13 +65,6 @@ func TestParse_HTML(t *testing.T) {
|
|||
Length: 5,
|
||||
Value: signalfmt.StyleBold,
|
||||
}}},
|
||||
{name: "UnnecessaryWhitespace", in: "<strong> Hello </strong>, World!", out: "Hello, World!", ent: signalfmt.BodyRangeList{{
|
||||
Start: 0,
|
||||
Length: 5,
|
||||
Value: signalfmt.StyleBold,
|
||||
}}},
|
||||
{name: "UnnecessaryWhitespaceParagraph", in: "<p> Hello </p>", out: "Hello"},
|
||||
{name: "EmptyParagraph", in: "<p>Hello</p><p> </p>", out: "Hello"},
|
||||
{
|
||||
name: "MultiBasic",
|
||||
in: "<strong><em>Hell</em>o</strong>, <del>Wo<span data-mx-spoiler>rld</span></del><code>!</code>",
|
||||
|
|
@ -4,16 +4,16 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/html"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
)
|
||||
|
||||
type EntityString struct {
|
||||
|
|
@ -81,26 +81,22 @@ func (es *EntityString) TrimSpace() *EntityString {
|
|||
return nil
|
||||
}
|
||||
DebugLog("TRIMSPACE %q %+v\n", es.String, es.Entities)
|
||||
cutStart := 0
|
||||
for ; cutStart < len(es.String); cutStart++ {
|
||||
var cutEnd, cutStart int
|
||||
for cutStart = 0; cutStart < len(es.String); cutStart++ {
|
||||
switch es.String[cutStart] {
|
||||
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
cutEnd := len(es.String)
|
||||
for ; cutEnd > cutStart; cutEnd-- {
|
||||
switch es.String[cutEnd-1] {
|
||||
for cutEnd = len(es.String) - 1; cutEnd >= 0; cutEnd-- {
|
||||
switch es.String[cutEnd] {
|
||||
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if cutEnd == cutStart {
|
||||
DebugLog(" -> EMPTY\n")
|
||||
return NewEntityString("")
|
||||
}
|
||||
cutEnd++
|
||||
if cutStart == 0 && cutEnd == len(es.String) {
|
||||
DebugLog(" -> NOOP\n")
|
||||
return es
|
||||
61
msgconv/msgconv.go
Normal file
61
msgconv/msgconv.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// 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 msgconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/database"
|
||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
type PortalMethods interface {
|
||||
UploadMatrixMedia(ctx context.Context, data []byte, fileName, contentType string) (id.ContentURIString, error)
|
||||
DownloadMatrixMedia(ctx context.Context, uri id.ContentURIString) ([]byte, error)
|
||||
GetMatrixReply(ctx context.Context, msg *signalpb.DataMessage_Quote) (replyTo id.EventID, replyTargetSender id.UserID)
|
||||
GetSignalReply(ctx context.Context, content *event.MessageEventContent) *signalpb.DataMessage_Quote
|
||||
|
||||
GetClient(ctx context.Context) *signalmeow.Client
|
||||
|
||||
GetData(ctx context.Context) *database.Portal
|
||||
}
|
||||
|
||||
type ExtendedPortalMethods interface {
|
||||
QueueFileTransfer(ctx context.Context, msgTS uint64, fileName string, ap *signalpb.AttachmentPointer) (id.ContentURIString, error)
|
||||
}
|
||||
|
||||
type MessageConverter struct {
|
||||
PortalMethods
|
||||
|
||||
SignalFmtParams *signalfmt.FormatParams
|
||||
MatrixFmtParams *matrixfmt.HTMLParser
|
||||
|
||||
ConvertVoiceMessages bool
|
||||
ConvertGIFToAPNG bool
|
||||
MaxFileSize int64
|
||||
AsyncFiles bool
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) IsPrivateChat(ctx context.Context) bool {
|
||||
return !mc.GetData(ctx).UserID().IsEmpty()
|
||||
}
|
||||
|
|
@ -19,12 +19,11 @@ package signalfmt
|
|||
import (
|
||||
"context"
|
||||
"html"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
|
|
@ -86,27 +85,15 @@ func Parse(ctx context.Context, message string, ranges []*signalpb.BodyRange, pa
|
|||
Start: int(*r.Start),
|
||||
Length: int(*r.Length),
|
||||
}.TruncateEnd(maxLength)
|
||||
var mentionACI uuid.UUID
|
||||
switch rv := r.GetAssociatedValue().(type) {
|
||||
case *signalpb.BodyRange_Style_:
|
||||
br.Value = Style(rv.Style)
|
||||
case *signalpb.BodyRange_MentionAci:
|
||||
var err error
|
||||
mentionACI, err = uuid.Parse(rv.MentionAci)
|
||||
parsed, err := uuid.Parse(rv.MentionAci)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
case *signalpb.BodyRange_MentionAciBinary:
|
||||
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)
|
||||
userInfo := params.GetUserInfo(ctx, parsed)
|
||||
if userInfo.MXID == "" {
|
||||
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,
|
||||
// or just replace the plaintext body by parsing the generated HTML.
|
||||
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)
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
|
|
@ -40,8 +40,8 @@ func (m Mention) String() string {
|
|||
}
|
||||
|
||||
func (m Mention) Proto() signalpb.BodyRangeAssociatedValue {
|
||||
return &signalpb.BodyRange_MentionAciBinary{
|
||||
MentionAciBinary: m.UUID[:],
|
||||
return &signalpb.BodyRange_MentionAci{
|
||||
MentionAci: m.UUID.String(),
|
||||
}
|
||||
}
|
||||
|
||||
141
msgconv/urlpreview.go
Normal file
141
msgconv/urlpreview.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// 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 msgconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/tidwall/gjson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
type BeeperLinkPreview struct {
|
||||
mautrix.RespPreviewURL
|
||||
MatchedURL string `json:"matched_url"`
|
||||
ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertURLPreviewsToBeeper(ctx context.Context, preview []*signalpb.Preview) []*BeeperLinkPreview {
|
||||
output := make([]*BeeperLinkPreview, len(preview))
|
||||
for i, p := range preview {
|
||||
output[i] = mc.convertURLPreviewToBeeper(ctx, p)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, preview *signalpb.Preview) *BeeperLinkPreview {
|
||||
output := &BeeperLinkPreview{
|
||||
MatchedURL: preview.GetUrl(),
|
||||
RespPreviewURL: mautrix.RespPreviewURL{
|
||||
CanonicalURL: preview.GetUrl(),
|
||||
Title: preview.GetTitle(),
|
||||
Description: preview.GetDescription(),
|
||||
},
|
||||
}
|
||||
if preview.Image != nil {
|
||||
msg, err := mc.reuploadAttachment(ctx, preview.Image)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to reupload link preview image")
|
||||
} else {
|
||||
output.ImageURL = msg.Content.URL
|
||||
output.ImageEncryption = msg.Content.File
|
||||
output.ImageType = msg.Content.Info.MimeType
|
||||
output.ImageSize = msg.Content.Info.Size
|
||||
output.ImageHeight = msg.Content.Info.Height
|
||||
output.ImageWidth = msg.Content.Info.Width
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
var URLRegex = regexp.MustCompile(`https?://[^\s/_*]+(?:/\S*)?`)
|
||||
|
||||
func (mc *MessageConverter) convertURLPreviewToSignal(ctx context.Context, evt *event.Event) []*signalpb.Preview {
|
||||
var previews []*BeeperLinkPreview
|
||||
|
||||
log := zerolog.Ctx(ctx)
|
||||
rawPreview := gjson.GetBytes(evt.Content.VeryRaw, `com\.beeper\.linkpreviews`)
|
||||
if rawPreview.Exists() && rawPreview.IsArray() {
|
||||
if err := json.Unmarshal([]byte(rawPreview.Raw), &previews); err != nil || len(previews) == 0 {
|
||||
return nil
|
||||
}
|
||||
} /* else if portal.bridge.Config.Bridge.URLPreviews {
|
||||
if matchedURL := URLRegex.FindString(evt.Content.AsMessage().Body); len(matchedURL) == 0 {
|
||||
return nil
|
||||
} else if parsed, err := url.Parse(matchedURL); err != nil {
|
||||
return nil
|
||||
} else if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
|
||||
return nil
|
||||
} else if mxPreview, err := portal.MainIntent().GetURLPreview(parsed.String()); err != nil {
|
||||
log.Err(err).Str("matched_url", matchedURL).Msg("Failed to fetch preview for URL found in message")
|
||||
return nil
|
||||
} else {
|
||||
previews = []*BeeperLinkPreview{{
|
||||
RespPreviewURL: *mxPreview,
|
||||
MatchedURL: matchedURL,
|
||||
}}
|
||||
}
|
||||
}*/
|
||||
if len(previews) == 0 {
|
||||
return nil
|
||||
}
|
||||
output := make([]*signalpb.Preview, len(previews))
|
||||
for i, preview := range previews {
|
||||
output[i] = &signalpb.Preview{
|
||||
Url: proto.String(preview.MatchedURL),
|
||||
Title: proto.String(preview.Title),
|
||||
Description: proto.String(preview.Description),
|
||||
Date: proto.Uint64(uint64(time.Now().UnixMilli())),
|
||||
}
|
||||
imageMXC := preview.ImageURL
|
||||
if preview.ImageEncryption != nil {
|
||||
imageMXC = preview.ImageEncryption.URL
|
||||
}
|
||||
if imageMXC != "" {
|
||||
data, err := mc.DownloadMatrixMedia(ctx, imageMXC)
|
||||
if err != nil {
|
||||
log.Err(err).Int("preview_index", i).Msg("Failed to download URL preview image")
|
||||
continue
|
||||
}
|
||||
if preview.ImageEncryption != nil {
|
||||
err = preview.ImageEncryption.DecryptInPlace(data)
|
||||
if err != nil {
|
||||
log.Err(err).Int("preview_index", i).Msg("Failed to decrypt URL preview image")
|
||||
continue
|
||||
}
|
||||
}
|
||||
uploaded, err := mc.GetClient(ctx).UploadAttachment(ctx, data)
|
||||
if err != nil {
|
||||
log.Err(err).Int("preview_index", i).Msg("Failed to reupload URL preview image")
|
||||
continue
|
||||
}
|
||||
uploaded.ContentType = proto.String(preview.ImageType)
|
||||
uploaded.Width = proto.Uint32(uint32(preview.ImageWidth))
|
||||
uploaded.Height = proto.Uint32(uint32(preview.ImageHeight))
|
||||
output[i].Image = uploaded
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
|
@ -1,207 +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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/msgconv"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
)
|
||||
|
||||
var _ bridgev2.BackfillingNetworkAPI = (*SignalClient)(nil)
|
||||
|
||||
func tryCastUUID(b []byte) uuid.UUID {
|
||||
if len(b) == 16 {
|
||||
return uuid.UUID(b)
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) FetchMessages(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if !s.IsLoggedIn() {
|
||||
return nil, bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
userID, groupID, err := signalid.ParsePortalID(params.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
}
|
||||
var chat *store.BackupChat
|
||||
if groupID != "" {
|
||||
chat, err = s.Client.Store.BackupStore.GetBackupChatByGroupID(ctx, groupID)
|
||||
} else {
|
||||
chat, err = s.Client.Store.BackupStore.GetBackupChatByUserID(ctx, userID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chat: %w", err)
|
||||
} else if chat == nil {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Chat not found, returning nil response for backfill")
|
||||
return nil, nil
|
||||
}
|
||||
var anchorTS time.Time
|
||||
if params.AnchorMessage != nil {
|
||||
anchorTS = params.AnchorMessage.Timestamp
|
||||
}
|
||||
minTS := anchorTS
|
||||
items, err := s.Client.Store.BackupStore.GetBackupChatItems(ctx, chat.Id, anchorTS, params.Forward, params.Count)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chat items: %w", err)
|
||||
}
|
||||
if len(items) > 0 {
|
||||
minTS = time.UnixMilli(int64(items[0].DateSent))
|
||||
}
|
||||
// GetBackupChatItems returns in reverse chronological order, so flip the list
|
||||
slices.Reverse(items)
|
||||
var firstDirectionfulProcessed bool
|
||||
var isRead bool
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(items))
|
||||
attMap := make(msgconv.AttachmentMap)
|
||||
recipientMap := make(map[uint64]*backuppb.Recipient)
|
||||
getRecipientACI := func(id uint64) (uuid.UUID, error) {
|
||||
recipient, ok := recipientMap[id]
|
||||
if !ok {
|
||||
recipient, err = s.Client.Store.BackupStore.GetBackupRecipient(ctx, id)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("failed to get recipient %d: %w", id, err)
|
||||
} else if len(recipient.GetContact().GetAci()) != 16 && recipient.GetSelf() == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Uint64("recipient_id", id).
|
||||
Type("recipient_type", recipient.GetDestination()).
|
||||
Msg("ACI not found for recipient")
|
||||
}
|
||||
recipientMap[id] = recipient
|
||||
}
|
||||
|
||||
switch dest := recipient.Destination.(type) {
|
||||
case *backuppb.Recipient_Self:
|
||||
return s.Client.Store.ACI, nil
|
||||
case *backuppb.Recipient_Contact:
|
||||
if len(dest.Contact.GetAci()) == 16 {
|
||||
return uuid.UUID(dest.Contact.GetAci()), nil
|
||||
}
|
||||
}
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
var prevStreamOrder int64
|
||||
findNextStreamOrder := func(i int) int64 {
|
||||
for ; i < len(items); i++ {
|
||||
inc, ok := items[i].DirectionalDetails.(*backuppb.ChatItem_Incoming)
|
||||
if ok {
|
||||
return int64(inc.Incoming.GetDateServerSent())
|
||||
}
|
||||
}
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
for i, item := range items {
|
||||
var streamOrder int64
|
||||
switch dt := item.DirectionalDetails.(type) {
|
||||
case *backuppb.ChatItem_Incoming:
|
||||
streamOrder = int64(dt.Incoming.GetDateServerSent())
|
||||
prevStreamOrder = streamOrder
|
||||
if !firstDirectionfulProcessed {
|
||||
firstDirectionfulProcessed = true
|
||||
isRead = dt.Incoming.Read
|
||||
}
|
||||
case *backuppb.ChatItem_Outgoing:
|
||||
streamOrder = int64(item.GetDateSent())
|
||||
// Ensure stream order is higher than previous incoming item, but lower than next incoming item
|
||||
streamOrder = min(streamOrder, findNextStreamOrder(i+1)-1)
|
||||
streamOrder = max(streamOrder, prevStreamOrder+1)
|
||||
|
||||
if !firstDirectionfulProcessed {
|
||||
firstDirectionfulProcessed = true
|
||||
isRead = true
|
||||
}
|
||||
}
|
||||
if len(attMap) > 0 {
|
||||
clear(attMap)
|
||||
}
|
||||
senderACI, err := getRecipientACI(item.AuthorId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if senderACI == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
dm, reactions := msgconv.BackupToDataMessage(item, attMap)
|
||||
if dm == nil {
|
||||
continue
|
||||
}
|
||||
cm := s.Main.MsgConv.ToMatrix(ctx, s.Client, params.Portal, senderACI, s.Main.Bridge.Bot, dm, attMap)
|
||||
convertedReactions := make([]*bridgev2.BackfillReaction, 0, len(reactions))
|
||||
for _, reaction := range reactions {
|
||||
reactionSenderACI, err := getRecipientACI(reaction.AuthorId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if reactionSenderACI == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
convertedReactions = append(convertedReactions, &bridgev2.BackfillReaction{
|
||||
TargetPart: ptr.Ptr(networkid.PartID("")),
|
||||
Timestamp: time.UnixMilli(int64(reaction.SentTimestamp)),
|
||||
Sender: s.makeEventSender(reactionSenderACI),
|
||||
Emoji: reaction.GetEmoji(),
|
||||
})
|
||||
}
|
||||
msgID := signalid.MakeMessageID(senderACI, item.DateSent)
|
||||
convertedMessages = append(convertedMessages, &bridgev2.BackfillMessage{
|
||||
ConvertedMessage: cm,
|
||||
Sender: s.makeEventSender(senderACI),
|
||||
ID: msgID,
|
||||
TxnID: networkid.TransactionID(msgID),
|
||||
Timestamp: time.UnixMilli(int64(item.DateSent)),
|
||||
StreamOrder: streamOrder,
|
||||
Reactions: convertedReactions,
|
||||
})
|
||||
}
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
Messages: convertedMessages,
|
||||
HasMore: len(items) >= params.Count,
|
||||
Forward: params.Forward,
|
||||
MarkRead: isRead,
|
||||
ApproxTotalCount: chat.TotalMessages,
|
||||
CompleteCallback: func() {
|
||||
// When reaching the last backwards backfill batch, delete the chat from the backup store.
|
||||
// If backwards backfilling isn't enabled, delete immediately after the first backfill request.
|
||||
if (!params.Forward && len(items) < params.Count) || !s.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
||||
err := s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Deleted chat from backup store as backfill seems finished")
|
||||
}
|
||||
} else {
|
||||
err := s.Client.Store.BackupStore.DeleteBackupChatItems(ctx, chat.Id, minTS)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Time("min_ts", minTS).Msg("Failed to delete messages from backup store")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().Time("min_ts", minTS).Msg("Deleted messages from backup store")
|
||||
}
|
||||
}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,241 +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 (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/util/ffmpeg"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
||||
if ffmpeg.Supported() {
|
||||
return event.CapLevelPartialSupport
|
||||
}
|
||||
return event.CapLevelRejected
|
||||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.signal.capabilities.2026_05_12"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
const MaxFileSize = 100 * 1024 * 1024
|
||||
const MaxTextLength = 2000
|
||||
|
||||
var signalCaps = &event.RoomFeatures{
|
||||
ID: capID(),
|
||||
|
||||
Formatting: map[event.FormattingFeature]event.CapabilitySupportLevel{
|
||||
// Features that Signal supports natively
|
||||
event.FmtBold: event.CapLevelFullySupported,
|
||||
event.FmtItalic: event.CapLevelFullySupported,
|
||||
event.FmtStrikethrough: event.CapLevelFullySupported,
|
||||
event.FmtSpoiler: event.CapLevelFullySupported,
|
||||
event.FmtInlineCode: event.CapLevelFullySupported,
|
||||
event.FmtCodeBlock: event.CapLevelFullySupported,
|
||||
event.FmtUserLink: event.CapLevelFullySupported,
|
||||
|
||||
// Features that aren't supported on Signal, but are converted into a markdown-like representation
|
||||
event.FmtBlockquote: event.CapLevelPartialSupport,
|
||||
event.FmtInlineLink: event.CapLevelPartialSupport,
|
||||
event.FmtUnorderedList: event.CapLevelPartialSupport,
|
||||
event.FmtOrderedList: event.CapLevelPartialSupport,
|
||||
event.FmtListStart: event.CapLevelPartialSupport,
|
||||
event.FmtHeaders: event.CapLevelPartialSupport,
|
||||
},
|
||||
File: map[event.CapabilityMsgType]*event.FileFeatures{
|
||||
event.MsgImage: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"image/gif": event.CapLevelFullySupported,
|
||||
"image/png": event.CapLevelFullySupported,
|
||||
"image/jpeg": event.CapLevelFullySupported,
|
||||
"image/webp": event.CapLevelFullySupported,
|
||||
"image/bmp": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxWidth: 4096,
|
||||
MaxHeight: 4096,
|
||||
MaxSize: MaxFileSize,
|
||||
Caption: event.CapLevelFullySupported,
|
||||
MaxCaptionLength: MaxTextLength,
|
||||
},
|
||||
event.MsgVideo: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"video/mp4": event.CapLevelFullySupported,
|
||||
"video/ogg": event.CapLevelFullySupported,
|
||||
"video/webm": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxSize: MaxFileSize,
|
||||
Caption: event.CapLevelFullySupported,
|
||||
MaxCaptionLength: MaxTextLength,
|
||||
},
|
||||
event.MsgAudio: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"audio/aac": event.CapLevelFullySupported,
|
||||
"audio/mpeg": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxSize: MaxFileSize,
|
||||
},
|
||||
event.MsgFile: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"*/*": event.CapLevelFullySupported,
|
||||
},
|
||||
MaxSize: MaxFileSize,
|
||||
Caption: event.CapLevelFullySupported,
|
||||
MaxCaptionLength: MaxTextLength,
|
||||
},
|
||||
event.CapMsgSticker: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
// Signal clients will only render static webp, so apng is preferred
|
||||
"image/webp": event.CapLevelPartialSupport,
|
||||
"image/png": event.CapLevelFullySupported,
|
||||
"image/apng": event.CapLevelFullySupported,
|
||||
"image/gif": supportedIfFFmpeg(),
|
||||
},
|
||||
Caption: event.CapLevelDropped,
|
||||
MaxSize: MaxFileSize,
|
||||
},
|
||||
event.CapMsgVoice: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"audio/aac": event.CapLevelFullySupported,
|
||||
"audio/ogg": supportedIfFFmpeg(),
|
||||
},
|
||||
Caption: event.CapLevelDropped,
|
||||
MaxSize: MaxFileSize,
|
||||
MaxDuration: ptr.Ptr(jsontime.S(1 * time.Hour)),
|
||||
},
|
||||
event.CapMsgGIF: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"image/gif": event.CapLevelFullySupported,
|
||||
"video/mp4": event.CapLevelFullySupported,
|
||||
},
|
||||
Caption: event.CapLevelFullySupported,
|
||||
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
|
||||
LocationMessage: event.CapLevelPartialSupport,
|
||||
Poll: event.CapLevelRejected,
|
||||
Thread: event.CapLevelUnsupported,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
|
||||
Delete: event.CapLevelFullySupported,
|
||||
DeleteForMe: false,
|
||||
DeleteMaxAge: ptr.Ptr(jsontime.S(24 * time.Hour)),
|
||||
DisappearingTimer: signalDisappearingCap,
|
||||
|
||||
Reaction: event.CapLevelFullySupported,
|
||||
ReactionCount: 1,
|
||||
AllowedReactions: nil,
|
||||
CustomEmojiReactions: false,
|
||||
ReadReceipts: true,
|
||||
TypingNotifications: true,
|
||||
|
||||
DeleteChat: true,
|
||||
MessageRequest: &event.MessageRequestFeatures{
|
||||
AcceptWithMessage: event.CapLevelPartialSupport,
|
||||
AcceptWithButton: event.CapLevelFullySupported,
|
||||
},
|
||||
}
|
||||
|
||||
var signalDisappearingCap = &event.DisappearingTimerCapability{
|
||||
Types: []event.DisappearingType{event.DisappearingTypeAfterRead},
|
||||
}
|
||||
|
||||
var signalCapsNoteToSelf *event.RoomFeatures
|
||||
var signalCapsDM *event.RoomFeatures
|
||||
|
||||
func init() {
|
||||
signalCapsDM = 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.DeleteMaxAge = nil
|
||||
signalCapsNoteToSelf.ID = capID() + "+note_to_self"
|
||||
}
|
||||
|
||||
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) {
|
||||
return signalCapsNoteToSelf
|
||||
} else if portal.RoomType == database.RoomTypeDM {
|
||||
return signalCapsDM
|
||||
}
|
||||
return signalCaps
|
||||
}
|
||||
|
||||
var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
||||
DisappearingMessages: true,
|
||||
AggressiveUpdateInfo: true,
|
||||
ImplicitReadReceipts: true,
|
||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||
ImagePackImport: true,
|
||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||
CreateDM: true,
|
||||
LookupPhone: true,
|
||||
LookupUsername: false, // TODO implement
|
||||
ContactList: true,
|
||||
},
|
||||
GroupCreation: map[string]bridgev2.GroupTypeCapabilities{
|
||||
"group": {
|
||||
TypeDescription: "a group chat",
|
||||
|
||||
Name: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MaxLength: 32},
|
||||
Avatar: bridgev2.GroupFieldCapability{Allowed: true},
|
||||
Disappear: bridgev2.GroupFieldCapability{Allowed: true, DisappearSettings: signalDisappearingCap},
|
||||
Participants: bridgev2.GroupFieldCapability{Allowed: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
|
||||
return signalGeneralCaps
|
||||
}
|
||||
|
||||
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
|
||||
return 1, 8
|
||||
}
|
||||
|
|
@ -1,495 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ bridgev2.IdentifierResolvingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.GroupCreatingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.ContactListingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.GhostDMCreatingNetworkAPI = (*SignalClient)(nil)
|
||||
)
|
||||
|
||||
var _ bridgev2.IdentifierValidatingNetwork = (*SignalConnector)(nil)
|
||||
|
||||
const PrivateChatTopic = "Signal private chat"
|
||||
const NoteToSelfName = "Signal Note to Self"
|
||||
|
||||
func (s *SignalClient) GetUserInfoWithRefreshAfter(ctx context.Context, ghost *bridgev2.Ghost, refreshAfter time.Duration) (*bridgev2.UserInfo, error) {
|
||||
userID, err := signalid.ParseUserIDAsServiceID(ghost.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ghost.Name != "" && s.Main.Bridge.Background {
|
||||
// Don't do unnecessary fetches in background mode
|
||||
return nil, nil
|
||||
}
|
||||
var contact *types.Recipient
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
meta := ghost.Metadata.(*signalid.GhostMetadata)
|
||||
if userID.Type != libsignalgo.ServiceIDTypePNI && (!s.Main.Config.UseOutdatedProfiles && meta.ProfileFetchedAt.After(contact.Profile.FetchedAt)) {
|
||||
return nil, nil
|
||||
}
|
||||
return s.contactToUserInfo(ctx, contact)
|
||||
}
|
||||
|
||||
func (s *SignalClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
|
||||
return s.GetUserInfoWithRefreshAfter(ctx, ghost, signalmeow.DefaultProfileRefreshAfter)
|
||||
}
|
||||
|
||||
func (s *SignalClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
|
||||
userID, groupID, err := signalid.ParsePortalID(portal.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse portal id: %w", err)
|
||||
}
|
||||
if groupID != "" {
|
||||
return s.getGroupInfo(ctx, groupID, 0, nil)
|
||||
} else {
|
||||
aci, pni := userID.ToACIAndPNI()
|
||||
contact, err := s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.makeCreateDMResponse(ctx, contact, nil).PortalInfo, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) contactToUserInfo(ctx context.Context, contact *types.Recipient) (*bridgev2.UserInfo, error) {
|
||||
isBot := false
|
||||
ui := &bridgev2.UserInfo{
|
||||
IsBot: &isBot,
|
||||
Identifiers: []string{},
|
||||
ExtraUpdates: func(ctx context.Context, ghost *bridgev2.Ghost) (changed bool) {
|
||||
meta := ghost.Metadata.(*signalid.GhostMetadata)
|
||||
if meta.ProfileFetchedAt.Before(contact.Profile.FetchedAt) {
|
||||
changed = meta.ProfileFetchedAt.IsZero() && !contact.Profile.FetchedAt.IsZero()
|
||||
meta.ProfileFetchedAt.Time = contact.Profile.FetchedAt
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
if contact.E164 != "" {
|
||||
ui.Identifiers = append(ui.Identifiers, "tel:"+contact.E164)
|
||||
}
|
||||
name := s.Main.Config.FormatDisplayname(contact)
|
||||
ui.Name = &name
|
||||
if s.Main.Config.UseContactAvatars && contact.ContactAvatar.Hash != "" {
|
||||
ui.Avatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID("hash:" + contact.ContactAvatar.Hash),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
if contact.ContactAvatar.Image == nil {
|
||||
return nil, fmt.Errorf("contact avatar not available")
|
||||
}
|
||||
return contact.ContactAvatar.Image, nil
|
||||
},
|
||||
}
|
||||
} else if contact.Profile.AvatarPath == "clear" {
|
||||
ui.Avatar = &bridgev2.Avatar{
|
||||
ID: "",
|
||||
Remove: true,
|
||||
}
|
||||
} else if contact.Profile.AvatarPath != "" {
|
||||
ui.Avatar = &bridgev2.Avatar{
|
||||
ID: makeAvatarPathID(contact.Profile.AvatarPath),
|
||||
}
|
||||
|
||||
if s.Main.MsgConv.DirectMedia {
|
||||
userID, err := signalid.ParseUserLoginID(s.UserLogin.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse user login ID: %w", err)
|
||||
}
|
||||
mediaID, err := signalid.DirectMediaProfileAvatar{
|
||||
UserID: userID,
|
||||
ContactID: contact.ACI,
|
||||
ProfileAvatarPath: contact.Profile.AvatarPath,
|
||||
}.AsMediaID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ui.Avatar.MXC, err = s.Main.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ui.Avatar.Hash = signalid.HashMediaID(mediaID)
|
||||
} else {
|
||||
ui.Avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
return s.Client.DownloadUserAvatar(ctx, contact.Profile.AvatarPath, contact.Profile.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ui, nil
|
||||
}
|
||||
|
||||
func (s *SignalConnector) ValidateUserID(id networkid.UserID) bool {
|
||||
_, err := signalid.ParseUserIDAsServiceID(id)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, 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 e164Number uint64
|
||||
var recipient *types.Recipient
|
||||
serviceID, err := signalid.ParseUserIDAsServiceID(networkid.UserID(number))
|
||||
if err != nil {
|
||||
number, err = bridgev2.CleanPhoneNumber(number)
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapRespErr(err, mautrix.MInvalidParam)
|
||||
}
|
||||
e164Number, err = strconv.ParseUint(strings.TrimPrefix(number, "+"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("error parsing phone number: %w", err), mautrix.MInvalidParam)
|
||||
}
|
||||
e164String := fmt.Sprintf("+%d", e164Number)
|
||||
if recipient, err = s.Client.ContactByE164(ctx, e164String); err != nil {
|
||||
return nil, fmt.Errorf("error looking up number in local contact list: %w", err)
|
||||
} else if recipient != nil && (recipient.ACI == uuid.Nil || !s.Client.Store.RecipientStore.IsUnregistered(ctx, libsignalgo.NewACIServiceID(recipient.ACI))) {
|
||||
aci = recipient.ACI
|
||||
pni = recipient.PNI
|
||||
} else if resp, err := s.Client.LookupPhone(ctx, e164Number); err != nil {
|
||||
return nil, fmt.Errorf("error looking up number on server: %w", err)
|
||||
} else {
|
||||
aci = resp[e164Number].ACI
|
||||
pni = resp[e164Number].PNI
|
||||
if aci == uuid.Nil && pni == uuid.Nil {
|
||||
return nil, nil
|
||||
}
|
||||
recipient, err = s.Client.Store.RecipientStore.UpdateRecipientE164(ctx, aci, pni, e164String)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save recipient entry after looking up phone")
|
||||
}
|
||||
aci, pni = recipient.ACI, recipient.PNI
|
||||
if aci != uuid.Nil {
|
||||
s.Client.Store.RecipientStore.MarkUnregistered(ctx, libsignalgo.NewACIServiceID(aci), false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
aci, pni = serviceID.ToACIAndPNI()
|
||||
recipient, err = s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading recipient: %w", err)
|
||||
}
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Uint64("e164", e164Number).
|
||||
Stringer("aci", aci).
|
||||
Stringer("pni", pni).
|
||||
Msg("Found resolve identifier target user")
|
||||
|
||||
userInfo, err := s.contactToUserInfo(ctx, recipient)
|
||||
if err != nil {
|
||||
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
|
||||
resp := &bridgev2.ResolveIdentifierResponse{
|
||||
UserID: userID,
|
||||
UserInfo: userInfo,
|
||||
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
|
||||
}
|
||||
resp.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, resp.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
|
||||
group := &signalmeow.Group{
|
||||
Title: ptr.Val(params.Name).Name,
|
||||
Members: make([]*signalmeow.GroupMember, 1, len(params.Participants)+1),
|
||||
Description: ptr.Val(params.Topic).Topic,
|
||||
AnnouncementsOnly: false,
|
||||
DisappearingMessagesDuration: uint32(ptr.Val(params.Disappear).Timer.Seconds()),
|
||||
AccessControl: &signalmeow.GroupAccessControl{
|
||||
Members: signalmeow.AccessControl_MEMBER,
|
||||
AddFromInviteLink: signalmeow.AccessControl_UNSATISFIABLE,
|
||||
Attributes: signalmeow.AccessControl_ADMINISTRATOR,
|
||||
},
|
||||
}
|
||||
var pl *event.PowerLevelsEventContent
|
||||
// TODO actually get PLs
|
||||
if pl != nil {
|
||||
if pl.EventsDefault > pl.UsersDefault {
|
||||
group.AnnouncementsOnly = true
|
||||
}
|
||||
if pl.Invite() > pl.UsersDefault {
|
||||
group.AccessControl.Members = signalmeow.AccessControl_ADMINISTRATOR
|
||||
}
|
||||
if pl.GetEventLevel(event.StateRoomName) <= pl.UsersDefault {
|
||||
group.AccessControl.Attributes = signalmeow.AccessControl_MEMBER
|
||||
}
|
||||
}
|
||||
group.Members[0] = &signalmeow.GroupMember{
|
||||
ACI: s.Client.Store.ACI,
|
||||
Role: signalmeow.GroupMember_ADMINISTRATOR,
|
||||
}
|
||||
currentTS := uint64(time.Now().UnixMilli())
|
||||
for _, member := range params.Participants {
|
||||
userID, err := signalid.ParseUserIDAsServiceID(member)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user ID %q: %w", member, err)
|
||||
}
|
||||
if userID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
group.Members = append(group.Members, &signalmeow.GroupMember{
|
||||
ACI: userID.UUID,
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare group creation: %w", err)
|
||||
}
|
||||
var avatarBytes []byte
|
||||
var avatarMXC id.ContentURIString
|
||||
if params.Avatar != nil && params.Avatar.URL != "" {
|
||||
avatarMXC = params.Avatar.URL
|
||||
avatarBytes, err = s.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
group.AvatarPath, err = s.Client.UploadGroupAvatar(ctx, avatarBytes, group.GroupIdentifier, group.GroupMasterKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to upload avatar: %w", err)
|
||||
}
|
||||
}
|
||||
portal, err := s.Main.Bridge.GetPortalByKey(ctx, s.makePortalKey(string(group.GroupIdentifier)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get portal: %w", err)
|
||||
}
|
||||
if params.RoomID != "" {
|
||||
err = portal.UpdateMatrixRoomID(ctx, params.RoomID, bridgev2.UpdateMatrixRoomIDParams{SyncDBMetadata: func() {
|
||||
portal.Name = group.Title
|
||||
portal.NameSet = true
|
||||
portal.Topic = group.Description
|
||||
portal.TopicSet = true
|
||||
portal.AvatarHash = sha256.Sum256(avatarBytes)
|
||||
portal.AvatarSet = true
|
||||
portal.AvatarMXC = avatarMXC
|
||||
portal.AvatarID = makeAvatarPathID(group.AvatarPath)
|
||||
if group.DisappearingMessagesDuration > 0 {
|
||||
portal.Disappear = database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(group.DisappearingMessagesDuration) * time.Second,
|
||||
}
|
||||
}
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set portal room ID: %w", err)
|
||||
}
|
||||
}
|
||||
resp, err := s.Client.CreateGroup(ctx, group)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create group: %w", err)
|
||||
}
|
||||
if params.RoomID != "" {
|
||||
// UpdateMatrixRoomID could do this for us if we passed ChatInfoSource to it,
|
||||
// but we only want to do it after the group is successfully created
|
||||
portal.UpdateBridgeInfo(ctx)
|
||||
portal.UpdateCapabilities(ctx, s.UserLogin, true)
|
||||
}
|
||||
wrappedInfo, err := s.wrapGroupInfo(ctx, resp, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to wrap group info for sync: %w", err)
|
||||
}
|
||||
return &bridgev2.CreateChatResponse{
|
||||
PortalKey: portal.PortalKey,
|
||||
Portal: portal,
|
||||
PortalInfo: wrappedInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
recipients, err := s.Client.Store.RecipientStore.LoadAllContacts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*bridgev2.ResolveIdentifierResponse, len(recipients))
|
||||
for i, recipient := range recipients {
|
||||
userInfo, err := s.contactToUserInfo(ctx, recipient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert contact: %w", err)
|
||||
}
|
||||
recipientResp := &bridgev2.ResolveIdentifierResponse{
|
||||
UserInfo: userInfo,
|
||||
Chat: s.makeCreateDMResponse(ctx, recipient, nil),
|
||||
}
|
||||
if recipient.ACI != uuid.Nil {
|
||||
recipientResp.UserID = signalid.MakeUserID(recipient.ACI)
|
||||
ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost for %s: %w", recipient.ACI, err)
|
||||
}
|
||||
recipientResp.Ghost = ghost
|
||||
} else {
|
||||
recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
|
||||
}
|
||||
resp[i] = recipientResp
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) makeCreateDMResponse(ctx context.Context, recipient *types.Recipient, backupChat *store.BackupChat) *bridgev2.CreateChatResponse {
|
||||
namePtr := bridgev2.DefaultChatName
|
||||
topic := PrivateChatTopic
|
||||
selfUser := s.makeEventSender(s.Client.Store.ACI)
|
||||
members := &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
selfUser.Sender: {
|
||||
EventSender: selfUser,
|
||||
Membership: event.MembershipJoin,
|
||||
PowerLevel: &moderatorPL,
|
||||
},
|
||||
},
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: 0,
|
||||
event.StateTopic: 0,
|
||||
event.StateRoomAvatar: 0,
|
||||
event.StateBeeperDisappearingTimer: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
if s.Main.Config.NumberInTopic && recipient.E164 != "" {
|
||||
topic = fmt.Sprintf("%s with %s", PrivateChatTopic, recipient.E164)
|
||||
}
|
||||
var serviceID libsignalgo.ServiceID
|
||||
var avatar *bridgev2.Avatar
|
||||
if recipient.ACI == uuid.Nil {
|
||||
namePtr = ptr.Ptr(s.Main.Config.FormatDisplayname(recipient))
|
||||
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
|
||||
} else {
|
||||
if backupChat == nil {
|
||||
var err error
|
||||
backupChat, err = s.Client.Store.BackupStore.GetBackupChatByUserID(ctx, libsignalgo.NewACIServiceID(recipient.ACI))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get backup chat for recipient")
|
||||
}
|
||||
}
|
||||
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
|
||||
if recipient.ACI == s.Client.Store.ACI {
|
||||
namePtr = ptr.Ptr(NoteToSelfName)
|
||||
avatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(s.Main.Config.NoteToSelfAvatar),
|
||||
Remove: len(s.Main.Config.NoteToSelfAvatar) == 0,
|
||||
MXC: s.Main.Config.NoteToSelfAvatar,
|
||||
Hash: sha256.Sum256([]byte(s.Main.Config.NoteToSelfAvatar)),
|
||||
}
|
||||
} else {
|
||||
// The other user is only present if their ACI is known
|
||||
recipientUser := s.makeEventSender(recipient.ACI)
|
||||
members.MemberMap[recipientUser.Sender] = bridgev2.ChatMember{
|
||||
EventSender: recipientUser,
|
||||
Membership: event.MembershipJoin,
|
||||
PowerLevel: &moderatorPL,
|
||||
}
|
||||
}
|
||||
serviceID = libsignalgo.NewACIServiceID(recipient.ACI)
|
||||
}
|
||||
return &bridgev2.CreateChatResponse{
|
||||
PortalKey: s.makeDMPortalKey(serviceID),
|
||||
PortalInfo: &bridgev2.ChatInfo{
|
||||
Name: namePtr,
|
||||
Avatar: avatar,
|
||||
Topic: &topic,
|
||||
Members: members,
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
|
||||
MessageRequest: ptr.Ptr(recipient.ACI != uuid.Nil && recipient.ProbablyMessageRequest()),
|
||||
CanBackfill: backupChat != nil,
|
||||
ExtraUpdates: updatePortalSyncMeta,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeAvatarPathID(avatarPath string) networkid.AvatarID {
|
||||
if avatarPath == "" {
|
||||
return ""
|
||||
}
|
||||
return networkid.AvatarID("path:" + avatarPath)
|
||||
}
|
||||
|
|
@ -1,180 +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 (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/simplevent"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf/backuppb"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
func (s *SignalClient) stopChatSync() {
|
||||
if cancel := s.cancelChatSync.Swap(nil); cancel != nil {
|
||||
(*cancel)()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) syncChats(ctx context.Context, cancel context.CancelFunc) {
|
||||
defer cancel()
|
||||
|
||||
if s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced {
|
||||
return
|
||||
}
|
||||
|
||||
if s.Client.Store.EphemeralBackupKey != nil {
|
||||
zerolog.Ctx(ctx).Info().Msg("Fetching transfer archive before syncing chats")
|
||||
meta, err := s.Client.WaitForTransfer(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to request transfer archive")
|
||||
return
|
||||
} else if meta.Error != "" {
|
||||
zerolog.Ctx(ctx).Error().Str("error_type", meta.Error).Msg("Transfer archive request was rejected")
|
||||
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced = true
|
||||
err = s.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login metadata after transfer archive request was rejected")
|
||||
}
|
||||
return
|
||||
}
|
||||
err = s.Client.FetchAndProcessTransfer(ctx, meta)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to fetch and process transfer archive")
|
||||
return
|
||||
}
|
||||
zerolog.Ctx(ctx).Info().Msg("Transfer archive fetched and processed, syncing chats")
|
||||
}
|
||||
chats, err := s.Client.Store.BackupStore.GetBackupChats(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get chats from backup store")
|
||||
return
|
||||
}
|
||||
zerolog.Ctx(ctx).Info().Int("chat_count", len(chats)).Msg("Fetched chats to sync from database")
|
||||
for _, chat := range chats {
|
||||
if ctx.Err() != nil {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
AnErr("ctx_err", ctx.Err()).
|
||||
Msg("Context cancelled while syncing chats, stopping")
|
||||
return
|
||||
}
|
||||
recipient, err := s.Client.Store.BackupStore.GetBackupRecipient(ctx, chat.RecipientId)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get recipient for chat")
|
||||
continue
|
||||
} else if recipient == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Uint64("backup_chat_id", chat.Id).
|
||||
Uint64("backup_recipient_id", chat.RecipientId).
|
||||
Msg("No recipient found for chat")
|
||||
continue
|
||||
}
|
||||
resyncEvt := &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
return c.
|
||||
Int("message_count", chat.TotalMessages).
|
||||
Uint64("backup_chat_id", chat.Id).
|
||||
Uint64("backup_recipient_id", chat.RecipientId)
|
||||
},
|
||||
CreatePortal: true,
|
||||
},
|
||||
LatestMessageTS: time.UnixMilli(int64(chat.LatestMessageID)),
|
||||
}
|
||||
switch dest := recipient.Destination.(type) {
|
||||
case *backuppb.Recipient_Contact:
|
||||
aci := tryCastUUID(dest.Contact.GetAci())
|
||||
pni := tryCastUUID(dest.Contact.GetPni())
|
||||
if chat.TotalMessages == 0 {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("aci", aci).
|
||||
Stringer("pni", pni).
|
||||
Uint64("e164", dest.Contact.GetE164()).
|
||||
Msg("Skipping direct chat with no messages and deleting data")
|
||||
err = s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")
|
||||
}
|
||||
continue
|
||||
}
|
||||
processedRecipient, err := s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full recipient data")
|
||||
continue
|
||||
}
|
||||
dmInfo := s.makeCreateDMResponse(ctx, processedRecipient, chat)
|
||||
resyncEvt.PortalKey = dmInfo.PortalKey
|
||||
resyncEvt.ChatInfo = dmInfo.PortalInfo
|
||||
case *backuppb.Recipient_Self:
|
||||
processedRecipient, err := s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, s.Client.Store.ACI, uuid.Nil, nil)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full recipient data")
|
||||
continue
|
||||
}
|
||||
dmInfo := s.makeCreateDMResponse(ctx, processedRecipient, chat)
|
||||
resyncEvt.PortalKey = dmInfo.PortalKey
|
||||
resyncEvt.ChatInfo = dmInfo.PortalInfo
|
||||
case *backuppb.Recipient_Group:
|
||||
if len(dest.Group.MasterKey) != libsignalgo.GroupMasterKeyLength {
|
||||
continue
|
||||
}
|
||||
rawGroupID, err := libsignalgo.GroupMasterKey(dest.Group.MasterKey).GroupIdentifier()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Uint64("recipient_id", recipient.Id).
|
||||
Msg("Failed to get group identifier from master key")
|
||||
continue
|
||||
}
|
||||
groupID := types.GroupIdentifier(base64.StdEncoding.EncodeToString(rawGroupID[:]))
|
||||
groupInfo, err := s.getGroupInfo(ctx, groupID, dest.Group.GetSnapshot().GetVersion(), chat)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get full group info")
|
||||
continue
|
||||
}
|
||||
resyncEvt.PortalKey = s.makePortalKey(string(groupID))
|
||||
resyncEvt.ChatInfo = groupInfo
|
||||
default:
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Type("destination_type", dest).
|
||||
Uint64("backup_chat_id", chat.Id).
|
||||
Uint64("backup_recipient_id", chat.RecipientId).
|
||||
Msg("Ignoring and deleting chat with unsupported destination type")
|
||||
err = s.Client.Store.BackupStore.DeleteBackupChat(ctx, chat.Id)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete chat from backup store")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !s.UserLogin.QueueRemoteEvent(resyncEvt).Success {
|
||||
return
|
||||
}
|
||||
}
|
||||
s.UserLogin.Metadata.(*signalid.UserLoginMetadata).ChatsSynced = true
|
||||
err = s.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login metadata after syncing chats")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,365 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
||||
)
|
||||
|
||||
type SignalClient struct {
|
||||
Main *SignalConnector
|
||||
UserLogin *bridgev2.UserLogin
|
||||
Client *signalmeow.Client
|
||||
Ghost *bridgev2.Ghost
|
||||
|
||||
queueEmptyWaiter *exsync.Event
|
||||
cancelChatSync atomic.Pointer[context.CancelFunc]
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
|
||||
)
|
||||
|
||||
var pushCfg = &bridgev2.PushConfig{
|
||||
FCM: &bridgev2.FCMPushConfig{
|
||||
// https://github.com/signalapp/Signal-Android/blob/main/app/src/main/res/values/firebase_messaging.xml#L4
|
||||
SenderID: "312334754206",
|
||||
},
|
||||
APNs: &bridgev2.APNsPushConfig{
|
||||
BundleID: "org.whispersystems.signal",
|
||||
},
|
||||
}
|
||||
|
||||
func (s *SignalClient) GetPushConfigs() *bridgev2.PushConfig {
|
||||
return pushCfg
|
||||
}
|
||||
|
||||
func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error {
|
||||
if s.Client == nil {
|
||||
return bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
switch pushType {
|
||||
case bridgev2.PushTypeFCM:
|
||||
return s.Client.RegisterFCM(ctx, token)
|
||||
case bridgev2.PushTypeAPNs:
|
||||
return s.Client.RegisterAPNs(ctx, token)
|
||||
default:
|
||||
return fmt.Errorf("unsupported push type: %s", pushType)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
||||
return s.Main.MsgConv.DownloadImagePack(ctx, url)
|
||||
}
|
||||
|
||||
func (s *SignalClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
||||
return []*event.ImagePackMetadata{}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) LogoutRemote(ctx context.Context) {
|
||||
if s.Client == nil {
|
||||
return
|
||||
}
|
||||
s.stopChatSync()
|
||||
err := s.Client.Unlink(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to unlink device")
|
||||
}
|
||||
err = s.Client.StopReceiveLoops()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop receive loops for logout")
|
||||
}
|
||||
err = s.Main.Store.DeleteDevice(context.TODO(), &s.Client.Store.DeviceData)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete device from store")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
|
||||
if s.Client == nil {
|
||||
return false
|
||||
}
|
||||
return userID == signalid.MakeUserID(s.Client.Store.ACI)
|
||||
}
|
||||
|
||||
func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnectionStatus) {
|
||||
var peekedConnectionStatus signalmeow.SignalConnectionStatus
|
||||
for {
|
||||
var connectionStatus signalmeow.SignalConnectionStatus
|
||||
if peekedConnectionStatus.Event != signalmeow.SignalConnectionEventNone {
|
||||
s.UserLogin.Log.Debug().
|
||||
Stringer("peeked_connection_status_event", peekedConnectionStatus.Event).
|
||||
Msg("Using peeked connectionStatus event")
|
||||
connectionStatus = peekedConnectionStatus
|
||||
peekedConnectionStatus = signalmeow.SignalConnectionStatus{}
|
||||
} else {
|
||||
var ok bool
|
||||
connectionStatus, ok = <-statusChan
|
||||
if !ok {
|
||||
s.UserLogin.Log.Debug().Msg("statusChan channel closed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := connectionStatus.Err
|
||||
switch connectionStatus.Event {
|
||||
case signalmeow.SignalConnectionEventConnected:
|
||||
s.UserLogin.Log.Debug().Msg("Sending Connected BridgeState")
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
|
||||
case signalmeow.SignalConnectionEventDisconnected:
|
||||
s.UserLogin.Log.Debug().Msg("Received SignalConnectionEventDisconnected")
|
||||
|
||||
// Debounce: wait 7s before sending TransientDisconnect, in case we get a reconnect
|
||||
// We should wait until the next message comes in, or 7 seconds has passed.
|
||||
// - If a disconnected event comes in, just loop again, unless it's been more than 7 seconds.
|
||||
// - If a non-disconnected event comes in, store it in peekedConnectionStatus,
|
||||
// break out of this loop and go back to the top of the goroutine to handle it in the switch.
|
||||
// - If 7 seconds passes without any non-disconnect messages, send the TransientDisconnect.
|
||||
// (Why 7 seconds? It was 5 at first, but websockets min retry is 5 seconds,
|
||||
// so it would send TransientDisconnect right before reconnecting. 7 seems to work well.)
|
||||
debounceTimer := time.NewTimer(7 * time.Second)
|
||||
PeekLoop:
|
||||
for {
|
||||
var ok bool
|
||||
select {
|
||||
case peekedConnectionStatus, ok = <-statusChan:
|
||||
// Handle channel closing
|
||||
if !ok {
|
||||
s.UserLogin.Log.Debug().Msg("connectionStatus channel closed")
|
||||
return
|
||||
}
|
||||
// If it's another Disconnected event, just keep looping
|
||||
if peekedConnectionStatus.Event == signalmeow.SignalConnectionEventDisconnected {
|
||||
peekedConnectionStatus = signalmeow.SignalConnectionStatus{}
|
||||
continue
|
||||
}
|
||||
// If it's a non-disconnect event, break out of the PeekLoop and handle it in the switch
|
||||
break PeekLoop
|
||||
case <-debounceTimer.C:
|
||||
// Time is up, so break out of the loop and send the TransientDisconnect
|
||||
break PeekLoop
|
||||
}
|
||||
}
|
||||
// We're out of the PeekLoop, so either we got a non-disconnect event, or it's been 7 seconds (or both).
|
||||
// We want to send TransientDisconnect if it's been 7 seconds, but not if the latest event was something
|
||||
// other than Disconnected
|
||||
if !debounceTimer.Stop() { // If the timer has already expired
|
||||
// Send TransientDisconnect only if the latest event is a disconnect or no event
|
||||
// (peekedConnectionStatus could be something else if the timer and the event race)
|
||||
if peekedConnectionStatus.Event == signalmeow.SignalConnectionEventDisconnected ||
|
||||
peekedConnectionStatus.Event == signalmeow.SignalConnectionEventNone {
|
||||
s.UserLogin.Log.Debug().Msg("Sending TransientDisconnect BridgeState")
|
||||
if err == nil {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect})
|
||||
} else {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case signalmeow.SignalConnectionEventLoggedOut:
|
||||
s.stopChatSync()
|
||||
s.UserLogin.Log.Debug().Msg("Sending BadCredentials BridgeState")
|
||||
if err == nil {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
|
||||
} else {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: err.Error()})
|
||||
}
|
||||
err = s.Client.ClearKeysAndDisconnect(context.TODO())
|
||||
if err != nil {
|
||||
s.UserLogin.Log.Error().Err(err).Msg("Failed to clear keys and disconnect")
|
||||
}
|
||||
|
||||
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.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
|
||||
|
||||
case signalmeow.SignalConnectionCleanShutdown:
|
||||
if s.Client.IsLoggedIn() {
|
||||
s.UserLogin.Log.Debug().Msg("Clean Shutdown - sending no BridgeState")
|
||||
} else {
|
||||
s.UserLogin.Log.Debug().Msg("Clean Shutdown, but logged out - Sending BadCredentials BridgeState")
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) Connect(ctx context.Context) {
|
||||
if s.Client == nil {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You're not logged into Signal"})
|
||||
return
|
||||
}
|
||||
s.updateRemoteProfile(ctx, false)
|
||||
s.tryConnect(ctx, 0, true)
|
||||
}
|
||||
|
||||
func (s *SignalClient) ConnectBackground(ctx context.Context, _ *bridgev2.ConnectBackgroundParams) error {
|
||||
s.queueEmptyWaiter.Clear()
|
||||
ch, unauthCh, err := s.Client.StartWebsockets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Disconnect()
|
||||
log := zerolog.Ctx(ctx)
|
||||
queueEmpty := s.queueEmptyWaiter.GetChan()
|
||||
didConnect := false
|
||||
for {
|
||||
select {
|
||||
case status := <-ch:
|
||||
switch status.Event {
|
||||
case web.SignalWebsocketConnectionEventConnected:
|
||||
log.Info().Msg("Authed websocket connected")
|
||||
didConnect = true
|
||||
case web.SignalWebsocketConnectionEventDisconnected:
|
||||
log.Err(status.Err).Msg("Authed websocket disconnected")
|
||||
case web.SignalWebsocketConnectionEventLoggedOut:
|
||||
log.Err(status.Err).Msg("Authed websocket logged out")
|
||||
return fmt.Errorf("authed websocket logged out: %w", status.Err)
|
||||
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError:
|
||||
log.Err(status.Err).Msg("Authed websocket error")
|
||||
return fmt.Errorf("authed websocket errored: %w", status.Err)
|
||||
case web.SignalWebsocketConnectionEventCleanShutdown:
|
||||
log.Info().Msg("Authed websocket clean shutdown")
|
||||
}
|
||||
case status := <-unauthCh:
|
||||
switch status.Event {
|
||||
case web.SignalWebsocketConnectionEventConnected:
|
||||
log.Info().Msg("Unauthed websocket connected")
|
||||
case web.SignalWebsocketConnectionEventDisconnected:
|
||||
log.Err(status.Err).Msg("Unauthed websocket disconnected")
|
||||
case web.SignalWebsocketConnectionEventLoggedOut:
|
||||
log.Err(status.Err).Msg("Unauthed websocket logged out")
|
||||
case web.SignalWebsocketConnectionEventError, web.SignalWebsocketConnectionEventFatalError:
|
||||
log.Err(status.Err).Msg("Unauthed websocket error")
|
||||
case web.SignalWebsocketConnectionEventCleanShutdown:
|
||||
log.Info().Msg("Unauthed websocket clean shutdown")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
log.Warn().Msg("Context finished before queue empty event")
|
||||
if didConnect {
|
||||
// Don't propagate timeout errors if the connection was successful at least once
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-queueEmpty:
|
||||
log.Info().Msg("Received queue empty event")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) Disconnect() {
|
||||
if s.Client == nil {
|
||||
return
|
||||
}
|
||||
s.stopChatSync()
|
||||
err := s.Client.StopReceiveLoops()
|
||||
if err != nil {
|
||||
s.UserLogin.Log.Err(err).Msg("Failed to stop receive loops")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) postLoginConnect() {
|
||||
ctx := s.UserLogin.Log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
s.tryConnect(ctx, 0, false)
|
||||
}
|
||||
|
||||
func (s *SignalClient) tryConnect(ctx context.Context, retryCount int, noLoginSync bool) {
|
||||
if ctx.Err() != nil {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("retry_count", retryCount).
|
||||
AnErr("ctx_err", ctx.Err()).
|
||||
Msg("Context is canceled, not trying to connect")
|
||||
return
|
||||
}
|
||||
if retryCount == 0 {
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
||||
}
|
||||
ch, err := s.Client.StartReceiveLoops(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to start receive loops")
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
|
||||
retryInSeconds := 2 << retryCount
|
||||
if retryInSeconds > 150 {
|
||||
retryInSeconds = 150
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
|
||||
select {
|
||||
case <-time.After(time.Duration(retryInSeconds) * time.Second):
|
||||
case <-ctx.Done():
|
||||
zerolog.Ctx(ctx).Info().Msg("Context canceled, exit tryConnect")
|
||||
return
|
||||
}
|
||||
s.tryConnect(ctx, retryCount+1, noLoginSync)
|
||||
return
|
||||
}
|
||||
syncCtx, cancel := context.WithCancel(ctx)
|
||||
if oldCancel := s.cancelChatSync.Swap(&cancel); oldCancel != nil {
|
||||
(*oldCancel)()
|
||||
}
|
||||
go s.bridgeStateLoop(ch)
|
||||
if noLoginSync {
|
||||
go s.syncChats(syncCtx, cancel)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) IsLoggedIn() bool {
|
||||
if s.Client == nil {
|
||||
return false
|
||||
}
|
||||
return s.Client.IsLoggedIn()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +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 connector
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
up "go.mau.fi/util/configupgrade"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
//go:embed example-config.yaml
|
||||
var ExampleConfig string
|
||||
|
||||
type SignalConfig struct {
|
||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||
UseContactAvatars bool `yaml:"use_contact_avatars"`
|
||||
SyncContactsOnStartup bool `yaml:"sync_contacts_on_startup"`
|
||||
UseOutdatedProfiles bool `yaml:"use_outdated_profiles"`
|
||||
NumberInTopic bool `yaml:"number_in_topic"`
|
||||
DeviceName string `yaml:"device_name"`
|
||||
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
|
||||
LocationFormat string `yaml:"location_format"`
|
||||
DisappearViewOnce bool `yaml:"disappear_view_once"`
|
||||
ExtEvPolls bool `yaml:"extev_polls"`
|
||||
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
type umConfig SignalConfig
|
||||
|
||||
func (c *SignalConfig) UnmarshalYAML(node *yaml.Node) error {
|
||||
err := node.Decode((*umConfig)(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.PostProcess()
|
||||
}
|
||||
|
||||
func (c *SignalConfig) PostProcess() error {
|
||||
var err error
|
||||
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
|
||||
return err
|
||||
}
|
||||
|
||||
type DisplaynameParams struct {
|
||||
ProfileName string
|
||||
ContactName string
|
||||
Nickname string
|
||||
Username string
|
||||
PhoneNumber string
|
||||
UUID string
|
||||
ACI string
|
||||
PNI string
|
||||
AboutEmoji string
|
||||
}
|
||||
|
||||
func (c *SignalConfig) FormatDisplayname(contact *types.Recipient) string {
|
||||
var nameBuf strings.Builder
|
||||
err := c.displaynameTemplate.Execute(&nameBuf, &DisplaynameParams{
|
||||
ProfileName: contact.Profile.Name,
|
||||
ContactName: contact.ContactName,
|
||||
Nickname: contact.Nickname,
|
||||
Username: "",
|
||||
PhoneNumber: contact.E164,
|
||||
UUID: contact.ACI.String(),
|
||||
ACI: contact.ACI.String(),
|
||||
PNI: contact.PNI.String(),
|
||||
AboutEmoji: contact.Profile.AboutEmoji,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nameBuf.String()
|
||||
}
|
||||
|
||||
func upgradeConfig(helper up.Helper) {
|
||||
helper.Copy(up.Str, "displayname_template")
|
||||
helper.Copy(up.Bool, "use_contact_avatars")
|
||||
helper.Copy(up.Bool, "sync_contacts_on_startup")
|
||||
helper.Copy(up.Bool, "use_outdated_profiles")
|
||||
helper.Copy(up.Bool, "number_in_topic")
|
||||
helper.Copy(up.Str, "device_name")
|
||||
helper.Copy(up.Str, "note_to_self_avatar")
|
||||
helper.Copy(up.Str, "location_format")
|
||||
helper.Copy(up.Bool, "disappear_view_once")
|
||||
helper.Copy(up.Bool, "extev_polls")
|
||||
}
|
||||
|
||||
func (s *SignalConnector) GetConfig() (string, any, up.Upgrader) {
|
||||
return ExampleConfig, &s.Config, up.SimpleUpgrader(upgradeConfig)
|
||||
}
|
||||
|
|
@ -1,137 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/exhttp"
|
||||
"go.mau.fi/util/exsync"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/commands"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"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/store"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
|
||||
)
|
||||
|
||||
type SignalConnector struct {
|
||||
MsgConv *msgconv.MessageConverter
|
||||
Store *store.Container
|
||||
Bridge *bridgev2.Bridge
|
||||
Config SignalConfig
|
||||
}
|
||||
|
||||
var _ bridgev2.NetworkConnector = (*SignalConnector)(nil)
|
||||
var _ bridgev2.MaxFileSizeingNetwork = (*SignalConnector)(nil)
|
||||
var _ bridgev2.TransactionIDGeneratingNetwork = (*SignalConnector)(nil)
|
||||
|
||||
func (s *SignalConnector) GetName() bridgev2.BridgeName {
|
||||
return bridgev2.BridgeName{
|
||||
DisplayName: "Signal",
|
||||
NetworkURL: "https://signal.org",
|
||||
NetworkIcon: "mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp",
|
||||
NetworkID: "signal",
|
||||
BeeperBridgeType: "signal",
|
||||
DefaultPort: 29328,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalConnector) Init(bridge *bridgev2.Bridge) {
|
||||
s.Store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "signalmeow").Logger()))
|
||||
s.Bridge = bridge
|
||||
s.MsgConv = msgconv.NewMessageConverter(bridge)
|
||||
s.MsgConv.LocationFormat = s.Config.LocationFormat
|
||||
s.MsgConv.DisappearViewOnce = s.Config.DisappearViewOnce
|
||||
s.MsgConv.ExtEvPolls = s.Config.ExtEvPolls
|
||||
bridge.Commands.(*commands.Processor).AddHandlers(CmdDiscardSenderKey)
|
||||
}
|
||||
|
||||
func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
|
||||
s.MsgConv.MaxFileSize = maxSize
|
||||
}
|
||||
|
||||
func (s *SignalConnector) Start(ctx context.Context) error {
|
||||
s.ResetHTTPTransport()
|
||||
err := s.Store.Upgrade(ctx)
|
||||
if err != nil {
|
||||
return bridgev2.DBUpgradeError{Err: err, Section: "signalmeow"}
|
||||
}
|
||||
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 {
|
||||
aci, err := uuid.Parse(string(login.ID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse user login ID: %w", err)
|
||||
}
|
||||
device, err := s.Store.DeviceByACI(ctx, aci)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get device from store: %w", err)
|
||||
}
|
||||
sc := &SignalClient{
|
||||
Main: s,
|
||||
UserLogin: login,
|
||||
|
||||
queueEmptyWaiter: exsync.NewEvent(),
|
||||
}
|
||||
if device != nil {
|
||||
sc.Client = signalmeow.NewClient(
|
||||
device,
|
||||
sc.UserLogin.Log.With().Str("component", "signalmeow").Logger(),
|
||||
sc.handleSignalEvent,
|
||||
)
|
||||
sc.Client.SyncContactsOnConnect = s.Config.SyncContactsOnStartup &&
|
||||
time.Since(login.Metadata.(*signalid.UserLoginMetadata).LastContactSync.Time) > 3*24*time.Hour
|
||||
}
|
||||
login.Client = sc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalConnector) GenerateTransactionID(userID id.UserID, roomID id.RoomID, eventType event.Type) networkid.RawTransactionID {
|
||||
return networkid.RawTransactionID(strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/mediaproxy"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
var _ bridgev2.DirectMediableNetwork = (*SignalConnector)(nil)
|
||||
|
||||
func (s *SignalConnector) SetUseDirectMedia() {
|
||||
s.MsgConv.DirectMedia = true
|
||||
}
|
||||
|
||||
func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
log := s.Bridge.Log.With().Str("component", "direct download").Logger()
|
||||
|
||||
info, err := signalid.ParseDirectMediaInfo(mediaID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse direct media id: %w", err)
|
||||
}
|
||||
|
||||
var rawDataResp []byte
|
||||
switch info := info.(type) {
|
||||
case *signalid.DirectMediaAttachment:
|
||||
log.Info().
|
||||
Uint64("cdn_id", info.CDNID).
|
||||
Str("cdn_key", info.CDNKey).
|
||||
Uint32("cdn_number", info.CDNNumber).
|
||||
Int("key_len", len(info.Key)).
|
||||
Int("digest_len", len(info.Digest)).
|
||||
Bool("plaintext_digest", info.PlaintextDigest).
|
||||
Uint32("size", info.Size).
|
||||
Msg("Direct downloading attachment")
|
||||
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
|
||||
_, err := signalmeow.DownloadAttachment(
|
||||
ctx, info.CDNID, info.CDNKey, info.CDNNumber, info.Key, info.Digest, info.PlaintextDigest, info.Size, w,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mediaproxy.FileMeta{}, nil
|
||||
},
|
||||
}, nil
|
||||
case *signalid.DirectMediaGroupAvatar:
|
||||
log.Info().
|
||||
Stringer("user_id", info.UserID).
|
||||
Hex("group_id", info.GroupID[:]).
|
||||
Str("group_avatar_path", info.GroupAvatarPath).
|
||||
Msg("Direct downloading group avatar")
|
||||
|
||||
groupID := types.GroupIdentifier(base64.StdEncoding.EncodeToString(info.GroupID[:]))
|
||||
|
||||
userLogin, err := s.Bridge.GetExistingUserLoginByID(ctx, signalid.MakeUserLoginID(info.UserID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user login: %w", err)
|
||||
} else if userLogin == nil {
|
||||
return nil, bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
|
||||
client := userLogin.Client.(*SignalClient)
|
||||
|
||||
groupMasterKey, err := client.Client.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, groupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to to get group master key: %w", err)
|
||||
}
|
||||
|
||||
rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, err
|
||||
}
|
||||
case *signalid.DirectMediaProfileAvatar:
|
||||
log.Info().
|
||||
Stringer("user_id", info.UserID).
|
||||
Stringer("contact_id", info.ContactID).
|
||||
Str("profile_avatar_path", info.ProfileAvatarPath).
|
||||
Msg("Direct downloading profile avatar")
|
||||
|
||||
userLogin, err := s.Bridge.GetExistingUserLoginByID(ctx, signalid.MakeUserLoginID(info.UserID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user login: %w", err)
|
||||
} else if userLogin == nil {
|
||||
return nil, bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
|
||||
client := userLogin.Client.(*SignalClient)
|
||||
|
||||
profileKey, err := client.Client.Store.RecipientStore.LoadProfileKey(ctx, info.ContactID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get contact: %w", err)
|
||||
} else if profileKey == nil {
|
||||
return nil, fmt.Errorf("profile key not found")
|
||||
}
|
||||
|
||||
rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, 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)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Direct download failed")
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
|
||||
}
|
||||
if rawDataResp == nil {
|
||||
return nil, fmt.Errorf("unexpected fallthrough with no data")
|
||||
}
|
||||
return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Displayname template for Signal users.
|
||||
# {{.ProfileName}} - The Signal profile name set by the user.
|
||||
# {{.ContactName}} - The name for the user from your phone's contact list. This is not safe on multi-user instances.
|
||||
# {{.Nickname}} - The nickname set for the user in the native Signal app. This is not safe on multi-user instances.
|
||||
# {{.PhoneNumber}} - The phone number of the user.
|
||||
# {{.UUID}} - The UUID of the Signal user.
|
||||
# {{.AboutEmoji}} - The emoji set by the user in their profile.
|
||||
displayname_template: '{{or .ProfileName .PhoneNumber "Unknown user"}}'
|
||||
# Should avatars from the user's contact list be used? This is not safe on multi-user instances.
|
||||
use_contact_avatars: false
|
||||
# Should the bridge request the user's contact list from the phone on startup?
|
||||
sync_contacts_on_startup: true
|
||||
# Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.
|
||||
use_outdated_profiles: false
|
||||
# Should the Signal user's phone number be included in the room topic in private chat portal rooms?
|
||||
number_in_topic: true
|
||||
# Default device name that shows up in the Signal app.
|
||||
device_name: mautrix-signal
|
||||
# Avatar image for the Note to Self room.
|
||||
note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
|
||||
# Format for generating URLs from location messages for sending to Signal.
|
||||
# Google Maps: 'https://www.google.com/maps/place/%[1]s,%[2]s'
|
||||
# OpenStreetMap: 'https://www.openstreetmap.org/?mlat=%[1]s&mlon=%[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?
|
||||
disappear_view_once: false
|
||||
# Should polls be sent using unstable MSC3381 event types?
|
||||
extev_polls: false
|
||||
|
|
@ -1,441 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
var defaultPL = 0
|
||||
var moderatorPL = 50
|
||||
|
||||
func roleToPL(role signalmeow.GroupMemberRole) *int {
|
||||
switch role {
|
||||
case signalmeow.GroupMember_ADMINISTRATOR:
|
||||
return &moderatorPL
|
||||
case signalmeow.GroupMember_DEFAULT:
|
||||
fallthrough
|
||||
default:
|
||||
return &defaultPL
|
||||
}
|
||||
}
|
||||
|
||||
func applyAnnouncementsOnly(plc *bridgev2.PowerLevelOverrides, announcementsOnly bool) {
|
||||
if announcementsOnly {
|
||||
plc.EventsDefault = &moderatorPL
|
||||
} else {
|
||||
plc.EventsDefault = &defaultPL
|
||||
}
|
||||
}
|
||||
|
||||
func applyAttributesAccess(plc *bridgev2.PowerLevelOverrides, attributeAccess signalmeow.AccessControl) {
|
||||
attributePL := defaultPL
|
||||
if attributeAccess == signalmeow.AccessControl_ADMINISTRATOR {
|
||||
attributePL = moderatorPL
|
||||
}
|
||||
plc.Events[event.StateRoomName] = attributePL
|
||||
plc.Events[event.StateRoomAvatar] = attributePL
|
||||
plc.Events[event.StateTopic] = attributePL
|
||||
plc.Events[event.StateBeeperDisappearingTimer] = attributePL
|
||||
}
|
||||
|
||||
func applyMembersAccess(plc *bridgev2.PowerLevelOverrides, memberAccess signalmeow.AccessControl) {
|
||||
if memberAccess == signalmeow.AccessControl_ADMINISTRATOR {
|
||||
plc.Invite = &moderatorPL
|
||||
} else {
|
||||
plc.Invite = &defaultPL
|
||||
}
|
||||
}
|
||||
|
||||
func inviteLinkToJoinRule(inviteLinkAccess signalmeow.AccessControl) event.JoinRule {
|
||||
switch inviteLinkAccess {
|
||||
case signalmeow.AccessControl_UNSATISFIABLE:
|
||||
return event.JoinRuleInvite
|
||||
case signalmeow.AccessControl_ADMINISTRATOR:
|
||||
return event.JoinRuleKnock
|
||||
case signalmeow.AccessControl_ANY:
|
||||
// TODO allow public portals?
|
||||
publicPortals := false
|
||||
if publicPortals {
|
||||
return event.JoinRulePublic
|
||||
} else {
|
||||
return event.JoinRuleKnock
|
||||
}
|
||||
default:
|
||||
return event.JoinRuleInvite
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve group by id: %w", err)
|
||||
}
|
||||
return s.wrapGroupInfo(ctx, groupInfo, backupChat)
|
||||
}
|
||||
|
||||
func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.Group, backupChat *store.BackupChat) (*bridgev2.ChatInfo, error) {
|
||||
members := &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(groupInfo.Members)+len(groupInfo.PendingMembers)+len(groupInfo.RequestingMembers)+len(groupInfo.BannedMembers)),
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
Events: map[event.Type]int{
|
||||
event.StatePowerLevels: moderatorPL,
|
||||
},
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
}
|
||||
applyAnnouncementsOnly(members.PowerLevels, groupInfo.AnnouncementsOnly)
|
||||
joinRule := event.JoinRuleInvite
|
||||
if groupInfo.AccessControl != nil {
|
||||
applyAttributesAccess(members.PowerLevels, groupInfo.AccessControl.Attributes)
|
||||
applyMembersAccess(members.PowerLevels, groupInfo.AccessControl.Members)
|
||||
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 {
|
||||
members.MemberMap.Set(bridgev2.ChatMember{
|
||||
EventSender: s.makeEventSender(member.ACI),
|
||||
PowerLevel: roleToPL(member.Role),
|
||||
Membership: event.MembershipJoin,
|
||||
})
|
||||
}
|
||||
for _, member := range groupInfo.BannedMembers {
|
||||
s.addChatMemberWithACIQuery(ctx, members.MemberMap, member.ServiceID, bridgev2.ChatMember{
|
||||
Membership: event.MembershipBan,
|
||||
})
|
||||
}
|
||||
if backupChat == nil {
|
||||
var err error
|
||||
// TODO allow using backup chat for data too instead of asking server?
|
||||
backupChat, err = s.Client.Store.BackupStore.GetBackupChatByGroupID(ctx, groupInfo.GroupIdentifier)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get backup chat for group")
|
||||
}
|
||||
}
|
||||
avatar, err := s.makeGroupAvatar(ctx, groupInfo.GroupIdentifier, &groupInfo.AvatarPath, groupInfo.GroupMasterKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make group avatar: %w", err)
|
||||
}
|
||||
return &bridgev2.ChatInfo{
|
||||
Name: &groupInfo.Title,
|
||||
Topic: &groupInfo.Description,
|
||||
Avatar: avatar,
|
||||
Disappear: &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(groupInfo.DisappearingMessagesDuration) * time.Second,
|
||||
},
|
||||
Members: members,
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
JoinRule: &event.JoinRulesEventContent{JoinRule: joinRule},
|
||||
ExtraUpdates: bridgev2.MergeExtraUpdaters(makeRevisionUpdater(groupInfo.Revision), updatePortalSyncMeta),
|
||||
CanBackfill: backupChat != nil,
|
||||
|
||||
ExcludeChangesFromTimeline: true,
|
||||
}, 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 {
|
||||
meta := portal.Metadata.(*signalid.PortalMetadata)
|
||||
meta.LastSync = jsontime.UnixNow()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SignalClient) makeGroupAvatar(ctx context.Context, groupID types.GroupIdentifier, path *string, groupMasterKey types.SerializedGroupMasterKey) (*bridgev2.Avatar, error) {
|
||||
if path == nil {
|
||||
return nil, nil
|
||||
}
|
||||
avatar := &bridgev2.Avatar{
|
||||
ID: makeAvatarPathID(*path),
|
||||
Remove: *path == "",
|
||||
}
|
||||
if s.Main.MsgConv.DirectMedia {
|
||||
userID, err := signalid.ParseUserLoginID(s.UserLogin.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse user login ID: %w", err)
|
||||
}
|
||||
groupIDBytes, err := groupID.Bytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group id bytes: %w", err)
|
||||
}
|
||||
mediaID, err := signalid.DirectMediaGroupAvatar{
|
||||
UserID: userID,
|
||||
GroupID: groupIDBytes,
|
||||
GroupAvatarPath: *path,
|
||||
}.AsMediaID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
avatar.MXC, err = s.Main.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
avatar.Hash = signalid.HashMediaID(mediaID)
|
||||
} else {
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
return s.Client.DownloadGroupAvatar(ctx, *path, groupMasterKey)
|
||||
}
|
||||
}
|
||||
return avatar, nil
|
||||
}
|
||||
|
||||
func makeRevisionUpdater(rev uint32) func(ctx context.Context, portal *bridgev2.Portal) bool {
|
||||
return func(ctx context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*signalid.PortalMetadata)
|
||||
if meta.Revision < rev {
|
||||
meta.Revision = rev
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, groupID types.GroupIdentifier, rev uint32, groupChange *signalmeow.GroupChange) (*bridgev2.ChatInfoChange, error) {
|
||||
avatar, err := s.makeGroupAvatar(ctx, groupID, groupChange.ModifyAvatar, groupChange.GroupMasterKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ic := &bridgev2.ChatInfoChange{
|
||||
ChatInfo: &bridgev2.ChatInfo{
|
||||
ExtraUpdates: makeRevisionUpdater(rev),
|
||||
Name: groupChange.ModifyTitle,
|
||||
Topic: groupChange.ModifyDescription,
|
||||
Avatar: avatar,
|
||||
},
|
||||
}
|
||||
if groupChange.ModifyDisappearingMessagesDuration != nil {
|
||||
ic.ChatInfo.Disappear = &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(*groupChange.ModifyDisappearingMessagesDuration) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
var pls *bridgev2.PowerLevelOverrides
|
||||
if groupChange.ModifyAnnouncementsOnly != nil ||
|
||||
groupChange.ModifyAttributesAccess != nil ||
|
||||
groupChange.ModifyMemberAccess != nil {
|
||||
pls = &bridgev2.PowerLevelOverrides{Events: make(map[event.Type]int)}
|
||||
if groupChange.ModifyAnnouncementsOnly != nil {
|
||||
applyAnnouncementsOnly(pls, *groupChange.ModifyAnnouncementsOnly)
|
||||
}
|
||||
if groupChange.ModifyAttributesAccess != nil {
|
||||
applyAttributesAccess(pls, *groupChange.ModifyAttributesAccess)
|
||||
}
|
||||
if groupChange.ModifyMemberAccess != nil {
|
||||
applyMembersAccess(pls, *groupChange.ModifyMemberAccess)
|
||||
}
|
||||
}
|
||||
if groupChange.ModifyAddFromInviteLinkAccess != nil {
|
||||
ic.ChatInfo.JoinRule = &event.JoinRulesEventContent{
|
||||
JoinRule: inviteLinkToJoinRule(*groupChange.ModifyAddFromInviteLinkAccess),
|
||||
}
|
||||
}
|
||||
mc := make(bridgev2.ChatMemberMap)
|
||||
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 {
|
||||
mc.Set(bridgev2.ChatMember{
|
||||
EventSender: s.makeEventSender(member.ACI),
|
||||
PowerLevel: roleToPL(member.Role),
|
||||
Membership: event.MembershipJoin,
|
||||
})
|
||||
}
|
||||
for _, member := range groupChange.ModifyMemberRoles {
|
||||
mc.Set(bridgev2.ChatMember{
|
||||
EventSender: s.makeEventSender(member.ACI),
|
||||
PowerLevel: roleToPL(member.Role),
|
||||
Membership: event.MembershipJoin,
|
||||
})
|
||||
}
|
||||
if len(mc) > 0 || pls != nil {
|
||||
ic.MemberChanges = &bridgev2.ChatMemberList{MemberMap: mc, PowerLevels: pls}
|
||||
}
|
||||
return ic, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) addChatMemberWithACIQuery(
|
||||
ctx context.Context, mc bridgev2.ChatMemberMap, serviceID libsignalgo.ServiceID, member bridgev2.ChatMember,
|
||||
) {
|
||||
member.EventSender = s.makeEventSenderFromServiceID(serviceID)
|
||||
mc.Set(member)
|
||||
if aci := s.tryResolvePNItoLoggedInACI(ctx, serviceID); aci != nil {
|
||||
member.EventSender = s.makeEventSender(*aci)
|
||||
mc.Add(member)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) {
|
||||
if fromRevision >= toRevision {
|
||||
return
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("action", "catch up group changes").
|
||||
Uint32("from_revision", fromRevision).
|
||||
Uint32("to_revision", toRevision).
|
||||
Logger()
|
||||
if fromRevision == 0 {
|
||||
log.Info().Msg("Syncing full group info")
|
||||
info, err := s.getGroupInfo(ctx, types.GroupIdentifier(portal.ID), toRevision, nil)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get group info")
|
||||
} else {
|
||||
portal.UpdateInfo(ctx, info, s.UserLogin, nil, time.Time{})
|
||||
}
|
||||
} else {
|
||||
log.Info().Msg("Syncing missed group changes")
|
||||
groupChanges, err := s.Client.GetGroupHistoryPage(ctx, types.GroupIdentifier(portal.ID), fromRevision, false)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get group history page")
|
||||
s.catchUpGroup(ctx, portal, 0, toRevision, ts)
|
||||
return
|
||||
}
|
||||
for _, gc := range groupChanges {
|
||||
log.Debug().Uint32("current_rev", gc.GroupChange.Revision).Msg("Processing group change")
|
||||
chatInfoChange, err := s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(portal.ID), gc.GroupChange.Revision, gc.GroupChange)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to convert group info")
|
||||
} else {
|
||||
portal.ProcessChatInfoChange(ctx, s.makeEventSenderFromServiceID(gc.GroupChange.SourceServiceID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
|
||||
}
|
||||
if gc.GroupChange.Revision == toRevision {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,912 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/util/variationselector"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/simplevent"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
)
|
||||
|
||||
var (
|
||||
_ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil)
|
||||
_ bridgev2.ChatViewingNetworkAPI = (*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 {
|
||||
userID, groupID, err := signalid.ParsePortalID(portalID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if groupID != "" {
|
||||
result, err := s.Client.SendGroupMessage(ctx, groupID, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalRecipients := len(result.FailedToSendTo) + len(result.SuccessfullySentTo)
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Int("total_recipients", totalRecipients).
|
||||
Int("failed_to_send_to_count", len(result.FailedToSendTo)).
|
||||
Int("successfully_sent_to_count", len(result.SuccessfullySentTo)).
|
||||
Logger()
|
||||
if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 {
|
||||
log.Debug().Msg("No successes or failures - Probably sent to myself")
|
||||
} else if len(result.SuccessfullySentTo) == 0 {
|
||||
log.Error().Msg("Failed to send event to all members of Signal group")
|
||||
return errors.New("failed to send to any members of Signal group")
|
||||
} 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")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msg("Sent event to all members of Signal group")
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
res := s.Client.SendMessage(ctx, userID, content)
|
||||
if !res.WasSuccessful {
|
||||
return res.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getTimestampForEvent(txnID networkid.RawTransactionID, evt *event.Event, origSender *bridgev2.OrigSender) uint64 {
|
||||
if origSender != nil {
|
||||
// Relaybot messages are never allowed to set the timestamp
|
||||
return uint64(time.Now().UnixMilli())
|
||||
}
|
||||
if len(txnID) > 0 {
|
||||
parsed, err := strconv.ParseUint(string(txnID), 10, 64)
|
||||
if err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return uint64(evt.Timestamp)
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
|
||||
converted, err := s.Main.MsgConv.ToSignal(
|
||||
ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
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)
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(msgID))
|
||||
err := s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(converted))
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
|
||||
}
|
||||
dbMsg := &database.Message{
|
||||
ID: msgID,
|
||||
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||
Timestamp: time.UnixMilli(int64(ts)),
|
||||
Metadata: meta,
|
||||
}
|
||||
return &bridgev2.MatrixMessageResponse{
|
||||
DB: dbMsg,
|
||||
RemovePending: networkid.TransactionID(msgID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
|
||||
_, targetSentTimestamp, err := signalid.ParseMessageID(msg.EditTarget.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
} else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
|
||||
return fmt.Errorf("cannot edit other people's messages")
|
||||
}
|
||||
var replyTo *database.Message
|
||||
if msg.EditTarget.ReplyTo.MessageID != "" {
|
||||
replyTo, err = s.Main.Bridge.DB.Message.GetFirstOrSpecificPartByID(ctx, msg.Portal.Receiver, msg.EditTarget.ReplyTo)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
converted.Timestamp = &ts
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapEditMessage(&signalpb.EditMessage{
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
DataMessage: converted,
|
||||
}))
|
||||
if err != nil {
|
||||
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
|
||||
}
|
||||
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts)
|
||||
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
|
||||
msg.EditTarget.EditCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||
return bridgev2.MatrixReactionPreResponse{
|
||||
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||
EmojiID: "",
|
||||
Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) {
|
||||
targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
|
||||
Reaction: &signalpb.DataMessage_Reaction{
|
||||
Emoji: proto.String(msg.PreHandleResp.Emoji),
|
||||
Remove: proto.Bool(false),
|
||||
TargetAuthorAciBinary: targetAuthorACI[:],
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &database.Reaction{}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
|
||||
targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetReaction.MessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)),
|
||||
Reaction: &signalpb.DataMessage_Reaction{
|
||||
Emoji: proto.String(msg.TargetReaction.Emoji),
|
||||
Remove: proto.Bool(true),
|
||||
TargetAuthorAciBinary: targetAuthorACI[:],
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
|
||||
_, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
} else if msg.TargetMessage.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
|
||||
return fmt.Errorf("cannot delete other people's messages")
|
||||
}
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
err = s.sendMessage(ctx, msg.Portal.ID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
Timestamp: proto.Uint64(ts),
|
||||
Delete: &signalpb.DataMessage_Delete{
|
||||
TargetSentTimestamp: proto.Uint64(targetSentTimestamp),
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
|
||||
if !receipt.ReadUpTo.After(receipt.LastRead) {
|
||||
return nil
|
||||
}
|
||||
if receipt.LastRead.IsZero() {
|
||||
receipt.LastRead = receipt.ReadUpTo.Add(-5 * time.Second)
|
||||
}
|
||||
dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(ctx, receipt.Portal.PortalKey, receipt.LastRead, receipt.ReadUpTo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get messages to mark as read: %w", err)
|
||||
} else if len(dbMessages) == 0 {
|
||||
return nil
|
||||
}
|
||||
messagesToRead := map[uuid.UUID][]uint64{}
|
||||
for _, msg := range dbMessages {
|
||||
userID, timestamp, err := signalid.ParseMessageID(msg.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
messagesToRead[userID] = append(messagesToRead[userID], timestamp)
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Any("targets", messagesToRead).
|
||||
Msg("Collected read receipt target messages")
|
||||
|
||||
// TODO send sync message manually containing all read receipts instead of a separate message for each recipient
|
||||
|
||||
for destination, messages := range messagesToRead {
|
||||
// Don't send read receipts for own messages
|
||||
if destination == s.Client.Store.ACI {
|
||||
continue
|
||||
}
|
||||
// Don't use portal.sendSignalMessage because we're sending this straight to
|
||||
// who sent the original message, not the portal's ChatID
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
result := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(destination), signalmeow.ReadReceptMessageForTimestamps(messages))
|
||||
cancel()
|
||||
if !result.WasSuccessful {
|
||||
zerolog.Ctx(ctx).Err(result.Error).
|
||||
Stringer("destination", destination).
|
||||
Uints64("message_ids", messages).
|
||||
Msg("Failed to send read receipt to Signal")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("destination", destination).
|
||||
Uints64("message_ids", messages).
|
||||
Msg("Sent read receipt to Signal")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error {
|
||||
userID, groupID, err := signalid.ParsePortalID(typing.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typingMessage := signalmeow.TypingMessage(typing.IsTyping)
|
||||
if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
result := s.Client.SendMessage(ctx, userID, typingMessage)
|
||||
if !result.WasSuccessful {
|
||||
return result.Error
|
||||
}
|
||||
} else if groupID != "" {
|
||||
_, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev2.Portal, gc *signalmeow.GroupChange, postUpdatePortal func()) (bool, error) {
|
||||
_, groupID, err := signalid.ParsePortalID(portal.ID)
|
||||
if err != nil || groupID == "" {
|
||||
return false, err
|
||||
}
|
||||
gc.Revision = portal.Metadata.(*signalid.PortalMetadata).Revision + 1
|
||||
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if gc.ModifyTitle != nil {
|
||||
portal.Name = *gc.ModifyTitle
|
||||
portal.NameSet = true
|
||||
}
|
||||
if gc.ModifyDescription != nil {
|
||||
portal.Topic = *gc.ModifyDescription
|
||||
portal.TopicSet = true
|
||||
}
|
||||
if gc.ModifyAvatar != nil {
|
||||
portal.AvatarID = makeAvatarPathID(*gc.ModifyAvatar)
|
||||
portal.AvatarSet = true
|
||||
}
|
||||
if postUpdatePortal != nil {
|
||||
postUpdatePortal()
|
||||
}
|
||||
portal.Metadata.(*signalid.PortalMetadata).Revision = revision
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
|
||||
return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{
|
||||
ModifyTitle: &msg.Content.Name,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
|
||||
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil || groupID == "" {
|
||||
return false, err
|
||||
}
|
||||
var avatarPath string
|
||||
var avatarHash [32]byte
|
||||
if msg.Content.URL != "" {
|
||||
data, err := s.Main.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
avatarHash = sha256.Sum256(data)
|
||||
avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to reupload avatar: %w", err)
|
||||
}
|
||||
}
|
||||
return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{
|
||||
ModifyAvatar: &avatarPath,
|
||||
}, func() {
|
||||
msg.Portal.AvatarMXC = msg.Content.URL
|
||||
msg.Portal.AvatarHash = avatarHash
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
|
||||
return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{
|
||||
ModifyDescription: &msg.Content.Topic,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
||||
if msg.Type.IsSelf && msg.OrigSender != nil {
|
||||
return nil, nil
|
||||
}
|
||||
var targetIntent bridgev2.MatrixAPI
|
||||
var targetSignalID libsignalgo.ServiceID
|
||||
var err error
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
switch msg.Type {
|
||||
case bridgev2.Invite:
|
||||
return nil, fmt.Errorf("cannot invite additional user to dm")
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target signal id: %w", err)
|
||||
}
|
||||
switch target := msg.Target.(type) {
|
||||
case *bridgev2.Ghost:
|
||||
targetIntent = target.Intent
|
||||
case *bridgev2.UserLogin:
|
||||
targetIntent = target.User.DoublePuppet(ctx)
|
||||
if targetIntent == nil {
|
||||
ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
|
||||
}
|
||||
targetIntent = ghost.Intent
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("From Membership", string(msg.Type.From)).
|
||||
Str("To Membership", string(msg.Type.To)).
|
||||
Logger()
|
||||
gc := &signalmeow.GroupChange{}
|
||||
role := signalmeow.GroupMember_DEFAULT
|
||||
if msg.Type.To == event.MembershipInvite || msg.Type == bridgev2.AcceptKnock {
|
||||
levels, err := msg.Portal.Bridge.Matrix.GetPowerLevels(ctx, msg.Portal.MXID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't get power levels")
|
||||
if levels.GetUserLevel(targetIntent.GetMXID()) >= moderatorPL {
|
||||
role = signalmeow.GroupMember_ADMINISTRATOR
|
||||
}
|
||||
}
|
||||
}
|
||||
switch msg.Type {
|
||||
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{{
|
||||
ACI: targetSignalID.UUID,
|
||||
}}
|
||||
case bridgev2.RevokeInvite, bridgev2.RejectInvite:
|
||||
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
case bridgev2.Leave, bridgev2.Kick:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't kick non-ACI service ID")
|
||||
}
|
||||
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
case bridgev2.Invite:
|
||||
if targetSignalID.Type == libsignalgo.ServiceIDTypeACI {
|
||||
gc.AddMembers = []*signalmeow.AddMember{{
|
||||
GroupMember: signalmeow.GroupMember{
|
||||
ACI: targetSignalID.UUID,
|
||||
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
|
||||
// because the joining/knocking member doesn't have the GroupMasterKey yet
|
||||
// case bridgev2.Join:
|
||||
// gc.AddMembers = []*signalmeow.AddMember{{
|
||||
// GroupMember: signalmeow.GroupMember{
|
||||
// ACI: targetSignalID,
|
||||
// Role: role,
|
||||
// },
|
||||
// JoinFromInviteLink: true,
|
||||
// }}
|
||||
// case bridgev2.Knock:
|
||||
// gc.AddRequestingMembers = []*signalmeow.RequestingMember{{
|
||||
// ACI: targetSignalID,
|
||||
// Timestamp: uint64(time.Now().UnixMilli()),
|
||||
// }}
|
||||
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{{
|
||||
ACI: targetSignalID.UUID,
|
||||
Role: role,
|
||||
}}
|
||||
case bridgev2.RetractKnock, bridgev2.RejectKnock:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
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:
|
||||
gc.AddBannedMembers = []*signalmeow.BannedMember{{
|
||||
ServiceID: targetSignalID,
|
||||
Timestamp: uint64(time.Now().UnixMilli()),
|
||||
}}
|
||||
switch msg.Type {
|
||||
case bridgev2.BanJoined:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't ban joined non-ACI service ID")
|
||||
}
|
||||
gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
case bridgev2.BanInvited:
|
||||
gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
case bridgev2.BanKnocked:
|
||||
if targetSignalID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
return nil, fmt.Errorf("can't ban knocked non-ACI service ID")
|
||||
}
|
||||
gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID}
|
||||
}
|
||||
case bridgev2.Unban:
|
||||
gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To)
|
||||
}
|
||||
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil || groupID == "" {
|
||||
return nil, err
|
||||
}
|
||||
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
|
||||
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
|
||||
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func plToRole(pl int) signalmeow.GroupMemberRole {
|
||||
if pl >= moderatorPL {
|
||||
return signalmeow.GroupMember_ADMINISTRATOR
|
||||
} else {
|
||||
return signalmeow.GroupMember_DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
func plToAccessControl(pl int) *signalmeow.AccessControl {
|
||||
var accessControl signalmeow.AccessControl
|
||||
if pl >= moderatorPL {
|
||||
accessControl = signalmeow.AccessControl_ADMINISTRATOR
|
||||
} else {
|
||||
accessControl = signalmeow.AccessControl_MEMBER
|
||||
}
|
||||
return &accessControl
|
||||
}
|
||||
|
||||
func hasAdminChanged(plc *bridgev2.SinglePowerLevelChange) bool {
|
||||
if plc == nil {
|
||||
return false
|
||||
}
|
||||
return (plc.NewLevel < moderatorPL) != (plc.OrigLevel < moderatorPL)
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixPowerLevels(ctx context.Context, msg *bridgev2.MatrixPowerLevelChange) (bool, error) {
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
return false, nil
|
||||
}
|
||||
gc := &signalmeow.GroupChange{}
|
||||
for _, plc := range msg.Users {
|
||||
if !hasAdminChanged(&plc.SinglePowerLevelChange) {
|
||||
continue
|
||||
}
|
||||
serviceID, err := signalid.ParseGhostOrUserLoginID(plc.Target)
|
||||
if err != nil || serviceID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
continue
|
||||
}
|
||||
gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{
|
||||
ACI: serviceID.UUID,
|
||||
Role: plToRole(plc.NewLevel),
|
||||
})
|
||||
}
|
||||
if hasAdminChanged(msg.EventsDefault) {
|
||||
announcementsOnly := msg.EventsDefault.NewLevel >= moderatorPL
|
||||
gc.ModifyAnnouncementsOnly = &announcementsOnly
|
||||
}
|
||||
if hasAdminChanged(msg.StateDefault) {
|
||||
gc.ModifyAttributesAccess = plToAccessControl(msg.StateDefault.NewLevel)
|
||||
}
|
||||
if hasAdminChanged(msg.Invite) {
|
||||
gc.ModifyMemberAccess = plToAccessControl(msg.Invite.NewLevel)
|
||||
}
|
||||
_, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil || groupID == "" {
|
||||
return false, err
|
||||
}
|
||||
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixViewingChat(ctx context.Context, msg *bridgev2.MatrixViewingChat) error {
|
||||
if msg.Portal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync the other users ghost in DMs
|
||||
if msg.Portal.OtherUserID != "" {
|
||||
ghost, err := s.Main.Bridge.GetExistingGhostByID(ctx, msg.Portal.OtherUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ghost for sync: %w", err)
|
||||
} else if ghost == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Str("other_user_id", string(msg.Portal.OtherUserID)).
|
||||
Msg("No ghost found for other user in portal")
|
||||
} else {
|
||||
meta := ghost.Metadata.(*signalid.GhostMetadata)
|
||||
if meta.ProfileFetchedAt.Time.Add(5 * time.Minute).Before(time.Now()) {
|
||||
// Reset, but don't save, portal last sync time for immediate sync now
|
||||
meta.ProfileFetchedAt.Time = time.Time{}
|
||||
info, err := s.GetUserInfoWithRefreshAfter(ctx, ghost, 5*time.Minute)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
ghost.UpdateInfo(ctx, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always resync the portal if its stale
|
||||
portalMeta := msg.Portal.Metadata.(*signalid.PortalMetadata)
|
||||
if portalMeta.LastSync.Add(24 * time.Hour).Before(time.Now()) {
|
||||
s.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
PortalKey: msg.Portal.PortalKey,
|
||||
},
|
||||
GetChatInfoFunc: s.GetChatInfo,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
|
||||
if msg.Content.Type != event.DisappearingTypeAfterRead && msg.Content.Timer.Duration != 0 {
|
||||
return false, fmt.Errorf("unsupported disappearing timer type: %s", msg.Content.Type)
|
||||
}
|
||||
userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
newSetting := database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterRead,
|
||||
Timer: msg.Content.Timer.Duration,
|
||||
}
|
||||
if newSetting.Timer == 0 {
|
||||
newSetting.Type = event.DisappearingTypeNone
|
||||
}
|
||||
if groupID != "" {
|
||||
return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{
|
||||
ModifyDisappearingMessagesDuration: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
|
||||
}, func() {
|
||||
msg.Portal.Disappear = newSetting
|
||||
})
|
||||
} else {
|
||||
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
|
||||
res := s.Client.SendMessage(ctx, userID, signalmeow.WrapDataMessage(&signalpb.DataMessage{
|
||||
Timestamp: ptr.Ptr(ts),
|
||||
Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)),
|
||||
ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())),
|
||||
}))
|
||||
if !res.WasSuccessful {
|
||||
return false, res.Error
|
||||
}
|
||||
msg.Portal.Disappear = newSetting
|
||||
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
|
||||
}
|
||||
|
|
@ -1,809 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"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/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/simplevent"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
|
||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||
)
|
||||
|
||||
func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
|
||||
switch evt := rawEvt.(type) {
|
||||
case *events.ChatEvent:
|
||||
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, &Bv2ChatEvent{ChatEvent: evt, s: s}).Success
|
||||
case *events.DecryptionError:
|
||||
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapDecryptionError(evt)).Success
|
||||
case *events.Receipt:
|
||||
return s.handleSignalReceipt(evt)
|
||||
case *events.ReadSelf:
|
||||
return s.handleSignalReadSelf(evt)
|
||||
case *events.DeleteForMe:
|
||||
return s.handleSignalDeleteForMe(evt)
|
||||
case *events.MessageRequestResponse:
|
||||
return s.handleSignalMessageRequestResponse(evt)
|
||||
case *events.Call:
|
||||
return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
|
||||
case *events.ContactList:
|
||||
s.handleSignalContactList(evt)
|
||||
case *events.ACIFound:
|
||||
s.handleSignalACIFound(evt)
|
||||
case *events.QueueEmpty:
|
||||
s.queueEmptyWaiter.Set()
|
||||
case *events.LoggedOut:
|
||||
s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: evt.Error.Error()})
|
||||
default:
|
||||
s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SignalClient) wrapCallEvent(evt *events.Call) bridgev2.RemoteMessage {
|
||||
return &simplevent.Message[*events.Call]{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessage,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
c = c.Stringer("sender_id", evt.Info.Sender)
|
||||
c = c.Uint64("message_ts", evt.Timestamp)
|
||||
return c
|
||||
},
|
||||
PortalKey: s.makePortalKey(evt.Info.ChatID),
|
||||
CreatePortal: true,
|
||||
Sender: s.makeEventSender(evt.Info.Sender),
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
},
|
||||
Data: evt,
|
||||
ID: signalid.MakeMessageID(evt.Info.Sender, evt.Timestamp),
|
||||
|
||||
ConvertMessageFunc: convertCallEvent,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data *events.Call) (*bridgev2.ConvertedMessage, error) {
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
}
|
||||
if data.IsRinging {
|
||||
content.Body = "Incoming call"
|
||||
if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
|
||||
content.MsgType = event.MsgText
|
||||
}
|
||||
content.BeeperActionMessage = &event.BeeperActionMessage{
|
||||
Type: event.BeeperActionMessageCall,
|
||||
}
|
||||
} else {
|
||||
content.Body = "Call ended"
|
||||
}
|
||||
return &bridgev2.ConvertedMessage{
|
||||
Parts: []*bridgev2.ConvertedMessagePart{{
|
||||
Type: event.EventMessage,
|
||||
Content: content,
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SignalClient) wrapDecryptionError(evt *events.DecryptionError) bridgev2.RemoteMessage {
|
||||
return &simplevent.Message[*events.DecryptionError]{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessage,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
c = c.Stringer("sender_id", evt.Sender)
|
||||
c = c.Uint64("message_ts", evt.Timestamp)
|
||||
return c
|
||||
},
|
||||
PortalKey: s.makePortalKey(evt.Sender.String()),
|
||||
CreatePortal: true,
|
||||
Sender: s.makeEventSender(evt.Sender),
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
StreamOrder: int64(evt.Timestamp),
|
||||
},
|
||||
Data: evt,
|
||||
// TODO use main message id and edit it if it later becomes decryptable?
|
||||
ID: "decrypterr|" + signalid.MakeMessageID(evt.Sender, evt.Timestamp),
|
||||
|
||||
ConvertMessageFunc: convertDecryptionError,
|
||||
}
|
||||
}
|
||||
|
||||
func convertDecryptionError(_ context.Context, _ *bridgev2.Portal, _ bridgev2.MatrixAPI, _ *events.DecryptionError) (*bridgev2.ConvertedMessage, error) {
|
||||
return &bridgev2.ConvertedMessage{
|
||||
Parts: []*bridgev2.ConvertedMessagePart{{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: "Message couldn't be decrypted. It may have been in this chat or a group chat. Please check your Signal app.",
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Bv2ChatEvent struct {
|
||||
*events.ChatEvent
|
||||
s *SignalClient
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.RemoteMessage = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteEdit = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteEventWithTimestamp = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteReaction = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteReactionRemove = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteMessageRemove = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteTyping = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemotePreHandler = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteChatInfoChange = (*Bv2ChatEvent)(nil)
|
||||
_ bridgev2.RemoteEventWithStreamOrder = (*Bv2ChatEvent)(nil)
|
||||
)
|
||||
|
||||
func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
|
||||
switch innerEvt := evt.Event.(type) {
|
||||
case *signalpb.DataMessage:
|
||||
switch {
|
||||
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.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
|
||||
innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
|
||||
return bridgev2.RemoteEventMessage
|
||||
case innerEvt.Reaction != nil:
|
||||
if innerEvt.Reaction.GetRemove() {
|
||||
return bridgev2.RemoteEventReactionRemove
|
||||
}
|
||||
return bridgev2.RemoteEventReaction
|
||||
case innerEvt.Delete != nil, innerEvt.AdminDelete != nil:
|
||||
return bridgev2.RemoteEventMessageRemove
|
||||
case innerEvt.GetGroupV2().GetGroupChange() != nil:
|
||||
return bridgev2.RemoteEventChatInfoChange
|
||||
}
|
||||
case *signalpb.EditMessage:
|
||||
return bridgev2.RemoteEventEdit
|
||||
case *signalpb.TypingMessage:
|
||||
return bridgev2.RemoteEventTyping
|
||||
}
|
||||
return bridgev2.RemoteEventUnknown
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetChatInfoChange(ctx context.Context) (*bridgev2.ChatInfoChange, error) {
|
||||
dm, _ := evt.Event.(*signalpb.DataMessage)
|
||||
gv2 := dm.GetGroupV2()
|
||||
if gv2 == nil || gv2.GroupChange == nil {
|
||||
return nil, fmt.Errorf("GetChatInfoChange() called for non-GroupChange event")
|
||||
}
|
||||
groupChange, err := evt.s.Client.DecryptGroupChange(ctx, gv2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt group change: %w", err)
|
||||
}
|
||||
// XXX: is this ID compatible with types.GroupIdentifier?
|
||||
return evt.s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(evt.Info.ChatID), gv2.GetRevision(), groupChange)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) {
|
||||
dataMsg, ok := evt.Event.(*signalpb.DataMessage)
|
||||
if !ok || dataMsg.GroupV2 == nil {
|
||||
return
|
||||
}
|
||||
portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision
|
||||
if evt.Info.GroupRevision > portalRev {
|
||||
toRevision := evt.Info.GroupRevision
|
||||
if dataMsg.GetGroupV2().GetGroupChange() != nil {
|
||||
toRevision--
|
||||
}
|
||||
evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp())
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetTimeout() time.Duration {
|
||||
if evt.Event.(*signalpb.TypingMessage).GetAction() == signalpb.TypingMessage_STARTED {
|
||||
return 15 * time.Second
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetPortalKey() networkid.PortalKey {
|
||||
return evt.s.makePortalKey(evt.Info.ChatID)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) ShouldCreatePortal() bool {
|
||||
return evt.GetType() == bridgev2.RemoteEventMessage || evt.GetType() == bridgev2.RemoteEventChatInfoChange
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) AddLogContext(c zerolog.Context) zerolog.Context {
|
||||
c = c.Stringer("sender_id", evt.Info.Sender)
|
||||
switch innerEvt := evt.Event.(type) {
|
||||
case *signalpb.DataMessage:
|
||||
c = c.Uint64("message_ts", innerEvt.GetTimestamp())
|
||||
switch {
|
||||
case innerEvt.Reaction != nil:
|
||||
c = c.Uint64("reaction_target_ts", innerEvt.Reaction.GetTargetSentTimestamp())
|
||||
case innerEvt.Delete != nil:
|
||||
c = c.Uint64("delete_target_ts", innerEvt.Delete.GetTargetSentTimestamp())
|
||||
}
|
||||
case *signalpb.EditMessage:
|
||||
c = c.
|
||||
Uint64("edit_target_ts", innerEvt.GetTargetSentTimestamp()).
|
||||
Uint64("edit_ts", innerEvt.GetDataMessage().GetTimestamp())
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetSender() bridgev2.EventSender {
|
||||
return evt.s.makeEventSender(evt.Info.Sender)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetID() networkid.MessageID {
|
||||
ts := evt.getDataMsgTimestamp()
|
||||
if ts == 0 {
|
||||
return ""
|
||||
}
|
||||
return signalid.MakeMessageID(evt.Info.Sender, ts)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) getDataMsgTimestamp() uint64 {
|
||||
switch innerEvt := evt.Event.(type) {
|
||||
case *signalpb.DataMessage:
|
||||
return innerEvt.GetTimestamp()
|
||||
case *signalpb.EditMessage:
|
||||
return innerEvt.GetDataMessage().GetTimestamp()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
|
||||
ts := evt.getDataMsgTimestamp()
|
||||
if ts == 0 {
|
||||
return time.Now()
|
||||
}
|
||||
return time.UnixMilli(int64(ts))
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
|
||||
var targetAuthorACI uuid.UUID
|
||||
var targetSentTS uint64
|
||||
switch innerEvt := evt.Event.(type) {
|
||||
case *signalpb.DataMessage:
|
||||
switch {
|
||||
case innerEvt.Reaction != nil:
|
||||
targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary())
|
||||
targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
|
||||
case innerEvt.Delete != nil:
|
||||
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:
|
||||
return ""
|
||||
}
|
||||
case *signalpb.EditMessage:
|
||||
targetSentTS = innerEvt.GetTargetSentTimestamp()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
if targetAuthorACI == uuid.Nil {
|
||||
targetAuthorACI = evt.Info.Sender
|
||||
}
|
||||
return signalid.MakeMessageID(targetAuthorACI, targetSentTS)
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
|
||||
dataMsg, ok := evt.Event.(*signalpb.DataMessage)
|
||||
if !ok || dataMsg.Reaction == nil {
|
||||
panic(fmt.Errorf("GetReactionEmoji() called for non-reaction event"))
|
||||
}
|
||||
return dataMsg.GetReaction().GetEmoji(), ""
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetRemovedEmojiID() networkid.EmojiID {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
||||
dataMsg, ok := evt.Event.(*signalpb.DataMessage)
|
||||
if !ok {
|
||||
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)
|
||||
if converted.Disappear.Type != "" {
|
||||
evtTS := evt.GetTimestamp()
|
||||
if !dataMsg.GetIsViewOnce() {
|
||||
portal.UpdateDisappearingSetting(ctx, converted.Disappear, bridgev2.UpdateDisappearingSettingOpts{
|
||||
Sender: intent,
|
||||
Timestamp: evtTS,
|
||||
Implicit: true,
|
||||
Save: true,
|
||||
SendNotice: true,
|
||||
})
|
||||
}
|
||||
if evt.Info.Sender == evt.s.Client.Store.ACI {
|
||||
converted.Disappear.DisappearAt = evtTS.Add(converted.Disappear.Timer)
|
||||
}
|
||||
}
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
|
||||
editMsg, ok := evt.Event.(*signalpb.EditMessage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
|
||||
}
|
||||
// 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)
|
||||
// TODO can anything other than the text be edited?
|
||||
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
|
||||
editPart.Part.EditCount++
|
||||
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
|
||||
return &bridgev2.ConvertedEdit{
|
||||
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (evt *Bv2ChatEvent) GetStreamOrder() int64 {
|
||||
return int64(evt.Info.ServerTimestamp)
|
||||
}
|
||||
|
||||
type Bv2Receipt struct {
|
||||
Type signalpb.ReceiptMessage_Type
|
||||
Chat networkid.PortalKey
|
||||
Sender bridgev2.EventSender
|
||||
|
||||
LastTS time.Time
|
||||
LastID networkid.MessageID
|
||||
IDs []networkid.MessageID
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetType() bridgev2.RemoteEventType {
|
||||
switch b.Type {
|
||||
case signalpb.ReceiptMessage_READ:
|
||||
return bridgev2.RemoteEventReadReceipt
|
||||
case signalpb.ReceiptMessage_DELIVERY:
|
||||
return bridgev2.RemoteEventDeliveryReceipt
|
||||
default:
|
||||
return bridgev2.RemoteEventUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetPortalKey() networkid.PortalKey {
|
||||
return b.Chat
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) AddLogContext(c zerolog.Context) zerolog.Context {
|
||||
return c.
|
||||
Str("sender_id", string(b.Sender.Sender)).
|
||||
Stringer("receipt_type", b.Type).
|
||||
Array("message_ids", exzerolog.ArrayOfStrs(b.IDs))
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetSender() bridgev2.EventSender {
|
||||
return b.Sender
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetLastReceiptTarget() networkid.MessageID {
|
||||
return b.LastID
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetReceiptTargets() []networkid.MessageID {
|
||||
return b.IDs
|
||||
}
|
||||
|
||||
func (b *Bv2Receipt) GetReadUpTo() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
var _ bridgev2.RemoteReadReceipt = (*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 {
|
||||
log := zerolog.Ctx(ctx)
|
||||
receipts := make(map[networkid.PortalKey]*Bv2Receipt)
|
||||
for _, msgID := range input {
|
||||
msg, err := getMessageFunc(ctx, msgID)
|
||||
if err != nil {
|
||||
log.Err(err).Any("message_id", msgID).Msg("Failed to get target message for receipt")
|
||||
} else if msg == nil {
|
||||
log.Debug().Any("message_id", msgID).Msg("Got receipt for unknown message")
|
||||
} else {
|
||||
receiptEvt, ok := receipts[msg.Room]
|
||||
if !ok {
|
||||
receiptEvt = &Bv2Receipt{Chat: msg.Room}
|
||||
receipts[msg.Room] = receiptEvt
|
||||
}
|
||||
receiptEvt.IDs = append(receiptEvt.IDs, msg.ID)
|
||||
if receiptEvt.LastTS.Before(msg.Timestamp) {
|
||||
receiptEvt.LastTS = msg.Timestamp
|
||||
receiptEvt.LastID = msg.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return receipts
|
||||
}
|
||||
|
||||
func (s *SignalClient) dispatchReceipts(sender uuid.UUID, receiptType signalpb.ReceiptMessage_Type, receipts map[networkid.PortalKey]*Bv2Receipt) bool {
|
||||
evtSender := s.makeEventSender(sender)
|
||||
for chat, receiptEvt := range receipts {
|
||||
receiptEvt.Chat = chat
|
||||
receiptEvt.Sender = evtSender
|
||||
receiptEvt.Type = receiptType
|
||||
if !s.Main.Bridge.QueueRemoteEvent(s.UserLogin, receiptEvt).Success {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
|
||||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle signal receipt").
|
||||
Stringer("sender_id", evt.Sender).
|
||||
Stringer("receipt_type", evt.Content.GetType()).
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
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.dispatchReceipts(evt.Sender, evt.Content.GetType(), receipts)
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) bool {
|
||||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle signal read self").
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) {
|
||||
aciUUID, err := signalmeow.ParseStringOrBinaryUUID(msgInfo.GetSenderAci(), msgInfo.GetSenderAciBinary())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(aciUUID, msgInfo.GetTimestamp()))
|
||||
})
|
||||
return s.dispatchReceipts(s.Client.Store.ACI, signalpb.ReceiptMessage_READ, receipts)
|
||||
}
|
||||
|
||||
func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signalpb.ConversationIdentifier) (networkid.PortalKey, bool) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
switch ident := cid.GetIdentifier().(type) {
|
||||
case *signalpb.ConversationIdentifier_ThreadServiceId:
|
||||
serviceID, err := libsignalgo.ServiceIDFromString(ident.ThreadServiceId)
|
||||
if err != nil {
|
||||
log.Err(err).Str("chat_id", ident.ThreadServiceId).Msg("Failed to parse delete for me conversation ID")
|
||||
return networkid.PortalKey{}, false
|
||||
}
|
||||
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:
|
||||
if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
|
||||
log.Error().
|
||||
Str("chat_id", base64.StdEncoding.EncodeToString(ident.ThreadGroupId)).
|
||||
Msg("Invalid group ID length in delete for me conversation")
|
||||
return networkid.PortalKey{}, false
|
||||
}
|
||||
return s.makePortalKey((*libsignalgo.GroupIdentifier)(ident.ThreadGroupId).String()), true
|
||||
case *signalpb.ConversationIdentifier_ThreadE164:
|
||||
log.Warn().Str("chat_id", ident.ThreadE164).Msg("Unsupported E164 conversation ID in delete for me")
|
||||
return networkid.PortalKey{}, false
|
||||
default:
|
||||
log.Warn().
|
||||
Type("chat_id_type", ident).
|
||||
Msg("Unsupported conversation ID protobuf type in delete for me")
|
||||
return networkid.PortalKey{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey networkid.PortalKey, am *signalpb.AddressableMessage) networkid.MessageID {
|
||||
log := zerolog.Ctx(ctx)
|
||||
switch typedAuthor := am.GetAuthor().(type) {
|
||||
case *signalpb.AddressableMessage_AuthorServiceId:
|
||||
serviceID, err := libsignalgo.ServiceIDFromString(typedAuthor.AuthorServiceId)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portalKey).
|
||||
Str("author_service_id", typedAuthor.AuthorServiceId).
|
||||
Msg("Failed to parse delete for me message author service ID")
|
||||
return ""
|
||||
} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
|
||||
log.Warn().
|
||||
Object("portal_key", portalKey).
|
||||
Str("author_service_id", typedAuthor.AuthorServiceId).
|
||||
Msg("Dropping delete for me message with unsupported service ID type")
|
||||
return ""
|
||||
}
|
||||
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:
|
||||
log.Warn().
|
||||
Object("portal_key", portalKey).
|
||||
Str("author_e164", typedAuthor.AuthorE164).
|
||||
Msg("Dropping delete for me message with unsupported E164 author")
|
||||
return ""
|
||||
default:
|
||||
log.Warn().
|
||||
Object("portal_key", portalKey).
|
||||
Type("author_type", typedAuthor).
|
||||
Msg("Dropping delete for me message with unrecognized author protobuf type")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
|
||||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle signal delete for me").
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
for _, conv := range evt.GetConversationDeletes() {
|
||||
if !conv.GetIsFullDelete() {
|
||||
// Non-full deletes might mean clearing chats?
|
||||
continue
|
||||
}
|
||||
portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatDelete,
|
||||
PortalKey: portalKey,
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
StreamOrder: int64(evt.Timestamp),
|
||||
},
|
||||
OnlyForMe: true,
|
||||
})
|
||||
if !res.Success {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, conv := range evt.GetLocalOnlyConversationDeletes() {
|
||||
portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatDelete,
|
||||
PortalKey: portalKey,
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
StreamOrder: int64(evt.Timestamp),
|
||||
},
|
||||
OnlyForMe: true,
|
||||
})
|
||||
if !res.Success {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, conv := range evt.GetMessageDeletes() {
|
||||
portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, msg := range conv.GetMessages() {
|
||||
msgID := s.addressableMessageToID(ctx, portalKey, msg)
|
||||
if msgID == "" {
|
||||
continue
|
||||
}
|
||||
res := s.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessageRemove,
|
||||
PortalKey: portalKey,
|
||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||
StreamOrder: int64(evt.Timestamp),
|
||||
},
|
||||
OnlyForMe: true,
|
||||
TargetMessage: msgID,
|
||||
})
|
||||
if !res.Success {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
log := s.UserLogin.Log.With().
|
||||
Str("action", "handle aci found").
|
||||
Stringer("aci", evt.ACI).
|
||||
Stringer("pni", evt.PNI).
|
||||
Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
pniPortalKey := s.makeDMPortalKey(evt.PNI)
|
||||
aciPortalKey := s.makeDMPortalKey(evt.ACI)
|
||||
result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to re-ID portal")
|
||||
} else if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd {
|
||||
// If the source portal is re-ID'd, we need to sync metadata and participants.
|
||||
// If the source is deleted, then it doesn't matter, any existing target will already be correct
|
||||
info, err := s.GetChatInfo(ctx, portal)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get chat info to update portal after re-ID")
|
||||
} else {
|
||||
portal.UpdateInfo(ctx, info, s.UserLogin, nil, time.Time{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
|
||||
log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger()
|
||||
ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
|
||||
for _, contact := range evt.Contacts {
|
||||
if contact.ACI == uuid.Nil {
|
||||
continue
|
||||
}
|
||||
if !evt.IsFromDB {
|
||||
fullContact, err := s.Client.ContactByACI(ctx, contact.ACI)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get full contact info from store")
|
||||
continue
|
||||
}
|
||||
fullContact.ContactAvatar = contact.ContactAvatar
|
||||
contact = fullContact
|
||||
}
|
||||
ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(contact.ACI))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get ghost to update contact info")
|
||||
continue
|
||||
}
|
||||
userInfo, err := s.contactToUserInfo(ctx, contact)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to convert contact info")
|
||||
continue
|
||||
}
|
||||
ghost.UpdateInfo(ctx, userInfo)
|
||||
if contact.ACI == s.Client.Store.ACI {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) updateRemoteProfile(ctx context.Context, resendState bool) {
|
||||
var err error
|
||||
if s.Ghost == nil {
|
||||
s.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(s.Client.Store.ACI))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost for remote profile update")
|
||||
return
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
if s.UserLogin.RemoteProfile.Name != s.Ghost.Name {
|
||||
s.UserLogin.RemoteProfile.Name = s.Ghost.Name
|
||||
changed = true
|
||||
}
|
||||
if s.UserLogin.RemoteProfile.Avatar != s.Ghost.AvatarMXC {
|
||||
s.UserLogin.RemoteProfile.Avatar = s.Ghost.AvatarMXC
|
||||
changed = true
|
||||
}
|
||||
if len(s.Ghost.Identifiers) > 0 && strings.HasPrefix(s.Ghost.Identifiers[0], "tel:") {
|
||||
phone := strings.TrimPrefix(s.Ghost.Identifiers[0], "tel:")
|
||||
if s.UserLogin.RemoteProfile.Phone != phone {
|
||||
s.UserLogin.RemoteProfile.Phone = phone
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
err = s.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save updated remote profile")
|
||||
}
|
||||
if resendState {
|
||||
// TODO this has potential race conditions
|
||||
s.UserLogin.BridgeState.Send(s.UserLogin.BridgeState.GetPrevUnsent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +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 connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
)
|
||||
|
||||
func (s *SignalClient) makePortalKey(chatID string) networkid.PortalKey {
|
||||
key := networkid.PortalKey{ID: networkid.PortalID(chatID)}
|
||||
// For non-group chats, add receiver
|
||||
if s.Main.Bridge.Config.SplitPortals || len(chatID) != 44 {
|
||||
key.Receiver = s.UserLogin.ID
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (s *SignalClient) makeDMPortalKey(serviceID libsignalgo.ServiceID) networkid.PortalKey {
|
||||
return networkid.PortalKey{
|
||||
ID: signalid.MakeDMPortalID(serviceID),
|
||||
Receiver: s.UserLogin.ID,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
|
||||
return bridgev2.EventSender{
|
||||
IsFromMe: sender == s.Client.Store.ACI,
|
||||
SenderLogin: signalid.MakeUserLoginID(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))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +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 connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
|
||||
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
||||
)
|
||||
|
||||
func (s *SignalConnector) GetLoginFlows() []bridgev2.LoginFlow {
|
||||
return []bridgev2.LoginFlow{{
|
||||
Name: "QR",
|
||||
Description: "Scan a QR code to pair the bridge to your Signal app",
|
||||
ID: "qr",
|
||||
}}
|
||||
}
|
||||
|
||||
func (s *SignalConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
if flowID != "qr" {
|
||||
return nil, fmt.Errorf("invalid login flow ID")
|
||||
}
|
||||
return &QRLogin{User: user, Main: s}, nil
|
||||
}
|
||||
|
||||
type QRLogin struct {
|
||||
User *bridgev2.User
|
||||
Main *SignalConnector
|
||||
cancelChan context.CancelFunc
|
||||
ProvChan chan signalmeow.ProvisioningResponse
|
||||
newQRCount int
|
||||
}
|
||||
|
||||
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
|
||||
|
||||
func (qr *QRLogin) Cancel() {
|
||||
qr.cancelChan()
|
||||
go func() {
|
||||
for range qr.ProvChan {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
const (
|
||||
LoginStepQR = "fi.mau.signal.login.qr"
|
||||
LoginStepProcess = "fi.mau.signal.login.processing"
|
||||
LoginStepComplete = "fi.mau.signal.login.complete"
|
||||
)
|
||||
|
||||
func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
log := qr.Main.Bridge.Log.With().
|
||||
Str("action", "login").
|
||||
Stringer("user_id", qr.User.MXID).
|
||||
Logger()
|
||||
provCtx, cancel := context.WithCancel(log.WithContext(qr.Main.Bridge.BackgroundCtx))
|
||||
qr.cancelChan = cancel
|
||||
// Don't use the start context here: the channel will outlive the start request.
|
||||
qr.ProvChan = signalmeow.PerformProvisioning(
|
||||
provCtx, qr.Main.Store, qr.Main.Config.DeviceName, qr.Main.Bridge.Config.Backfill.Enabled,
|
||||
)
|
||||
var resp signalmeow.ProvisioningResponse
|
||||
select {
|
||||
case resp = <-qr.ProvChan:
|
||||
if resp.Err != nil {
|
||||
return nil, resp.Err
|
||||
} else if resp.State != signalmeow.StateProvisioningURLReceived {
|
||||
return nil, fmt.Errorf("unexpected state %v", resp.State)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return nil, ctx.Err()
|
||||
// TODO separate timeout here?
|
||||
}
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepQR,
|
||||
Instructions: "Scan the QR code on your Signal app to log in",
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeQR,
|
||||
Data: resp.ProvisioningURL,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if qr.ProvChan == nil {
|
||||
return nil, fmt.Errorf("login not started")
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-qr.ProvChan:
|
||||
if resp.Err != nil {
|
||||
qr.cancelChan()
|
||||
return nil, resp.Err
|
||||
} else if resp.State != signalmeow.StateProvisioningDataReceived {
|
||||
qr.cancelChan()
|
||||
return nil, fmt.Errorf("unexpected state %v", resp.State)
|
||||
} else if resp.ProvisioningData.ACI == uuid.Nil {
|
||||
qr.cancelChan()
|
||||
return nil, fmt.Errorf("no signal account ID received")
|
||||
}
|
||||
return qr.loginComplete(ctx, resp.ProvisioningData)
|
||||
|
||||
// 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.
|
||||
case <-time.After(45 * time.Second):
|
||||
qr.cancelChan()
|
||||
qr.newQRCount++
|
||||
if qr.newQRCount >= 6 {
|
||||
return nil, fmt.Errorf("too many QR code refreshes")
|
||||
}
|
||||
return qr.Start(ctx)
|
||||
|
||||
case <-ctx.Done():
|
||||
qr.cancelChan()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (qr *QRLogin) loginComplete(ctx context.Context, provData *store.DeviceData) (*bridgev2.LoginStep, error) {
|
||||
defer qr.cancelChan()
|
||||
ul, err := qr.User.NewLogin(ctx, &database.UserLogin{
|
||||
ID: signalid.MakeUserLoginID(provData.ACI),
|
||||
RemoteName: provData.Number,
|
||||
RemoteProfile: status.RemoteProfile{
|
||||
Phone: provData.Number,
|
||||
},
|
||||
Metadata: &signalid.UserLoginMetadata{},
|
||||
}, &bridgev2.NewLoginParams{
|
||||
DeleteOnConflict: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user login: %w", err)
|
||||
}
|
||||
ul.Client.(*SignalClient).postLoginConnect()
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
StepID: LoginStepComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", provData.Number, provData.ACI),
|
||||
CompleteParams: &bridgev2.LoginCompleteParams{
|
||||
UserLoginID: ul.ID,
|
||||
UserLogin: ul,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -45,35 +45,27 @@ func NewUUIDAddressFromString(uuidStr string, deviceID uint) (*Address, error) {
|
|||
}
|
||||
|
||||
func newAddress(name string, deviceID uint) (*Address, error) {
|
||||
var pa C.SignalMutPointerProtocolAddress
|
||||
var pa *C.SignalProtocolAddress
|
||||
signalFfiError := C.signal_address_new(&pa, C.CString(name), C.uint(deviceID))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapAddress(pa.raw), nil
|
||||
}
|
||||
|
||||
func (pa *Address) mutPtr() C.SignalMutPointerProtocolAddress {
|
||||
return C.SignalMutPointerProtocolAddress{pa.ptr}
|
||||
}
|
||||
|
||||
func (pa *Address) constPtr() C.SignalConstPointerProtocolAddress {
|
||||
return C.SignalConstPointerProtocolAddress{pa.ptr}
|
||||
return wrapAddress(pa), nil
|
||||
}
|
||||
|
||||
func (pa *Address) Clone() (*Address, error) {
|
||||
var cloned C.SignalMutPointerProtocolAddress
|
||||
signalFfiError := C.signal_address_clone(&cloned, pa.constPtr())
|
||||
var cloned *C.SignalProtocolAddress
|
||||
signalFfiError := C.signal_address_clone(&cloned, pa.ptr)
|
||||
runtime.KeepAlive(pa)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapAddress(cloned.raw), nil
|
||||
return wrapAddress(cloned), nil
|
||||
}
|
||||
|
||||
func (pa *Address) Destroy() error {
|
||||
pa.CancelFinalizer()
|
||||
return wrapError(C.signal_address_destroy(pa.mutPtr()))
|
||||
return wrapError(C.signal_address_destroy(pa.ptr))
|
||||
}
|
||||
|
||||
func (pa *Address) CancelFinalizer() {
|
||||
|
|
@ -82,7 +74,7 @@ func (pa *Address) CancelFinalizer() {
|
|||
|
||||
func (pa *Address) Name() (string, error) {
|
||||
var name *C.char
|
||||
signalFfiError := C.signal_address_get_name(&name, pa.constPtr())
|
||||
signalFfiError := C.signal_address_get_name(&name, pa.ptr)
|
||||
runtime.KeepAlive(pa)
|
||||
if signalFfiError != nil {
|
||||
return "", wrapError(signalFfiError)
|
||||
|
|
@ -100,7 +92,7 @@ func (pa *Address) NameServiceID() (ServiceID, error) {
|
|||
|
||||
func (pa *Address) DeviceID() (uint, error) {
|
||||
var deviceID C.uint
|
||||
signalFfiError := C.signal_address_get_device_id(&deviceID, pa.constPtr())
|
||||
signalFfiError := C.signal_address_get_device_id(&deviceID, pa.ptr)
|
||||
runtime.KeepAlive(pa)
|
||||
if signalFfiError != nil {
|
||||
return 0, wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -35,37 +35,23 @@ func wrapAES256_GCM_SIV(ptr *C.SignalAes256GcmSiv) *AES256_GCM_SIV {
|
|||
}
|
||||
|
||||
func NewAES256_GCM_SIV(key []byte) (*AES256_GCM_SIV, error) {
|
||||
var aes C.SignalMutPointerAes256GcmSiv
|
||||
var aes *C.SignalAes256GcmSiv
|
||||
signalFfiError := C.signal_aes256_gcm_siv_new(&aes, BytesToBuffer(key))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapAES256_GCM_SIV(aes.raw), nil
|
||||
}
|
||||
|
||||
func (aes *AES256_GCM_SIV) mutPtr() C.SignalMutPointerAes256GcmSiv {
|
||||
return C.SignalMutPointerAes256GcmSiv{aes.ptr}
|
||||
}
|
||||
|
||||
func (aes *AES256_GCM_SIV) constPtr() C.SignalConstPointerAes256GcmSiv {
|
||||
return C.SignalConstPointerAes256GcmSiv{aes.ptr}
|
||||
return wrapAES256_GCM_SIV(aes), nil
|
||||
}
|
||||
|
||||
func (aes *AES256_GCM_SIV) Destroy() error {
|
||||
runtime.SetFinalizer(aes, nil)
|
||||
return wrapError(C.signal_aes256_gcm_siv_destroy(C.SignalMutPointerAes256GcmSiv{raw: aes.ptr}))
|
||||
return wrapError(C.signal_aes256_gcm_siv_destroy(aes.ptr))
|
||||
}
|
||||
|
||||
func (aes *AES256_GCM_SIV) Encrypt(plaintext, nonce, associatedData []byte) ([]byte, error) {
|
||||
var encrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
|
||||
signalFfiError := C.signal_aes256_gcm_siv_encrypt(
|
||||
&encrypted,
|
||||
C.SignalConstPointerAes256GcmSiv{raw: aes.ptr},
|
||||
BytesToBuffer(plaintext),
|
||||
BytesToBuffer(nonce),
|
||||
BytesToBuffer(associatedData),
|
||||
)
|
||||
signalFfiError := C.signal_aes256_gcm_siv_encrypt(&encrypted, aes.ptr, BytesToBuffer(plaintext), BytesToBuffer(nonce), BytesToBuffer(associatedData))
|
||||
runtime.KeepAlive(aes)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -75,13 +61,7 @@ func (aes *AES256_GCM_SIV) Encrypt(plaintext, nonce, associatedData []byte) ([]b
|
|||
|
||||
func (aes *AES256_GCM_SIV) Decrypt(ciphertext, nonce, associatedData []byte) ([]byte, error) {
|
||||
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_aes256_gcm_siv_decrypt(
|
||||
&decrypted,
|
||||
C.SignalConstPointerAes256GcmSiv{raw: aes.ptr},
|
||||
BytesToBuffer(ciphertext),
|
||||
BytesToBuffer(nonce),
|
||||
BytesToBuffer(associatedData),
|
||||
)
|
||||
signalFfiError := C.signal_aes256_gcm_siv_decrypt(&decrypted, aes.ptr, BytesToBuffer(ciphertext), BytesToBuffer(nonce), BytesToBuffer(associatedData))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber
|
||||
// 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
|
||||
|
|
@ -18,12 +17,12 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -40,30 +39,29 @@ func (ac *AuthCredentialWithPni) Slice() []byte {
|
|||
}
|
||||
|
||||
func ReceiveAuthCredentialWithPni(
|
||||
serverPublicParams *ServerPublicParams,
|
||||
serverPublicParams ServerPublicParams,
|
||||
aci uuid.UUID,
|
||||
pni uuid.UUID,
|
||||
redemptionTime uint64,
|
||||
authCredResponse AuthCredentialWithPniResponse,
|
||||
) (*AuthCredentialWithPni, error) {
|
||||
var c_result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
c_result := [C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN]C.uchar{}
|
||||
c_serverPublicParams := (*[C.SignalSERVER_PUBLIC_PARAMS_LEN]C.uchar)(unsafe.Pointer(&serverPublicParams[0]))
|
||||
c_authCredResponse := (*[C.SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]C.uchar)(unsafe.Pointer(&authCredResponse[0]))
|
||||
|
||||
signalFfiError := C.signal_server_public_params_receive_auth_credential_with_pni_as_service_id(
|
||||
&c_result,
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
c_serverPublicParams,
|
||||
NewACIServiceID(aci).CFixedBytes(),
|
||||
NewPNIServiceID(pni).CFixedBytes(),
|
||||
C.uint64_t(redemptionTime),
|
||||
BytesToBuffer(authCredResponse[:]),
|
||||
c_authCredResponse,
|
||||
)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
resultBytes := CopySignalOwnedBufferToBytes(c_result)
|
||||
if len(resultBytes) != C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN {
|
||||
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN)
|
||||
}
|
||||
return (*AuthCredentialWithPni)(resultBytes), nil
|
||||
result := AuthCredentialWithPni(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func NewAuthCredentialWithPniResponse(b []byte) (*AuthCredentialWithPniResponse, error) {
|
||||
|
|
@ -77,21 +75,23 @@ func NewAuthCredentialWithPniResponse(b []byte) (*AuthCredentialWithPniResponse,
|
|||
}
|
||||
|
||||
func CreateAuthCredentialWithPniPresentation(
|
||||
serverPublicParams *ServerPublicParams,
|
||||
serverPublicParams ServerPublicParams,
|
||||
randomness Randomness,
|
||||
groupSecretParams GroupSecretParams,
|
||||
authCredWithPni AuthCredentialWithPni,
|
||||
) (*AuthCredentialPresentation, error) {
|
||||
var c_result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
c_serverPublicParams := (*[C.SignalSERVER_PUBLIC_PARAMS_LEN]C.uchar)(unsafe.Pointer(&serverPublicParams[0]))
|
||||
c_randomness := (*[C.SignalRANDOMNESS_LEN]C.uchar)(unsafe.Pointer(&randomness[0]))
|
||||
c_groupSecretParams := (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(&groupSecretParams[0]))
|
||||
c_authCredWithPni := (*[C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN]C.uchar)(unsafe.Pointer(&authCredWithPni[0]))
|
||||
|
||||
signalFfiError := C.signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic(
|
||||
&c_result,
|
||||
C.SignalConstPointerServerPublicParams{serverPublicParams},
|
||||
c_serverPublicParams,
|
||||
c_randomness,
|
||||
c_groupSecretParams,
|
||||
BytesToBuffer(authCredWithPni[:]),
|
||||
c_authCredWithPni,
|
||||
)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -1,140 +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 (
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"go.mau.fi/util/random"
|
||||
)
|
||||
|
||||
type BackupKey [C.SignalBACKUP_KEY_LEN]byte
|
||||
|
||||
func (bk *BackupKey) Slice() []byte {
|
||||
if bk == nil {
|
||||
return nil
|
||||
}
|
||||
return bk[:]
|
||||
}
|
||||
|
||||
const BackupIDLength = 16
|
||||
|
||||
type BackupID [BackupIDLength]byte
|
||||
type BackupMetadataKey [C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]byte
|
||||
type BackupMediaID [C.SignalMEDIA_ID_LEN]byte
|
||||
type BackupMediaKey [C.SignalMEDIA_ENCRYPTION_KEY_LEN]byte
|
||||
|
||||
func GenerateRandomBackupKey() *BackupKey {
|
||||
return (*BackupKey)(random.Bytes(C.SignalBACKUP_KEY_LEN))
|
||||
}
|
||||
|
||||
func BytesToBackupKey(bytes []byte) *BackupKey {
|
||||
if len(bytes) != C.SignalBACKUP_KEY_LEN {
|
||||
return nil
|
||||
}
|
||||
return (*BackupKey)(bytes)
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveBackupID(aci ServiceID) (*BackupID, error) {
|
||||
var out BackupID
|
||||
signalFfiError := C.signal_backup_key_derive_backup_id(
|
||||
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
aci.CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
|
||||
var out C.SignalMutPointerPrivateKey
|
||||
signalFfiError := C.signal_backup_key_derive_ec_key(
|
||||
&out,
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&bk)),
|
||||
aci.CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapPrivateKey(out.raw), nil
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error) {
|
||||
var out BackupMetadataKey
|
||||
signalFfiError := C.signal_backup_key_derive_local_backup_metadata_key(
|
||||
(*[C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
|
||||
var out BackupMediaID
|
||||
signalFfiError := C.signal_backup_key_derive_media_id(
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
C.CString(mediaName),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
|
||||
var out BackupMediaKey
|
||||
signalFfiError := C.signal_backup_key_derive_media_encryption_key(
|
||||
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
runtime.KeepAlive(mediaID)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (bk *BackupKey) DeriveThumbnailTransitEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
|
||||
var out BackupMediaKey
|
||||
signalFfiError := C.signal_backup_key_derive_thumbnail_transit_encryption_key(
|
||||
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
|
||||
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
|
||||
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
|
||||
)
|
||||
runtime.KeepAlive(bk)
|
||||
runtime.KeepAlive(mediaID)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
|
@ -17,12 +17,11 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
|
|
@ -44,22 +43,6 @@ func BytesToBuffer(data []byte) C.SignalBorrowedBuffer {
|
|||
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 {
|
||||
return C.SignalBorrowedBuffer{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm -lz -lstdc++
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -44,28 +44,17 @@ func wrapCiphertextMessage(ptr *C.SignalCiphertextMessage) *CiphertextMessage {
|
|||
}
|
||||
|
||||
func NewCiphertextMessage(plaintext *PlaintextContent) (*CiphertextMessage, error) {
|
||||
var ciphertextMessage C.SignalMutPointerCiphertextMessage
|
||||
signalFfiError := C.signal_ciphertext_message_from_plaintext_content(
|
||||
&ciphertextMessage,
|
||||
plaintext.constPtr(),
|
||||
)
|
||||
var ciphertextMessage *C.SignalCiphertextMessage
|
||||
signalFfiError := C.signal_ciphertext_message_from_plaintext_content(&ciphertextMessage, plaintext.ptr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapCiphertextMessage(ciphertextMessage.raw), nil
|
||||
}
|
||||
|
||||
func (c *CiphertextMessage) mutPtr() C.SignalMutPointerCiphertextMessage {
|
||||
return C.SignalMutPointerCiphertextMessage{c.ptr}
|
||||
}
|
||||
|
||||
func (c *CiphertextMessage) constPtr() C.SignalConstPointerCiphertextMessage {
|
||||
return C.SignalConstPointerCiphertextMessage{c.ptr}
|
||||
return wrapCiphertextMessage(ciphertextMessage), nil
|
||||
}
|
||||
|
||||
func (c *CiphertextMessage) Destroy() error {
|
||||
c.CancelFinalizer()
|
||||
return wrapError(C.signal_ciphertext_message_destroy(c.mutPtr()))
|
||||
return wrapError(C.signal_ciphertext_message_destroy(c.ptr))
|
||||
}
|
||||
|
||||
func (c *CiphertextMessage) CancelFinalizer() {
|
||||
|
|
@ -74,7 +63,7 @@ func (c *CiphertextMessage) CancelFinalizer() {
|
|||
|
||||
func (c *CiphertextMessage) Serialize() ([]byte, error) {
|
||||
var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_ciphertext_message_serialize(&serialized, c.constPtr())
|
||||
signalFfiError := C.signal_ciphertext_message_serialize(&serialized, c.ptr)
|
||||
runtime.KeepAlive(c)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -84,7 +73,7 @@ func (c *CiphertextMessage) Serialize() ([]byte, error) {
|
|||
|
||||
func (c *CiphertextMessage) MessageType() (CiphertextMessageType, error) {
|
||||
var messageType C.uint8_t
|
||||
signalFfiError := C.signal_ciphertext_message_type(&messageType, c.constPtr())
|
||||
signalFfiError := C.signal_ciphertext_message_type(&messageType, c.ptr)
|
||||
runtime.KeepAlive(c)
|
||||
if signalFfiError != nil {
|
||||
return 0, wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -39,17 +40,3 @@ func CopySignalOwnedBufferToBytes(buffer C.SignalOwnedBuffer) (b []byte) {
|
|||
C.signal_free_buffer(buffer.base, buffer.length)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,11 +17,13 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DecryptionErrorMessage struct {
|
||||
|
|
@ -37,64 +38,47 @@ func wrapDecryptionErrorMessage(ptr *C.SignalDecryptionErrorMessage) *Decryption
|
|||
}
|
||||
|
||||
func DeserializeDecryptionErrorMessage(messageBytes []byte) (*DecryptionErrorMessage, error) {
|
||||
var dem C.SignalMutPointerDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_deserialize(
|
||||
&dem,
|
||||
BytesToBuffer(messageBytes),
|
||||
)
|
||||
var dem *C.SignalDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_deserialize(&dem, BytesToBuffer(messageBytes))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapDecryptionErrorMessage(dem.raw), nil
|
||||
return wrapDecryptionErrorMessage(dem), nil
|
||||
}
|
||||
|
||||
func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType CiphertextMessageType, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) {
|
||||
var dem C.SignalMutPointerDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_for_original_message(
|
||||
&dem,
|
||||
BytesToBuffer(originalBytes),
|
||||
C.uint8_t(originalType),
|
||||
C.uint64_t(originalTs),
|
||||
C.uint32_t(originalSenderDeviceID),
|
||||
)
|
||||
func DecryptionErrorMessageForOriginalMessage(originalBytes []byte, originalType uint8, originalTs uint64, originalSenderDeviceID uint) (*DecryptionErrorMessage, error) {
|
||||
var dem *C.SignalDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_for_original_message(&dem, BytesToBuffer(originalBytes), C.uint8_t(originalType), C.uint64_t(originalTs), C.uint32_t(originalSenderDeviceID))
|
||||
runtime.KeepAlive(originalBytes)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapDecryptionErrorMessage(dem.raw), nil
|
||||
return wrapDecryptionErrorMessage(dem), nil
|
||||
}
|
||||
|
||||
func DecryptionErrorMessageFromSerializedContent(serialized []byte) (*DecryptionErrorMessage, error) {
|
||||
var dem C.SignalMutPointerDecryptionErrorMessage
|
||||
var dem *C.SignalDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_extract_from_serialized_content(&dem, BytesToBuffer(serialized))
|
||||
runtime.KeepAlive(serialized)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapDecryptionErrorMessage(dem.raw), nil
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) mutPtr() C.SignalMutPointerDecryptionErrorMessage {
|
||||
return C.SignalMutPointerDecryptionErrorMessage{dem.ptr}
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) constPtr() C.SignalConstPointerDecryptionErrorMessage {
|
||||
return C.SignalConstPointerDecryptionErrorMessage{dem.ptr}
|
||||
return wrapDecryptionErrorMessage(dem), nil
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) Clone() (*DecryptionErrorMessage, error) {
|
||||
var cloned C.SignalMutPointerDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_clone(&cloned, dem.constPtr())
|
||||
var cloned *C.SignalDecryptionErrorMessage
|
||||
signalFfiError := C.signal_decryption_error_message_clone(&cloned, dem.ptr)
|
||||
runtime.KeepAlive(dem)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapDecryptionErrorMessage(cloned.raw), nil
|
||||
return wrapDecryptionErrorMessage(cloned), nil
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) Destroy() error {
|
||||
dem.CancelFinalizer()
|
||||
return wrapError(C.signal_decryption_error_message_destroy(dem.mutPtr()))
|
||||
return wrapError(C.signal_decryption_error_message_destroy(dem.ptr))
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) CancelFinalizer() {
|
||||
|
|
@ -103,7 +87,7 @@ func (dem *DecryptionErrorMessage) CancelFinalizer() {
|
|||
|
||||
func (dem *DecryptionErrorMessage) Serialize() ([]byte, error) {
|
||||
var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_decryption_error_message_serialize(&serialized, dem.constPtr())
|
||||
signalFfiError := C.signal_decryption_error_message_serialize(&serialized, dem.ptr)
|
||||
runtime.KeepAlive(dem)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -111,19 +95,19 @@ func (dem *DecryptionErrorMessage) Serialize() ([]byte, error) {
|
|||
return CopySignalOwnedBufferToBytes(serialized), nil
|
||||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) GetTimestamp() (uint64, error) {
|
||||
func (dem *DecryptionErrorMessage) GetTimestamp() (time.Time, error) {
|
||||
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.ptr)
|
||||
runtime.KeepAlive(dem)
|
||||
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) {
|
||||
var deviceID C.uint32_t
|
||||
signalFfiError := C.signal_decryption_error_message_get_device_id(&deviceID, dem.constPtr())
|
||||
signalFfiError := C.signal_decryption_error_message_get_device_id(&deviceID, dem.ptr)
|
||||
runtime.KeepAlive(dem)
|
||||
if signalFfiError != nil {
|
||||
return 0, wrapError(signalFfiError)
|
||||
|
|
@ -132,11 +116,11 @@ func (dem *DecryptionErrorMessage) GetDeviceID() (uint32, error) {
|
|||
}
|
||||
|
||||
func (dem *DecryptionErrorMessage) GetRatchetKey() (*PublicKey, error) {
|
||||
var pk C.SignalMutPointerPublicKey
|
||||
signalFfiError := C.signal_decryption_error_message_get_ratchet_key(&pk, dem.constPtr())
|
||||
var pk *C.SignalPublicKey
|
||||
signalFfiError := C.signal_decryption_error_message_get_ratchet_key(&pk, dem.ptr)
|
||||
runtime.KeepAlive(dem)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapPublicKey(pk.raw), nil
|
||||
return wrapPublicKey(pk), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -26,10 +27,6 @@ import (
|
|||
|
||||
type ErrorCode int
|
||||
|
||||
func (e ErrorCode) Error() string {
|
||||
return fmt.Sprintf("libsignalgo.ErrorCode(%d)", int(e))
|
||||
}
|
||||
|
||||
const (
|
||||
ErrorCodeUnknownError ErrorCode = 1
|
||||
ErrorCodeInvalidState ErrorCode = 2
|
||||
|
|
@ -38,7 +35,6 @@ const (
|
|||
ErrorCodeInvalidArgument ErrorCode = 5
|
||||
ErrorCodeInvalidType ErrorCode = 6
|
||||
ErrorCodeInvalidUtf8String ErrorCode = 7
|
||||
ErrorCodeCancelled ErrorCode = 8
|
||||
ErrorCodeProtobufError ErrorCode = 10
|
||||
ErrorCodeLegacyCiphertextVersion ErrorCode = 21
|
||||
ErrorCodeUnknownCiphertextVersion ErrorCode = 22
|
||||
|
|
@ -56,61 +52,9 @@ const (
|
|||
ErrorCodeInvalidRegistrationId ErrorCode = 81
|
||||
ErrorCodeInvalidSession ErrorCode = 82
|
||||
ErrorCodeInvalidSenderKeySession ErrorCode = 83
|
||||
ErrorCodeInvalidProtocolAddress ErrorCode = 84
|
||||
ErrorCodeDuplicatedMessage ErrorCode = 90
|
||||
ErrorCodeCallbackError ErrorCode = 100
|
||||
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 {
|
||||
|
|
@ -122,10 +66,6 @@ func (e *SignalError) Error() string {
|
|||
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 {
|
||||
if signalError == nil {
|
||||
return nil
|
||||
|
|
@ -153,7 +93,7 @@ func wrapError(signalError *C.SignalFfiError) error {
|
|||
|
||||
func wrapSignalError(signalError *C.SignalFfiError, errorType C.uint32_t) error {
|
||||
var messageBytes *C.char
|
||||
getMessageError := C.signal_error_get_message(&messageBytes, signalError)
|
||||
getMessageError := C.signal_error_get_message(signalError, &messageBytes)
|
||||
if getMessageError != nil {
|
||||
// Ignore any errors from this, it will just end up being an empty string.
|
||||
C.signal_error_free(getMessageError)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -42,48 +42,32 @@ func wrapFingerprint(ptr *C.SignalFingerprint) *Fingerprint {
|
|||
}
|
||||
|
||||
func NewFingerprint(iterations, version FingerprintVersion, localIdentifier []byte, localKey *PublicKey, remoteIdentifier []byte, remoteKey *PublicKey) (*Fingerprint, error) {
|
||||
var pa C.SignalMutPointerFingerprint
|
||||
signalFfiError := C.signal_fingerprint_new(
|
||||
&pa,
|
||||
C.uint32_t(iterations),
|
||||
C.uint32_t(version),
|
||||
BytesToBuffer(localIdentifier),
|
||||
localKey.constPtr(),
|
||||
BytesToBuffer(remoteIdentifier),
|
||||
remoteKey.constPtr(),
|
||||
)
|
||||
var pa *C.SignalFingerprint
|
||||
signalFfiError := C.signal_fingerprint_new(&pa, C.uint32_t(iterations), C.uint32_t(version), BytesToBuffer(localIdentifier), localKey.ptr, BytesToBuffer(remoteIdentifier), remoteKey.ptr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapFingerprint(pa.raw), nil
|
||||
}
|
||||
|
||||
func (f *Fingerprint) mutPtr() C.SignalMutPointerFingerprint {
|
||||
return C.SignalMutPointerFingerprint{f.ptr}
|
||||
}
|
||||
|
||||
func (f *Fingerprint) constPtr() C.SignalConstPointerFingerprint {
|
||||
return C.SignalConstPointerFingerprint{f.ptr}
|
||||
return wrapFingerprint(pa), nil
|
||||
}
|
||||
|
||||
func (f *Fingerprint) Clone() (*Fingerprint, error) {
|
||||
var cloned C.SignalMutPointerFingerprint
|
||||
signalFfiError := C.signal_fingerprint_clone(&cloned, f.constPtr())
|
||||
var cloned *C.SignalFingerprint
|
||||
signalFfiError := C.signal_fingerprint_clone(&cloned, f.ptr)
|
||||
runtime.KeepAlive(f)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapFingerprint(cloned.raw), nil
|
||||
return wrapFingerprint(cloned), nil
|
||||
}
|
||||
|
||||
func (f *Fingerprint) Destroy() error {
|
||||
runtime.SetFinalizer(f, nil)
|
||||
return wrapError(C.signal_fingerprint_destroy(f.mutPtr()))
|
||||
return wrapError(C.signal_fingerprint_destroy(f.ptr))
|
||||
}
|
||||
|
||||
func (f *Fingerprint) ScannableEncoding() ([]byte, error) {
|
||||
var scannableEncoding C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_fingerprint_scannable_encoding(&scannableEncoding, f.constPtr())
|
||||
signalFfiError := C.signal_fingerprint_scannable_encoding(&scannableEncoding, f.ptr)
|
||||
runtime.KeepAlive(f)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -93,7 +77,7 @@ func (f *Fingerprint) ScannableEncoding() ([]byte, error) {
|
|||
|
||||
func (f *Fingerprint) DisplayString() (string, error) {
|
||||
var displayString *C.char
|
||||
signalFfiError := C.signal_fingerprint_display_string(&displayString, f.constPtr())
|
||||
signalFfiError := C.signal_fingerprint_display_string(&displayString, f.ptr)
|
||||
runtime.KeepAlive(f)
|
||||
if signalFfiError != nil {
|
||||
return "", wrapError(signalFfiError)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -32,11 +32,11 @@ import (
|
|||
func GroupEncrypt(ctx context.Context, ptext []byte, sender *Address, distributionID uuid.UUID, store SenderKeyStore) (*CiphertextMessage, error) {
|
||||
callbackCtx := NewCallbackContext(ctx)
|
||||
defer callbackCtx.Unref()
|
||||
var ciphertextMessage C.SignalMutPointerCiphertextMessage
|
||||
var ciphertextMessage *C.SignalCiphertextMessage
|
||||
signalFfiError := C.signal_group_encrypt_message(
|
||||
&ciphertextMessage,
|
||||
sender.constPtr(),
|
||||
*(*C.SignalUuid)(unsafe.Pointer(&distributionID)),
|
||||
sender.ptr,
|
||||
(*[C.SignalUUID_LEN]C.uchar)(unsafe.Pointer(&distributionID)),
|
||||
BytesToBuffer(ptext),
|
||||
callbackCtx.wrapSenderKeyStore(store))
|
||||
runtime.KeepAlive(ptext)
|
||||
|
|
@ -44,7 +44,7 @@ func GroupEncrypt(ctx context.Context, ptext []byte, sender *Address, distributi
|
|||
if signalFfiError != nil {
|
||||
return nil, callbackCtx.wrapError(signalFfiError)
|
||||
}
|
||||
return wrapCiphertextMessage(ciphertextMessage.raw), nil
|
||||
return wrapCiphertextMessage(ciphertextMessage), nil
|
||||
}
|
||||
|
||||
func GroupDecrypt(ctx context.Context, ctext []byte, sender *Address, store SenderKeyStore) ([]byte, error) {
|
||||
|
|
@ -53,7 +53,7 @@ func GroupDecrypt(ctx context.Context, ctext []byte, sender *Address, store Send
|
|||
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_group_decrypt_message(
|
||||
&resp,
|
||||
sender.constPtr(),
|
||||
sender.ptr,
|
||||
BytesToBuffer(ctext),
|
||||
callbackCtx.wrapSenderKeyStore(store))
|
||||
runtime.KeepAlive(ctext)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,12 +17,12 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
|
@ -43,19 +42,11 @@ func GenerateRandomness() Randomness {
|
|||
}
|
||||
|
||||
const GroupMasterKeyLength = C.SignalGROUP_MASTER_KEY_LEN
|
||||
const GroupIdentifierLength = C.SignalGROUP_IDENTIFIER_LEN
|
||||
|
||||
type GroupMasterKey [GroupMasterKeyLength]byte
|
||||
type GroupSecretParams [C.SignalGROUP_SECRET_PARAMS_LEN]byte
|
||||
type GroupPublicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]byte
|
||||
type GroupIdentifier [GroupIdentifierLength]byte
|
||||
|
||||
func (gid *GroupIdentifier) String() string {
|
||||
if gid == nil {
|
||||
return ""
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(gid[:])
|
||||
}
|
||||
type GroupIdentifier [C.SignalGROUP_IDENTIFIER_LEN]byte
|
||||
|
||||
type UUIDCiphertext [C.SignalUUID_CIPHERTEXT_LEN]byte
|
||||
type ProfileKeyCiphertext [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]byte
|
||||
|
|
@ -64,22 +55,6 @@ func GenerateGroupSecretParams() (GroupSecretParams, error) {
|
|||
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness())
|
||||
}
|
||||
|
||||
func (gmk GroupMasterKey) GroupIdentifier() (*GroupIdentifier, error) {
|
||||
if groupSecretParams, err := DeriveGroupSecretParamsFromMasterKey(gmk); err != nil {
|
||||
return nil, fmt.Errorf("DeriveGroupSecretParamsFromMasterKey error: %w", err)
|
||||
} else if groupPublicParams, err := groupSecretParams.GetPublicParams(); err != nil {
|
||||
return nil, fmt.Errorf("GetPublicParams error: %w", err)
|
||||
} else if groupIdentifier, err := GetGroupIdentifier(*groupPublicParams); err != nil {
|
||||
return nil, fmt.Errorf("GetGroupIdentifier error: %w", err)
|
||||
} else {
|
||||
return groupIdentifier, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
|
||||
return DeriveGroupSecretParamsFromMasterKey(gmk)
|
||||
}
|
||||
|
||||
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
|
||||
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_generate_deterministic(¶ms, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
|
||||
|
|
@ -164,36 +139,41 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
|
|||
return CopySignalOwnedBufferToBytes(ciphertext), nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
|
||||
func (gsp *GroupSecretParams) DecryptUUID(ciphertextUUID UUIDCiphertext) (uuid.UUID, error) {
|
||||
// TODO this should probably be DecryptServiceID
|
||||
|
||||
u := C.SignalServiceIdFixedWidthBinaryBytes{}
|
||||
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
|
||||
&u,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
|
||||
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextUUID)),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(ciphertextServiceID)
|
||||
runtime.KeepAlive(ciphertextUUID)
|
||||
if signalFfiError != nil {
|
||||
return EmptyServiceID, wrapError(signalFfiError)
|
||||
return uuid.Nil, wrapError(signalFfiError)
|
||||
}
|
||||
|
||||
serviceID := ServiceIDFromCFixedBytes(&u)
|
||||
return serviceID, nil
|
||||
if serviceID.Type != ServiceIDTypeACI {
|
||||
return uuid.Nil, fmt.Errorf("unexpected service ID type %d", serviceID.Type)
|
||||
}
|
||||
return serviceID.UUID, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) {
|
||||
var cipherTextServiceID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
|
||||
func (gsp *GroupSecretParams) EncryptUUID(uuid uuid.UUID) (*UUIDCiphertext, error) {
|
||||
var cipherTextUUID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
|
||||
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
|
||||
&cipherTextServiceID,
|
||||
&cipherTextUUID,
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
|
||||
serviceID.CFixedBytes(),
|
||||
NewACIServiceID(uuid).CFixedBytes(),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
var result UUIDCiphertext
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextServiceID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
|
||||
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextUUID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
|
|
@ -234,17 +214,18 @@ func (gsp *GroupSecretParams) EncryptProfileKey(profileKey ProfileKey, u uuid.UU
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp *ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
|
||||
func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
|
||||
var out C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
randomness := GenerateRandomness()
|
||||
signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
|
||||
&out,
|
||||
C.SignalConstPointerServerPublicParams{spp},
|
||||
(*[C.SignalSERVER_PUBLIC_PARAMS_LEN]C.uchar)(unsafe.Pointer(&spp)),
|
||||
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
|
||||
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
|
||||
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar)(unsafe.Pointer(&credential)),
|
||||
)
|
||||
runtime.KeepAlive(gsp)
|
||||
runtime.KeepAlive(spp)
|
||||
runtime.KeepAlive(credential)
|
||||
runtime.KeepAlive(randomness)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -35,34 +35,22 @@ func wrapHSMEnclaveClient(ptr *C.SignalHsmEnclaveClient) *HSMEnclaveClient {
|
|||
}
|
||||
|
||||
func NewHSMEnclaveClient(trustedPublicKey, trustedCodeHashes []byte) (*HSMEnclaveClient, error) {
|
||||
var cds C.SignalMutPointerHsmEnclaveClient
|
||||
signalFfiError := C.signal_hsm_enclave_client_new(
|
||||
&cds,
|
||||
BytesToBuffer(trustedPublicKey),
|
||||
BytesToBuffer(trustedCodeHashes),
|
||||
)
|
||||
var cds *C.SignalHsmEnclaveClient
|
||||
signalFfiError := C.signal_hsm_enclave_client_new(&cds, BytesToBuffer(trustedPublicKey), BytesToBuffer(trustedCodeHashes))
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapHSMEnclaveClient(cds.raw), nil
|
||||
}
|
||||
|
||||
func (hsm *HSMEnclaveClient) mutPtr() C.SignalMutPointerHsmEnclaveClient {
|
||||
return C.SignalMutPointerHsmEnclaveClient{hsm.ptr}
|
||||
}
|
||||
|
||||
func (hsm *HSMEnclaveClient) constPtr() C.SignalConstPointerHsmEnclaveClient {
|
||||
return C.SignalConstPointerHsmEnclaveClient{hsm.ptr}
|
||||
return wrapHSMEnclaveClient(cds), nil
|
||||
}
|
||||
|
||||
func (hsm *HSMEnclaveClient) Destroy() error {
|
||||
runtime.SetFinalizer(hsm, nil)
|
||||
return wrapError(C.signal_hsm_enclave_client_destroy(hsm.mutPtr()))
|
||||
return wrapError(C.signal_hsm_enclave_client_destroy(hsm.ptr))
|
||||
}
|
||||
|
||||
func (hsm *HSMEnclaveClient) InitialRequest() ([]byte, error) {
|
||||
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_hsm_enclave_client_initial_request(&resp, hsm.constPtr())
|
||||
signalFfiError := C.signal_hsm_enclave_client_initial_request(&resp, hsm.ptr)
|
||||
runtime.KeepAlive(hsm)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -71,7 +59,7 @@ func (hsm *HSMEnclaveClient) InitialRequest() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (hsm *HSMEnclaveClient) CompleteHandshake(handshakeReceived []byte) error {
|
||||
signalFfiError := C.signal_hsm_enclave_client_complete_handshake(hsm.mutPtr(), BytesToBuffer(handshakeReceived))
|
||||
signalFfiError := C.signal_hsm_enclave_client_complete_handshake(hsm.ptr, BytesToBuffer(handshakeReceived))
|
||||
runtime.KeepAlive(hsm)
|
||||
runtime.KeepAlive(handshakeReceived)
|
||||
return wrapError(signalFfiError)
|
||||
|
|
@ -79,7 +67,7 @@ func (hsm *HSMEnclaveClient) CompleteHandshake(handshakeReceived []byte) error {
|
|||
|
||||
func (hsm *HSMEnclaveClient) EstablishedSend(plaintext []byte) ([]byte, error) {
|
||||
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_hsm_enclave_client_established_send(&resp, hsm.mutPtr(), BytesToBuffer(plaintext))
|
||||
signalFfiError := C.signal_hsm_enclave_client_established_send(&resp, hsm.ptr, BytesToBuffer(plaintext))
|
||||
runtime.KeepAlive(hsm)
|
||||
runtime.KeepAlive(plaintext)
|
||||
if signalFfiError != nil {
|
||||
|
|
@ -90,7 +78,7 @@ func (hsm *HSMEnclaveClient) EstablishedSend(plaintext []byte) ([]byte, error) {
|
|||
|
||||
func (hsm *HSMEnclaveClient) EstablishedReceive(ciphertext []byte) ([]byte, error) {
|
||||
var resp C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_hsm_enclave_client_established_recv(&resp, hsm.mutPtr(), BytesToBuffer(ciphertext))
|
||||
signalFfiError := C.signal_hsm_enclave_client_established_recv(&resp, hsm.ptr, BytesToBuffer(ciphertext))
|
||||
runtime.KeepAlive(hsm)
|
||||
runtime.KeepAlive(ciphertext)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -57,23 +57,18 @@ func (i *IdentityKey) Serialize() ([]byte, error) {
|
|||
}
|
||||
|
||||
func DeserializeIdentityKey(bytes []byte) (*IdentityKey, error) {
|
||||
var publicKey C.SignalMutPointerPublicKey
|
||||
var publicKey *C.SignalPublicKey
|
||||
signalFfiError := C.signal_publickey_deserialize(&publicKey, BytesToBuffer(bytes))
|
||||
runtime.KeepAlive(bytes)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &IdentityKey{publicKey: wrapPublicKey(publicKey.raw)}, nil
|
||||
return &IdentityKey{publicKey: wrapPublicKey(publicKey)}, nil
|
||||
}
|
||||
|
||||
func (i *IdentityKey) VerifyAlternateIdentity(other *IdentityKey, signature []byte) (bool, error) {
|
||||
var verify C.bool
|
||||
signalFfiError := C.signal_identitykey_verify_alternate_identity(
|
||||
&verify,
|
||||
i.publicKey.constPtr(),
|
||||
other.publicKey.constPtr(),
|
||||
BytesToBuffer(signature),
|
||||
)
|
||||
signalFfiError := C.signal_identitykey_verify_alternate_identity(&verify, i.publicKey.ptr, other.publicKey.ptr, BytesToBuffer(signature))
|
||||
runtime.KeepAlive(i)
|
||||
runtime.KeepAlive(other)
|
||||
runtime.KeepAlive(signature)
|
||||
|
|
@ -84,7 +79,8 @@ func (i *IdentityKey) VerifyAlternateIdentity(other *IdentityKey, signature []by
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -113,13 +109,14 @@ func GenerateIdentityKeyPair() (*IdentityKeyPair, error) {
|
|||
}
|
||||
|
||||
func DeserializeIdentityKeyPair(bytes []byte) (*IdentityKeyPair, error) {
|
||||
var keys C.SignalPairOfMutPointerPublicKeyMutPointerPrivateKey
|
||||
signalFfiError := C.signal_identitykeypair_deserialize(&keys, BytesToBuffer(bytes))
|
||||
var privateKey *C.SignalPrivateKey
|
||||
var publicKey *C.SignalPublicKey
|
||||
signalFfiError := C.signal_identitykeypair_deserialize(&privateKey, &publicKey, BytesToBuffer(bytes))
|
||||
runtime.KeepAlive(bytes)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return &IdentityKeyPair{publicKey: wrapPublicKey(keys.first.raw), privateKey: wrapPrivateKey(keys.second.raw)}, nil
|
||||
return &IdentityKeyPair{publicKey: wrapPublicKey(publicKey), privateKey: wrapPrivateKey(privateKey)}, nil
|
||||
}
|
||||
|
||||
func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*IdentityKeyPair, error) {
|
||||
|
|
@ -128,11 +125,7 @@ func NewIdentityKeyPair(publicKey *PublicKey, privateKey *PrivateKey) (*Identity
|
|||
|
||||
func (i *IdentityKeyPair) Serialize() ([]byte, error) {
|
||||
var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_identitykeypair_serialize(
|
||||
&serialized,
|
||||
i.publicKey.constPtr(),
|
||||
i.privateKey.constPtr(),
|
||||
)
|
||||
signalFfiError := C.signal_identitykeypair_serialize(&serialized, i.publicKey.ptr, i.privateKey.ptr)
|
||||
runtime.KeepAlive(i)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -146,12 +139,7 @@ func (i *IdentityKeyPair) GetIdentityKey() *IdentityKey {
|
|||
|
||||
func (i *IdentityKeyPair) SignAlternateIdentity(other *IdentityKey) ([]byte, error) {
|
||||
var signature C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_identitykeypair_sign_alternate_identity(
|
||||
&signature,
|
||||
i.publicKey.constPtr(),
|
||||
i.privateKey.constPtr(),
|
||||
other.publicKey.constPtr(),
|
||||
)
|
||||
signalFfiError := C.signal_identitykeypair_sign_alternate_identity(&signature, i.publicKey.ptr, i.privateKey.ptr, other.publicKey.ptr)
|
||||
runtime.KeepAlive(i)
|
||||
runtime.KeepAlive(other)
|
||||
if signalFfiError != nil {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Sumner Evans
|
||||
// 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
|
||||
|
|
@ -18,14 +17,17 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#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_save_identity_key_callback(void *store_ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
|
||||
extern int signal_get_identity_key_callback(void *store_ctx, SignalMutPointerPublicKey *public_keyp, SignalMutPointerProtocolAddress address);
|
||||
extern int signal_is_trusted_identity_callback(void *store_ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
|
||||
extern void signal_destroy_identity_key_store_callback(void *store_ctx);
|
||||
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, SignalPublicKey **public_keyp, const_address *address);
|
||||
extern int signal_is_trusted_identity_callback(void *store_ctx, const_address *address, const_public_key *public_key, unsigned int direction);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
|
|
@ -49,29 +51,22 @@ type IdentityKeyStore interface {
|
|||
}
|
||||
|
||||
//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 {
|
||||
key, err := store.GetIdentityKeyPair(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == nil {
|
||||
keyp.first.raw = nil
|
||||
keyp.second.raw = nil
|
||||
return nil
|
||||
}
|
||||
privClone, err := key.privateKey.Clone()
|
||||
*keyp = nil
|
||||
} else {
|
||||
clone, err := key.privateKey.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pubClone, err := key.publicKey.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
clone.CancelFinalizer()
|
||||
*keyp = clone.ptr
|
||||
}
|
||||
privClone.CancelFinalizer()
|
||||
pubClone.CancelFinalizer()
|
||||
keyp.first.raw = privClone.ptr
|
||||
keyp.second.raw = pubClone.ptr
|
||||
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
|
||||
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t, address C.SignalMutPointerProtocolAddress, publicKey C.SignalMutPointerPublicKey) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
publicKeyStruct := PublicKey{ptr: publicKey.raw}
|
||||
func signal_save_identity_key_callback(storeCtx unsafe.Pointer, address *C.const_address, publicKey *C.const_public_key) C.int {
|
||||
return wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
|
||||
publicKeyStruct := PublicKey{ptr: (*C.SignalPublicKey)(unsafe.Pointer(publicKey))}
|
||||
cloned, err := publicKeyStruct.Clone()
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
replaced, err := store.SaveIdentityKey(
|
||||
ctx,
|
||||
|
|
@ -106,21 +101,20 @@ func signal_save_identity_key_callback(storeCtx unsafe.Pointer, out *C.uint8_t,
|
|||
&IdentityKey{cloned},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return -1, err
|
||||
}
|
||||
if replaced {
|
||||
*out = 1
|
||||
return 1, nil
|
||||
} else {
|
||||
*out = 0
|
||||
return 0, nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//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 {
|
||||
addr := &Address{ptr: address.raw}
|
||||
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
|
||||
theirServiceID, err := addr.NameServiceID()
|
||||
if err != nil {
|
||||
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)
|
||||
if err == nil && key != nil {
|
||||
key.publicKey.CancelFinalizer()
|
||||
public_keyp.raw = key.publicKey.ptr
|
||||
*public_keyp = key.publicKey.ptr
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
//export signal_is_trusted_identity_callback
|
||||
func signal_is_trusted_identity_callback(storeCtx unsafe.Pointer, out *C.bool, address C.SignalMutPointerProtocolAddress, public_key C.SignalMutPointerPublicKey, direction C.uint32_t) C.int {
|
||||
return wrapStoreCallback(storeCtx, func(store IdentityKeyStore, ctx context.Context) error {
|
||||
addr := &Address{ptr: address.raw}
|
||||
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 wrapStoreCallbackCustomReturn(storeCtx, func(store IdentityKeyStore, ctx context.Context) (int, error) {
|
||||
addr := &Address{ptr: (*C.SignalProtocolAddress)(unsafe.Pointer(address))}
|
||||
theirServiceID, err := addr.NameServiceID()
|
||||
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 {
|
||||
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 {
|
||||
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
|
||||
func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) *C.SignalIdentityKeyStore {
|
||||
return &C.SignalIdentityKeyStore{
|
||||
ctx: wrapStore(ctx, store),
|
||||
get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback),
|
||||
get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
|
||||
get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback),
|
||||
save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback),
|
||||
is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback),
|
||||
destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
|
||||
}}
|
||||
get_identity_key_pair: C.SignalGetIdentityKeyPair(C.signal_get_identity_key_pair_callback),
|
||||
get_local_registration_id: C.SignalGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
|
||||
save_identity: C.SignalSaveIdentityKey(C.signal_save_identity_key_callback),
|
||||
get_identity: C.SignalGetIdentityKey(C.signal_get_identity_key_callback),
|
||||
is_trusted_identity: C.SignalIsTrustedIdentity(C.signal_is_trusted_identity_callback),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// mautrix-signal - A Matrix-signal puppeting bridge.
|
||||
// Copyright (C) 2023 Scott Weber
|
||||
// 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
|
||||
|
|
@ -18,6 +17,7 @@
|
|||
package libsignalgo
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
|
||||
#include "./libsignal-ffi.h"
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -52,17 +52,9 @@ func wrapKyberKeyPair(ptr *C.SignalKyberKeyPair) *KyberKeyPair {
|
|||
return kp
|
||||
}
|
||||
|
||||
func (kp *KyberKeyPair) mutPtr() C.SignalMutPointerKyberKeyPair {
|
||||
return C.SignalMutPointerKyberKeyPair{kp.ptr}
|
||||
}
|
||||
|
||||
func (kp *KyberKeyPair) constPtr() C.SignalConstPointerKyberKeyPair {
|
||||
return C.SignalConstPointerKyberKeyPair{kp.ptr}
|
||||
}
|
||||
|
||||
func (kp *KyberKeyPair) Destroy() error {
|
||||
kp.CancelFinalizer()
|
||||
return wrapError(C.signal_kyber_key_pair_destroy(kp.mutPtr()))
|
||||
return wrapError(C.signal_kyber_key_pair_destroy(kp.ptr))
|
||||
}
|
||||
|
||||
func (kp *KyberKeyPair) CancelFinalizer() {
|
||||
|
|
@ -75,17 +67,9 @@ func wrapKyberPublicKey(ptr *C.SignalKyberPublicKey) *KyberPublicKey {
|
|||
return publicKey
|
||||
}
|
||||
|
||||
func (k *KyberPublicKey) mutPtr() C.SignalMutPointerKyberPublicKey {
|
||||
return C.SignalMutPointerKyberPublicKey{k.ptr}
|
||||
}
|
||||
|
||||
func (k *KyberPublicKey) constPtr() C.SignalConstPointerKyberPublicKey {
|
||||
return C.SignalConstPointerKyberPublicKey{k.ptr}
|
||||
}
|
||||
|
||||
func (k *KyberPublicKey) Destroy() error {
|
||||
k.CancelFinalizer()
|
||||
return wrapError(C.signal_kyber_public_key_destroy(k.mutPtr()))
|
||||
return wrapError(C.signal_publickey_destroy(k.ptr))
|
||||
}
|
||||
|
||||
func (k *KyberPublicKey) CancelFinalizer() {
|
||||
|
|
@ -98,17 +82,9 @@ func wrapKyberSecretKey(ptr *C.SignalKyberSecretKey) *KyberSecretKey {
|
|||
return secretKey
|
||||
}
|
||||
|
||||
func (k *KyberSecretKey) mutPtr() C.SignalMutPointerKyberSecretKey {
|
||||
return C.SignalMutPointerKyberSecretKey{k.ptr}
|
||||
}
|
||||
|
||||
func (k *KyberSecretKey) constPtr() C.SignalConstPointerKyberSecretKey {
|
||||
return C.SignalConstPointerKyberSecretKey{k.ptr}
|
||||
}
|
||||
|
||||
func (k *KyberSecretKey) Destroy() error {
|
||||
k.CancelFinalizer()
|
||||
return wrapError(C.signal_kyber_secret_key_destroy(k.mutPtr()))
|
||||
return wrapError(C.signal_kyber_secret_key_destroy(k.ptr))
|
||||
}
|
||||
|
||||
func (k *KyberSecretKey) CancelFinalizer() {
|
||||
|
|
@ -122,18 +98,18 @@ func wrapKyberPreKeyRecord(ptr *C.SignalKyberPreKeyRecord) *KyberPreKeyRecord {
|
|||
}
|
||||
|
||||
func (kp *KyberKeyPair) GetPublicKey() (*KyberPublicKey, error) {
|
||||
var pub C.SignalMutPointerKyberPublicKey
|
||||
signalFfiError := C.signal_kyber_key_pair_get_public_key(&pub, kp.constPtr())
|
||||
var pub *C.SignalKyberPublicKey
|
||||
signalFfiError := C.signal_kyber_key_pair_get_public_key(&pub, kp.ptr)
|
||||
runtime.KeepAlive(kp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPublicKey(pub.raw), nil
|
||||
return wrapKyberPublicKey(pub), nil
|
||||
}
|
||||
|
||||
func (kp *KyberPublicKey) Serialize() ([]byte, error) {
|
||||
var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_kyber_public_key_serialize(&serialized, kp.constPtr())
|
||||
signalFfiError := C.signal_kyber_public_key_serialize(&serialized, kp.ptr)
|
||||
runtime.KeepAlive(kp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -142,63 +118,49 @@ func (kp *KyberPublicKey) Serialize() ([]byte, error) {
|
|||
}
|
||||
|
||||
func DeserializeKyberPublicKey(serialized []byte) (*KyberPublicKey, error) {
|
||||
var kyberPublicKey C.SignalMutPointerKyberPublicKey
|
||||
var kyberPublicKey *C.SignalKyberPublicKey
|
||||
signalFfiError := C.signal_kyber_public_key_deserialize(&kyberPublicKey, BytesToBuffer(serialized))
|
||||
runtime.KeepAlive(serialized)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPublicKey(kyberPublicKey.raw), nil
|
||||
return wrapKyberPublicKey(kyberPublicKey), nil
|
||||
}
|
||||
|
||||
func NewKyberPreKeyRecord(id uint32, timestamp time.Time, keyPair *KyberKeyPair, signature []byte) (*KyberPreKeyRecord, error) {
|
||||
var kpkr C.SignalMutPointerKyberPreKeyRecord
|
||||
signalFfiError := C.signal_kyber_pre_key_record_new(
|
||||
&kpkr,
|
||||
C.uint32_t(id),
|
||||
C.uint64_t(timestamp.UnixMilli()),
|
||||
keyPair.constPtr(),
|
||||
BytesToBuffer(signature),
|
||||
)
|
||||
var kpkr *C.SignalKyberPreKeyRecord
|
||||
signalFfiError := C.signal_kyber_pre_key_record_new(&kpkr, C.uint32_t(id), C.uint64_t(timestamp.UnixMilli()), keyPair.ptr, BytesToBuffer(signature))
|
||||
runtime.KeepAlive(keyPair)
|
||||
runtime.KeepAlive(signature)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPreKeyRecord(kpkr.raw), nil
|
||||
return wrapKyberPreKeyRecord(kpkr), nil
|
||||
}
|
||||
|
||||
func DeserializeKyberPreKeyRecord(serialized []byte) (*KyberPreKeyRecord, error) {
|
||||
var kpkr C.SignalMutPointerKyberPreKeyRecord
|
||||
var kpkr *C.SignalKyberPreKeyRecord
|
||||
signalFfiError := C.signal_kyber_pre_key_record_deserialize(&kpkr, BytesToBuffer(serialized))
|
||||
runtime.KeepAlive(serialized)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPreKeyRecord(kpkr.raw), nil
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) mutPtr() C.SignalMutPointerKyberPreKeyRecord {
|
||||
return C.SignalMutPointerKyberPreKeyRecord{kpkr.ptr}
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) constPtr() C.SignalConstPointerKyberPreKeyRecord {
|
||||
return C.SignalConstPointerKyberPreKeyRecord{kpkr.ptr}
|
||||
return wrapKyberPreKeyRecord(kpkr), nil
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) Clone() (*KyberPreKeyRecord, error) {
|
||||
var cloned C.SignalMutPointerKyberPreKeyRecord
|
||||
signalFfiError := C.signal_kyber_pre_key_record_clone(&cloned, kpkr.constPtr())
|
||||
var cloned *C.SignalKyberPreKeyRecord
|
||||
signalFfiError := C.signal_kyber_pre_key_record_clone(&cloned, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPreKeyRecord(cloned.raw), nil
|
||||
return wrapKyberPreKeyRecord(cloned), nil
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) Destroy() error {
|
||||
kpkr.CancelFinalizer()
|
||||
return wrapError(C.signal_kyber_pre_key_record_destroy(kpkr.mutPtr()))
|
||||
return wrapError(C.signal_kyber_pre_key_record_destroy(kpkr.ptr))
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) CancelFinalizer() {
|
||||
|
|
@ -207,7 +169,7 @@ func (kpkr *KyberPreKeyRecord) CancelFinalizer() {
|
|||
|
||||
func (kpkr *KyberPreKeyRecord) Serialize() ([]byte, error) {
|
||||
var serialized C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_kyber_pre_key_record_serialize(&serialized, kpkr.constPtr())
|
||||
signalFfiError := C.signal_kyber_pre_key_record_serialize(&serialized, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -217,7 +179,7 @@ func (kpkr *KyberPreKeyRecord) Serialize() ([]byte, error) {
|
|||
|
||||
func (kpkr *KyberPreKeyRecord) GetSignature() ([]byte, error) {
|
||||
var signature C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_signature(&signature, kpkr.constPtr())
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_signature(&signature, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
|
|
@ -227,7 +189,7 @@ func (kpkr *KyberPreKeyRecord) GetSignature() ([]byte, error) {
|
|||
|
||||
func (kpkr *KyberPreKeyRecord) GetID() (uint32, error) {
|
||||
var id C.uint32_t
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_id(&id, kpkr.constPtr())
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_id(&id, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return 0, wrapError(signalFfiError)
|
||||
|
|
@ -237,7 +199,7 @@ func (kpkr *KyberPreKeyRecord) GetID() (uint32, error) {
|
|||
|
||||
func (kpkr *KyberPreKeyRecord) GetTimestamp() (time.Time, error) {
|
||||
var ts C.uint64_t
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_timestamp(&ts, kpkr.constPtr())
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_timestamp(&ts, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return time.Time{}, wrapError(signalFfiError)
|
||||
|
|
@ -246,30 +208,30 @@ func (kpkr *KyberPreKeyRecord) GetTimestamp() (time.Time, error) {
|
|||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) GetPublicKey() (*KyberPublicKey, error) {
|
||||
var pub C.SignalMutPointerKyberPublicKey
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_public_key(&pub, kpkr.constPtr())
|
||||
var pub *C.SignalKyberPublicKey
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_public_key(&pub, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberPublicKey(pub.raw), nil
|
||||
return wrapKyberPublicKey(pub), nil
|
||||
}
|
||||
|
||||
func (kpkr *KyberPreKeyRecord) GetSecretKey() (*KyberSecretKey, error) {
|
||||
var sec C.SignalMutPointerKyberSecretKey
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_secret_key(&sec, kpkr.constPtr())
|
||||
var sec *C.SignalKyberSecretKey
|
||||
signalFfiError := C.signal_kyber_pre_key_record_get_secret_key(&sec, kpkr.ptr)
|
||||
runtime.KeepAlive(kpkr)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberSecretKey(sec.raw), nil
|
||||
return wrapKyberSecretKey(sec), nil
|
||||
}
|
||||
|
||||
func KyberKeyPairGenerate() (*KyberKeyPair, error) {
|
||||
var kp C.SignalMutPointerKyberKeyPair
|
||||
var kp *C.SignalKyberKeyPair
|
||||
signalFfiError := C.signal_kyber_key_pair_generate(&kp)
|
||||
if signalFfiError != nil {
|
||||
return nil, wrapError(signalFfiError)
|
||||
}
|
||||
return wrapKyberKeyPair(kp.raw), nil
|
||||
return wrapKyberKeyPair(kp), nil
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue