diff --git a/.gitignore b/.gitignore
index 0031287..ab00deb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,5 @@
*.log*
/mautrix-signal
-/mautrix-signalgo
-/mautrix-signal-v2
/start
/libsignal_ffi.a
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 03bd8f5..952dabc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,17 +1,12 @@
include:
-- project: 'mautrix/ci'
- file: '/go.yml'
- project: 'mautrix/ci'
file: '/gov2.yml'
variables:
BUILDER_IMAGE: dock.mau.dev/tulir/gomuks-build-docker/signal
+ BINARY_NAME_V2: mautrix-signal
# 32-bit arm builds aren't supported
-build arm:
- rules:
- - when: never
-
build arm v2:
rules:
- when: never
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b22a061..8a667b3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v4.6.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
@@ -18,11 +18,11 @@ repos:
- "go.mau.fi/mautrix-signal"
- "-w"
- id: go-vet-mod
- #- id: go-staticcheck-repo-mod
+# - id: go-staticcheck-repo-mod
# TODO: reenable this and fix all the problems
- repo: https://github.com/beeper/pre-commit-go
- rev: v0.3.0
+ rev: v0.3.1
hooks:
- id: zerolog-ban-msgf
- id: zerolog-use-stringer
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f00f374..f0c11f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# v0.7.0 (unreleased)
+
+* Updated to libsignal v0.54.0.
+* Rewrote bridge using bridgev2 architecture.
+ * It is recommended to check the config file after upgrading.
+
# v0.6.3 (2024-07-16)
* Updated to libsignal v0.52.0.
diff --git a/Dockerfile.ci b/Dockerfile.ci
deleted file mode 100644
index e75845e..0000000
--- a/Dockerfile.ci
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM alpine:3.19
-
-ENV UID=1337 \
- GID=1337
-
-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
-VOLUME /data
-
-CMD ["/docker-run.sh"]
diff --git a/Makefile b/Makefile
deleted file mode 100644
index b497635..0000000
--- a/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-.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)
diff --git a/ROADMAP.md b/ROADMAP.md
index 85c7494..9c0cedb 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,17 +1,17 @@
# Features & roadmap
* Matrix → Signal
- * [ ] Message content
+ * [x] Message content
* [x] Text
* [x] Formatting
* [x] Mentions
- * [ ] Media
+ * [x] Media
* [x] Images
* [x] Audio files
* [x] Voice messages
* [x] Files
* [x] Gifs
- * [ ] Locations
+ * [x] Locations
* [x] Stickers
* [x] Message edits
* [x] Message reactions
@@ -22,9 +22,9 @@
* [x] Topic
* [ ] Membership actions
* [ ] Join (accepting invites)
- * [x] Invite
- * [x] Leave
- * [x] Kick/Ban/Unban
+ * [ ] Invite
+ * [ ] Leave
+ * [ ] Kick/Ban/Unban
* [x] Group permissions
* [x] Typing notifications
* [x] Read receipts
@@ -70,5 +70,5 @@
* [x] When receiving message
* [x] Linking as secondary device
* [ ] Registering as primary device
- * [x] Private chat/group creation by inviting Matrix puppet of Signal user to new room
+ * [ ] Private chat/group creation by inviting Matrix puppet of Signal user to new room
* [x] Option to use own Matrix account for messages sent from other Signal clients
diff --git a/build-go-v2.sh b/build-go-v2.sh
deleted file mode 100755
index 199f474..0000000
--- a/build-go-v2.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
-GO_LDFLAGS="-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
-if [ "$DBG" = 1 ]; then
- GO_GCFLAGS='all=-N -l'
-else
- GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
-fi
-go build -gcflags="$GO_GCFLAGS" -ldflags="$GO_LDFLAGS" -o mautrix-signal-v2 ./cmd/mautrix-signal-v2 "$@"
diff --git a/build-go.sh b/build-go.sh
index ecf8907..faaac6f 100755
--- a/build-go.sh
+++ b/build-go.sh
@@ -1,9 +1,9 @@
#!/bin/sh
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'"
+GO_LDFLAGS="-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
if [ "$DBG" = 1 ]; then
GO_GCFLAGS='all=-N -l'
else
GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
fi
-go build -gcflags="$GO_GCFLAGS" -ldflags="$GO_LDFLAGS" -o mautrix-signal "$@"
+go build -gcflags="$GO_GCFLAGS" -ldflags="$GO_LDFLAGS" -o mautrix-signal ./cmd/mautrix-signal "$@"
diff --git a/build-v2.sh b/build-v2.sh
index b541932..2b51084 100755
--- a/build-v2.sh
+++ b/build-v2.sh
@@ -1,4 +1,4 @@
#!/bin/sh
-#./build-rust.sh
-#cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
-LIBRARY_PATH=.:$LIBRARY_PATH ./build-go-v2.sh
+./build-rust.sh
+cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
+LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh
diff --git a/build.sh b/build.sh
deleted file mode 100755
index b17ad4f..0000000
--- a/build.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-git submodule init
-git submodule update
-make
diff --git a/cmd/mautrix-signal-v2/legacymigrate.go b/cmd/mautrix-signal/legacymigrate.go
similarity index 100%
rename from cmd/mautrix-signal-v2/legacymigrate.go
rename to cmd/mautrix-signal/legacymigrate.go
diff --git a/cmd/mautrix-signal-v2/legacymigrate.sql b/cmd/mautrix-signal/legacymigrate.sql
similarity index 100%
rename from cmd/mautrix-signal-v2/legacymigrate.sql
rename to cmd/mautrix-signal/legacymigrate.sql
diff --git a/cmd/mautrix-signal-v2/legacyprovision.go b/cmd/mautrix-signal/legacyprovision.go
similarity index 69%
rename from cmd/mautrix-signal-v2/legacyprovision.go
rename to cmd/mautrix-signal/legacyprovision.go
index b3c3f80..8e8e752 100644
--- a/cmd/mautrix-signal-v2/legacyprovision.go
+++ b/cmd/mautrix-signal/legacyprovision.go
@@ -28,8 +28,8 @@ import (
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
+ "maunium.net/go/mautrix/id"
- "go.mau.fi/mautrix-signal/legacyprovision"
"go.mau.fi/mautrix-signal/pkg/connector"
)
@@ -54,7 +54,7 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
user := m.Matrix.Provisioning.GetUser(r)
defLogin := user.GetDefaultLogin()
if defLogin != nil && defLogin.Client != nil && defLogin.Client.IsLoggedIn() {
- legacyprovision.JSONResponse(w, http.StatusConflict, &legacyprovision.Error{
+ JSONResponse(w, http.StatusConflict, &Error{
Error: "Already logged in",
ErrCode: "FI.MAU.ALREADY_LOGGED_IN",
})
@@ -64,7 +64,7 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
login, err := m.Connector.CreateLogin(r.Context(), user, "qr")
if err != nil {
log.Err(err).Msg("Failed to create login")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, &Error{
Error: "Internal error starting login",
ErrCode: "M_UNKNOWN",
})
@@ -73,14 +73,14 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
firstStep, err := login.Start(r.Context())
if err != nil {
log.Err(err).Msg("Failed to start login")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, &Error{
Error: "Internal error starting login",
ErrCode: "M_UNKNOWN",
})
return
} else if firstStep.StepID != connector.LoginStepQR || firstStep.Type != bridgev2.LoginStepTypeDisplayAndWait || firstStep.DisplayAndWaitParams.Type != bridgev2.LoginDisplayTypeQR {
log.Error().Any("first_step", firstStep).Msg("Unexpected first step")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, &Error{
Error: "Unexpected first login step",
ErrCode: "M_UNKNOWN",
})
@@ -93,7 +93,7 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
User: user,
}
loginSessionsLock.Unlock()
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
+ JSONResponse(w, http.StatusOK, Response{
Success: true,
Status: "provisioning_url_received",
SessionID: strconv.Itoa(int(handleID)),
@@ -102,10 +102,10 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
}
func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess {
- var body legacyprovision.LinkWaitForAccountRequest
+ var body LinkWaitForAccountRequest
err := json.NewDecoder(r.Body).Decode(&body)
if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
+ JSONResponse(w, http.StatusBadRequest, Error{
Success: false,
Error: "Error decoding JSON body",
ErrCode: mautrix.MBadJSON.ErrCode,
@@ -114,7 +114,7 @@ func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess
}
sessionID, err := strconv.Atoi(body.SessionID)
if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
+ JSONResponse(w, http.StatusBadRequest, Error{
Success: false,
Error: "Error decoding session ID in JSON body",
ErrCode: mautrix.MBadJSON.ErrCode,
@@ -124,7 +124,7 @@ func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess
process, ok := loginSessions[uint32(sessionID)]
user := m.Matrix.Provisioning.GetUser(r)
if !ok || process.User != user {
- legacyprovision.JSONResponse(w, http.StatusNotFound, legacyprovision.Error{
+ JSONResponse(w, http.StatusNotFound, Error{
Success: false,
Error: "No session found",
ErrCode: mautrix.MNotFound.ErrCode,
@@ -142,7 +142,7 @@ func legacyProvLinkWaitScan(w http.ResponseWriter, r *http.Request) {
res, err := login.Login.(bridgev2.LoginProcessDisplayAndWait).Wait(r.Context())
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to log in")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, Error{
Error: "Failed to log in",
ErrCode: "M_UNKNOWN",
})
@@ -150,14 +150,14 @@ func legacyProvLinkWaitScan(w http.ResponseWriter, r *http.Request) {
return
} else if res.StepID != connector.LoginStepProcess {
zerolog.Ctx(r.Context()).Error().Any("first_step", res).Msg("Unexpected login step")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, Error{
Error: "Unexpected login step",
ErrCode: "M_UNKNOWN",
})
login.Delete()
return
}
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
+ JSONResponse(w, http.StatusOK, Response{
Success: true,
Status: "provisioning_data_received",
})
@@ -171,18 +171,18 @@ func legacyProvLinkWaitAccount(w http.ResponseWriter, r *http.Request) {
res, err := login.Login.(bridgev2.LoginProcessDisplayAndWait).Wait(r.Context())
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to log in")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, Error{
Error: "Failed to log in",
ErrCode: "M_UNKNOWN",
})
} else if res.StepID != connector.LoginStepComplete || res.Type != bridgev2.LoginStepTypeComplete {
zerolog.Ctx(r.Context()).Error().Any("first_step", res).Msg("Unexpected login step")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, Error{
Error: "Unexpected login step",
ErrCode: "M_UNKNOWN",
})
} else {
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
+ JSONResponse(w, http.StatusOK, Response{
Success: true,
Status: "prekeys_registered",
UUID: string(res.CompleteParams.UserLogin.ID),
@@ -194,7 +194,7 @@ func legacyProvLinkWaitAccount(w http.ResponseWriter, r *http.Request) {
func legacyProvLogout(w http.ResponseWriter, r *http.Request) {
// No-op for backwards compatibility
- legacyprovision.JSONResponse(w, http.StatusOK, nil)
+ JSONResponse(w, http.StatusOK, nil)
}
func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request, create bool) {
@@ -206,21 +206,21 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
resp, err := api.ResolveIdentifier(r.Context(), mux.Vars(r)["phonenum"], create)
if err != nil {
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to resolve identifier")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &legacyprovision.Error{
+ JSONResponse(w, http.StatusInternalServerError, &Error{
Error: fmt.Sprintf("Failed to resolve identifier: %v", err),
ErrCode: "M_UNKNOWN",
})
return
} else if resp == nil {
- legacyprovision.JSONResponse(w, http.StatusNotFound, &legacyprovision.Error{
+ JSONResponse(w, http.StatusNotFound, &Error{
ErrCode: mautrix.MNotFound.ErrCode,
Error: "User not found on Signal",
})
return
}
status := http.StatusOK
- apiResp := &legacyprovision.ResolveIdentifierResponse{
- ChatID: legacyprovision.ResolveIdentifierResponseChatID{
+ apiResp := &ResolveIdentifierResponse{
+ ChatID: ResolveIdentifierResponseChatID{
UUID: string(resp.UserID),
Number: "",
},
@@ -229,7 +229,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
if resp.UserInfo != nil {
resp.Ghost.UpdateInfo(r.Context(), resp.UserInfo)
}
- apiResp.OtherUser = &legacyprovision.ResolveIdentifierResponseOtherUser{
+ apiResp.OtherUser = &ResolveIdentifierResponseOtherUser{
MXID: resp.Ghost.Intent.GetMXID(),
DisplayName: resp.Ghost.Name,
AvatarURL: resp.Ghost.AvatarMXC.ParseOrIgnore(),
@@ -240,7 +240,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
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")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
+ JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
Err: "Failed to get portal",
ErrCode: "M_UNKNOWN",
})
@@ -253,7 +253,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
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")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
+ JSONResponse(w, http.StatusInternalServerError, &mautrix.RespError{
Err: "Failed to create portal room",
ErrCode: "M_UNKNOWN",
})
@@ -262,7 +262,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
}
apiResp.RoomID = resp.Chat.Portal.MXID
}
- legacyprovision.JSONResponse(w, status, &legacyprovision.Response{
+ JSONResponse(w, status, &Response{
Success: true,
Status: "ok",
ResolveIdentifierResponse: apiResp,
@@ -276,3 +276,71 @@ func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
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 LinkNew
+ SessionID string `json:"session_id,omitempty"`
+ URI string `json:"uri,omitempty"`
+
+ // For response in LinkWaitForAccount
+ UUID string `json:"uuid,omitempty"`
+ Number string `json:"number,omitempty"`
+
+ // For response in ResolveIdentifier
+ *ResolveIdentifierResponse
+}
+
+type WhoAmIResponse struct {
+ Permissions int `json:"permissions"`
+ MXID string `json:"mxid"`
+ Signal *WhoAmIResponseSignal `json:"signal,omitempty"`
+}
+
+type WhoAmIResponseSignal struct {
+ Number string `json:"number"`
+ UUID string `json:"uuid"`
+ Name string `json:"name"`
+ Ok bool `json:"ok"`
+}
+
+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"`
+}
+
+type LinkWaitForScanRequest struct {
+ SessionID string `json:"session_id"`
+}
+
+type LinkWaitForAccountRequest struct {
+ SessionID string `json:"session_id"`
+ DeviceName string `json:"device_name"` // TODO this seems to not be used anywhere
+}
diff --git a/cmd/mautrix-signal-v2/main.go b/cmd/mautrix-signal/main.go
similarity index 100%
rename from cmd/mautrix-signal-v2/main.go
rename to cmd/mautrix-signal/main.go
diff --git a/commands.go b/commands.go
deleted file mode 100644
index 7600d66..0000000
--- a/commands.go
+++ /dev/null
@@ -1,1161 +0,0 @@
-// 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 .
-
-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,
- cmdSync,
- cmdDeleteSession,
- cmdSetRelay,
- cmdUnsetRelay,
- cmdDeletePortal,
- cmdDeleteAllPortals,
- cmdCleanupLostPortals,
- cmdInviteLink,
- cmdResetInviteLink,
- cmdCreate,
- cmdInvite,
- cmdListInvited,
- cmdRevokeInvite,
- )
-}
-
-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: "",
- },
- RequiresLogin: true,
-}
-
-func fnSetDeviceName(ce *WrappedCommandEvent) {
- if len(ce.Args) == 0 {
- ce.Reply("**Usage:** `set-device-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 `")
- 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 cmdInvite = &commands.FullHandler{
- Func: wrapCommand(fnInvite),
- Name: "invite",
- Help: commands.HelpMeta{
- Section: HelpSectionPortalManagement,
- Description: "Invite a user by phone number.",
- Args: "<_international phone number_>",
- },
- RequiresLogin: true,
- RequiresPortal: true,
-}
-
-func fnInvite(ce *WrappedCommandEvent) {
- if len(ce.Args) == 0 {
- ce.Reply("**Usage:** `invite `")
- 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 Invite target user")
-
- var groupChange signalmeow.GroupChange
- if aci != uuid.Nil {
- groupChange.AddMembers = []*signalmeow.AddMember{
- {
- GroupMember: signalmeow.GroupMember{
- ACI: aci,
- Role: signalmeow.GroupMember_DEFAULT,
- },
- },
- }
- } else {
- groupChange.AddPendingMembers = []*signalmeow.PendingMember{
- {
- ServiceID: libsignalgo.NewPNIServiceID(pni),
- AddedByUserID: ce.User.SignalID,
- Role: signalmeow.GroupMember_DEFAULT,
- },
- }
- }
- revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &groupChange, ce.Portal.GroupID())
- if err != nil {
- ce.Reply("Failed to update group: %w", err)
- return
- }
- ce.Portal.Revision = revision
- if aci != uuid.Nil {
- group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), revision)
- if err != nil {
- ce.Reply("Failed to fetch group after invite: %w", err)
- }
- ce.Portal.SyncParticipants(ce.Ctx, user, group)
- ce.Portal.Update(ce.Ctx)
- return
- }
- ce.Portal.Update(ce.Ctx)
- ce.Reply("Invited " + e164)
-}
-
-var cmdListInvited = &commands.FullHandler{
- Func: wrapCommand(fnListInvited),
- Name: "list-invited",
- Help: commands.HelpMeta{
- Section: HelpSectionPortalManagement,
- Description: "list pending invites",
- Args: "<_international phone number_>",
- },
- RequiresLogin: true,
- RequiresPortal: true,
-}
-
-func fnListInvited(ce *WrappedCommandEvent) {
- group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), ce.Portal.Revision)
- if err != nil {
- ce.Reply("Failed to fetch group info: %w", err)
- return
- }
- var memberList []string
- for _, pendingMember := range group.PendingMembers {
- recipientString, err := pendingMemberToString(ce.Ctx, ce.User, pendingMember)
- if err != nil {
- ce.Reply("Failed to fetch recipient for %s: %w", pendingMember.ServiceID, err)
- continue
- }
- memberList = append(memberList, recipientString)
- }
- if len(memberList) == 0 {
- ce.Reply("No pending Invites")
- } else {
- ce.Reply(strings.Join(memberList, "\n"))
- }
-}
-
-func pendingMemberToString(ctx context.Context, user *User, pendingMember *signalmeow.PendingMember) (string, error) {
- var pni, aci uuid.UUID
- if pendingMember.ServiceID.Type == libsignalgo.ServiceIDTypeACI {
- aci = pendingMember.ServiceID.UUID
- } else {
- pni = pendingMember.ServiceID.UUID
- }
- recipient, err := user.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
- if err != nil {
- return "", err
- }
- if recipient.E164 != "" {
- return recipient.E164, nil
- } else {
- return "Unidentified User", nil
- }
-}
-
-var cmdRevokeInvite = &commands.FullHandler{
- Func: wrapCommand(fnRevokeInvite),
- Name: "revoke-invite",
- Help: commands.HelpMeta{
- Section: HelpSectionPortalManagement,
- Description: "Revoke an invite by phone number.",
- Args: "<_international phone number_>",
- },
- RequiresLogin: true,
- RequiresPortal: true,
-}
-
-func fnRevokeInvite(ce *WrappedCommandEvent) {
- if len(ce.Args) == 0 {
- ce.Reply("**Usage:** `RevokeInvite `")
- return
- }
- e164 := "+" + numberCleaner.Replace(strings.Join(ce.Args, ""))
-
- user := ce.User
- var serviceID libsignalgo.ServiceID
- group, err := ce.User.Client.RetrieveGroupByID(ce.Ctx, ce.Portal.GroupID(), ce.Portal.Revision)
- if err != nil {
- ce.Reply("Failed to fetch group info: %w", err)
- return
- }
- var pni, aci uuid.UUID
- for _, pendingMember := range group.PendingMembers {
- if pendingMember.ServiceID.Type == libsignalgo.ServiceIDTypeACI {
- aci = pendingMember.ServiceID.UUID
- } else {
- pni = pendingMember.ServiceID.UUID
- }
- recipient, err := user.Client.Store.RecipientStore.LoadAndUpdateRecipient(ce.Ctx, aci, pni, nil)
- if err != nil {
- continue
- }
- if recipient.E164 == e164 {
- serviceID = pendingMember.ServiceID
- break
- }
- }
- if serviceID.UUID == uuid.Nil {
- ce.Reply("User not in Group")
- return
- }
- var groupChange signalmeow.GroupChange
- groupChange.DeletePendingMembers = []*libsignalgo.ServiceID{&serviceID}
- revision, err := ce.User.Client.UpdateGroup(ce.Ctx, &groupChange, ce.Portal.GroupID())
- if err != nil {
- ce.Reply("Failed to update group: %w", err)
- return
- }
- if aci != uuid.Nil {
- target := ce.Bridge.GetPuppetBySignalID(aci)
- if target != nil {
- ce.Bot.SendCustomMembershipEvent(ce.Ctx, ce.Portal.MXID, target.IntentFor(ce.Portal).UserID, event.MembershipLeave, "removed by "+user.MXID.String())
- }
- }
- ce.Portal.Revision = revision
- ce.Portal.Update(ce.Ctx)
- ce.Reply("Revoked the invitation for " + e164)
-}
-
-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: "",
- },
- 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 cmdSync = &commands.FullHandler{
- Func: wrapCommand(fnSync),
- Name: "sync",
- Help: commands.HelpMeta{
- Section: HelpSectionMiscellaneous,
- Description: "Synchronize Signal bridge data",
- Args: "",
- },
- RequiresLogin: true,
-}
-
-func fnSync(ce *WrappedCommandEvent) {
- args := strings.ToLower(strings.Join(ce.Args, " "))
- space := strings.Contains(args, "space")
- groups := strings.Contains(args, "groups")
- if !space && !groups {
- ce.Reply("**Usage:** `sync `")
- return
- }
- if !ce.Bridge.Config.Bridge.PersonalFilteringSpaces && space {
- 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
- }
- allPortals := ce.Bridge.GetAllPortalsWithMXID()
- if space {
- count := 0
- 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)
- }
- if groups {
- count := 0
- for _, portal := range allPortals {
- if portal.IsPrivateChat() {
- continue
- }
- if ce.Bridge.StateStore.IsInRoom(ctx, portal.MXID, ce.User.MXID) {
- groupInfo := portal.UpdateGroupInfo(ce.Ctx, ce.User, nil, 0, true)
- if groupInfo != nil {
- members := portal.SyncParticipants(ctx, ce.User, groupInfo)
- portal.updatePowerLevelsAndJoinRule(ctx, groupInfo, members)
- count++
- }
- }
- }
- plural := "s"
- if count == 1 {
- plural = ""
- }
- ce.Reply("Synced %d group%s", 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: %s
", 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
- avatarSet := false
- if ok {
- roomAvatarEvent.Content.ParseRaw(event.StateRoomAvatar)
- avatarURL = roomAvatarEvent.Content.AsRoomAvatar().URL.ParseOrIgnore()
- 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})
- avatarSet = true
- }
- }
- 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{
- ACI: uuid,
- Role: role,
- })
- } else if membership == event.MembershipBan {
- bannedMembers = append(bannedMembers, &signalmeow.BannedMember{
- ServiceID: libsignalgo.NewACIServiceID(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.CreateGroup(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 != nil && 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
- }
- portal.Revision = group.Revision
- portal.AvatarHash = avatarHash
- portal.AvatarURL = avatarURL
- portal.AvatarPath = group.AvatarPath
- portal.AvatarSet = avatarSet
- 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())
-}
diff --git a/config/bridge.go b/config/bridge.go
deleted file mode 100644
index e3e756c..0000000
--- a/config/bridge.go
+++ /dev/null
@@ -1,241 +0,0 @@
-// 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 .
-
-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"`
- LocationFormat string `yaml:"location_format"`
- 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
-}
diff --git a/config/config.go b/config/config.go
deleted file mode 100644
index de62c82..0000000
--- a/config/config.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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 .
-
-package config
-
-import (
- "maunium.net/go/mautrix/bridge/bridgeconfig"
- "maunium.net/go/mautrix/id"
-)
-
-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
-}
diff --git a/config/upgrade.go b/config/upgrade.go
deleted file mode 100644
index 219b7fc..0000000
--- a/config/upgrade.go
+++ /dev/null
@@ -1,167 +0,0 @@
-// 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 .
-
-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.Str, "bridge", "location_format")
- 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"},
-}
diff --git a/custompuppet.go b/custompuppet.go
deleted file mode 100644
index cde3059..0000000
--- a/custompuppet.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// 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 .
-
-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")
- }
-}
diff --git a/database/database.go b/database/database.go
deleted file mode 100644
index daa365f..0000000
--- a/database/database.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// 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 .
-
-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)},
- }
-}
diff --git a/database/disappearingmessage.go b/database/disappearingmessage.go
deleted file mode 100644
index b32a8ee..0000000
--- a/database/disappearingmessage.go
+++ /dev/null
@@ -1,125 +0,0 @@
-// 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 .
-
-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)
-}
diff --git a/database/lostportal.go b/database/lostportal.go
deleted file mode 100644
index 29779b8..0000000
--- a/database/lostportal.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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 .
-
-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)
-}
diff --git a/database/message.go b/database/message.go
deleted file mode 100644
index 5184bd8..0000000
--- a/database/message.go
+++ /dev/null
@@ -1,179 +0,0 @@
-// 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 .
-
-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)
-}
diff --git a/database/portal.go b/database/portal.go
deleted file mode 100644
index 786f084..0000000
--- a/database/portal.go
+++ /dev/null
@@ -1,206 +0,0 @@
-// 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 .
-
-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)
-}
diff --git a/database/puppet.go b/database/puppet.go
deleted file mode 100644
index 99a65ff..0000000
--- a/database/puppet.go
+++ /dev/null
@@ -1,158 +0,0 @@
-// 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 .
-
-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()...)
-}
diff --git a/database/reaction.go b/database/reaction.go
deleted file mode 100644
index fc5228e..0000000
--- a/database/reaction.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// 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 .
-
-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)
-}
diff --git a/database/upgrades/00-latest.sql b/database/upgrades/00-latest.sql
deleted file mode 100644
index 4dec3ff..0000000
--- a/database/upgrades/00-latest.sql
+++ /dev/null
@@ -1,116 +0,0 @@
--- 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
-);
diff --git a/database/upgrades/13-upgrade-mx-state-store.sql b/database/upgrades/13-upgrade-mx-state-store.sql
deleted file mode 100644
index ec1b6c2..0000000
--- a/database/upgrades/13-upgrade-mx-state-store.sql
+++ /dev/null
@@ -1,18 +0,0 @@
--- 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;
diff --git a/database/upgrades/14-remove-notice-room.sql b/database/upgrades/14-remove-notice-room.sql
deleted file mode 100644
index 9ad0692..0000000
--- a/database/upgrades/14-remove-notice-room.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- 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;
diff --git a/database/upgrades/15-remove-unused-puppet-columns.sql b/database/upgrades/15-remove-unused-puppet-columns.sql
deleted file mode 100644
index 925c3a8..0000000
--- a/database/upgrades/15-remove-unused-puppet-columns.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- v15: Remove unused columns in puppet table
-ALTER TABLE puppet DROP COLUMN next_batch;
-ALTER TABLE puppet DROP COLUMN base_url;
diff --git a/database/upgrades/16-refactor-postgres.sql b/database/upgrades/16-refactor-postgres.sql
deleted file mode 100644
index 9ab94e8..0000000
--- a/database/upgrades/16-refactor-postgres.sql
+++ /dev/null
@@ -1,123 +0,0 @@
--- 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;
diff --git a/database/upgrades/17-refactor-sqlite.sql b/database/upgrades/17-refactor-sqlite.sql
deleted file mode 100644
index f55dc0b..0000000
--- a/database/upgrades/17-refactor-sqlite.sql
+++ /dev/null
@@ -1,198 +0,0 @@
--- 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;
diff --git a/database/upgrades/18-spaces.sql b/database/upgrades/18-spaces.sql
deleted file mode 100644
index 24248ba..0000000
--- a/database/upgrades/18-spaces.sql
+++ /dev/null
@@ -1,17 +0,0 @@
--- 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
-);
diff --git a/database/upgrades/19-more-portal-metadata.sql b/database/upgrades/19-more-portal-metadata.sql
deleted file mode 100644
index 6230b21..0000000
--- a/database/upgrades/19-more-portal-metadata.sql
+++ /dev/null
@@ -1,5 +0,0 @@
--- 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 '';
diff --git a/database/upgrades/20-puppet-profile-fetch-ts.sql b/database/upgrades/20-puppet-profile-fetch-ts.sql
deleted file mode 100644
index b398b2f..0000000
--- a/database/upgrades/20-puppet-profile-fetch-ts.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- v20 (compatible with v17+): Add profile fetch timestamp for puppets
-ALTER TABLE puppet ADD profile_fetched_at BIGINT;
diff --git a/database/user.go b/database/user.go
deleted file mode 100644
index aa90686..0000000
--- a/database/user.go
+++ /dev/null
@@ -1,115 +0,0 @@
-// 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 .
-
-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
-}
diff --git a/database/userportal.go b/database/userportal.go
deleted file mode 100644
index 6081ee8..0000000
--- a/database/userportal.go
+++ /dev/null
@@ -1,116 +0,0 @@
-// 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 .
-
-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.
-
-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{}{}
- }
-}
diff --git a/docker-run.sh b/docker-run.sh
index fdb22b2..5f1ec65 100755
--- a/docker-run.sh
+++ b/docker-run.sh
@@ -17,11 +17,7 @@ function fixperms {
}
if [[ ! -f /data/config.yaml ]]; then
- if [[ "$BRIDGEV2" == "1" ]]; then
- $BINARY_NAME -c /data/config.yaml -e
- else
- cp /opt/mautrix-signal/example-config.yaml /data/config.yaml
- fi
+ $BINARY_NAME -c /data/config.yaml -e
echo "Didn't find a config file."
echo "Copied default config file to /data/config.yaml"
echo "Modify that config file to your liking."
diff --git a/example-config.yaml b/example-config.yaml
deleted file mode 100644
index 93a7955..0000000
--- a/example-config.yaml
+++ /dev/null
@@ -1,317 +0,0 @@
-# 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:?_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
- # 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'
- # 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: "{{ .Sender.Displayname }}: {{ .Message }}"
- m.notice: "{{ .Sender.Displayname }}: {{ .Message }}"
- m.emote: "* {{ .Sender.Displayname }} {{ .Message }}"
- m.file: "{{ .Sender.Displayname }} sent a file"
- m.image: "{{ .Sender.Displayname }} sent an image"
- m.audio: "{{ .Sender.Displayname }} sent an audio file"
- m.video: "{{ .Sender.Displayname }} sent a video"
- m.location: "{{ .Sender.Displayname }} 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
diff --git a/go.mod b/go.mod
index a04e88a..45187a6 100644
--- a/go.mod
+++ b/go.mod
@@ -48,3 +48,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect
)
+
+//replace maunium.net/go/mautrix => ../mautrix-go
+//replace go.mau.fi/util => ../../Go/go-util
diff --git a/legacyprovision/types.go b/legacyprovision/types.go
deleted file mode 100644
index 72f393a..0000000
--- a/legacyprovision/types.go
+++ /dev/null
@@ -1,92 +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 .
-
-package legacyprovision
-
-import (
- "encoding/json"
- "net/http"
-
- "maunium.net/go/mautrix/id"
-)
-
-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 LinkNew
- SessionID string `json:"session_id,omitempty"`
- URI string `json:"uri,omitempty"`
-
- // For response in LinkWaitForAccount
- UUID string `json:"uuid,omitempty"`
- Number string `json:"number,omitempty"`
-
- // For response in ResolveIdentifier
- *ResolveIdentifierResponse
-}
-
-type WhoAmIResponse struct {
- Permissions int `json:"permissions"`
- MXID string `json:"mxid"`
- Signal *WhoAmIResponseSignal `json:"signal,omitempty"`
-}
-
-type WhoAmIResponseSignal struct {
- Number string `json:"number"`
- UUID string `json:"uuid"`
- Name string `json:"name"`
- Ok bool `json:"ok"`
-}
-
-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"`
-}
-
-type LinkWaitForScanRequest struct {
- SessionID string `json:"session_id"`
-}
-
-type LinkWaitForAccountRequest struct {
- SessionID string `json:"session_id"`
- DeviceName string `json:"device_name"` // TODO this seems to not be used anywhere
-}
diff --git a/main.go b/main.go
deleted file mode 100644
index eb24aa0..0000000
--- a/main.go
+++ /dev/null
@@ -1,352 +0,0 @@
-// 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 .
-
-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.6.3",
- 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()
-}
diff --git a/messagetracking.go b/messagetracking.go
deleted file mode 100644
index e95abb4..0000000
--- a/messagetracking.go
+++ /dev/null
@@ -1,311 +0,0 @@
-// 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 .
-
-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
-}
diff --git a/metrics.go b/metrics.go
deleted file mode 100644
index 2e76a04..0000000
--- a/metrics.go
+++ /dev/null
@@ -1,281 +0,0 @@
-// 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 .
-
-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")
- }
-}
diff --git a/msgconv/msgconv.go b/msgconv/msgconv.go
deleted file mode 100644
index ee3ff55..0000000
--- a/msgconv/msgconv.go
+++ /dev/null
@@ -1,65 +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 .
-
-package msgconv
-
-import (
- "context"
- "time"
-
- "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
- UpdateDisappearing func(ctx context.Context, newTimer time.Duration)
-
- LocationFormat string
-}
-
-func (mc *MessageConverter) IsPrivateChat(ctx context.Context) bool {
- return !mc.GetData(ctx).UserID().IsEmpty()
-}
diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go
index 732b0ae..a5d99c9 100644
--- a/pkg/connector/chatinfo.go
+++ b/pkg/connector/chatinfo.go
@@ -32,6 +32,7 @@ import (
"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/types"
)
@@ -39,7 +40,7 @@ const PrivateChatTopic = "Signal private chat"
const NoteToSelfName = "Signal Note to Self"
func (s *SignalClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
- userID, err := parseUserID(ghost.ID)
+ userID, err := signalid.ParseUserID(ghost.ID)
if err != nil {
return nil, err
}
@@ -51,7 +52,7 @@ func (s *SignalClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (
}
func (s *SignalClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
- userID, groupID, err := parsePortalID(portal.ID)
+ userID, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil {
return nil, err
}
@@ -143,19 +144,19 @@ func (s *SignalClient) ResolveIdentifier(ctx context.Context, number string, cre
// createChat is a no-op: chats don't need to be created, and we always return chat info
if aci != uuid.Nil {
- ghost, err := s.Main.Bridge.GetGhostByID(ctx, makeUserID(aci))
+ ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(aci))
if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err)
}
return &bridgev2.ResolveIdentifierResponse{
- UserID: makeUserID(aci),
+ UserID: signalid.MakeUserID(aci),
UserInfo: s.contactToUserInfo(recipient),
Ghost: ghost,
Chat: s.makeCreateDMResponse(recipient),
}, nil
} else {
return &bridgev2.ResolveIdentifierResponse{
- UserID: makeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
+ UserID: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
UserInfo: s.contactToUserInfo(recipient),
Chat: s.makeCreateDMResponse(recipient),
}, nil
@@ -179,14 +180,14 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
Chat: s.makeCreateDMResponse(recipient),
}
if recipient.ACI != uuid.Nil {
- recipientResp.UserID = makeUserID(recipient.ACI)
+ recipientResp.UserID = signalid.MakeUserID(recipient.ACI)
ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID)
if err != nil {
return nil, fmt.Errorf("failed to get ghost for %s: %w", recipient.ACI, err)
}
recipientResp.Ghost = ghost
} else {
- recipientResp.UserID = makeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
+ recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
}
resp[i] = recipientResp
}
@@ -215,7 +216,7 @@ func (s *SignalClient) makeCreateDMResponse(recipient *types.Recipient) *bridgev
name = s.Main.Config.FormatDisplayname(recipient)
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
} else {
- members.OtherUserID = makeUserID(recipient.ACI)
+ members.OtherUserID = signalid.MakeUserID(recipient.ACI)
if recipient.ACI == s.Client.Store.ACI {
name = NoteToSelfName
avatar = &bridgev2.Avatar{
diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index 838c0ee..56ab59a 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -11,6 +11,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
)
@@ -112,7 +113,7 @@ func (s *SignalClient) IsThisUser(_ context.Context, userID networkid.UserID) bo
if s.Client == nil {
return false
}
- return userID == makeUserID(s.Client.Store.ACI)
+ return userID == signalid.MakeUserID(s.Client.Store.ACI)
}
func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnectionStatus) {
diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go
index 4c36674..6a4e4dc 100644
--- a/pkg/connector/connector.go
+++ b/pkg/connector/connector.go
@@ -20,19 +20,12 @@ import (
"context"
"fmt"
"text/template"
- "time"
"github.com/google/uuid"
- "github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2"
- "maunium.net/go/mautrix/bridgev2/database"
- "maunium.net/go/mautrix/bridgev2/networkid"
- "maunium.net/go/mautrix/id"
- "go.mau.fi/mautrix-signal/msgconv"
- "go.mau.fi/mautrix-signal/msgconv/matrixfmt"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
)
@@ -82,65 +75,7 @@ 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.MessageConverter{
- PortalMethods: &msgconvPortalMethods{},
- SignalFmtParams: &signalfmt.FormatParams{
- GetUserInfo: func(ctx context.Context, uuid uuid.UUID) signalfmt.UserInfo {
- ghost, err := s.Bridge.GetGhostByID(ctx, makeUserID(uuid))
- if err != nil {
- // TODO log?
- return signalfmt.UserInfo{}
- }
- userInfo := signalfmt.UserInfo{
- MXID: ghost.Intent.GetMXID(),
- Name: ghost.Name,
- }
- userLogin := s.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(uuid.String()))
- if userLogin != nil {
- userInfo.MXID = userLogin.UserMXID
- // TODO find matrix user displayname?
- }
- return userInfo
- },
- },
- MatrixFmtParams: &matrixfmt.HTMLParser{
- GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
- parsed, ok := s.Bridge.Matrix.ParseGhostMXID(userID)
- if ok {
- u, _ := uuid.Parse(string(parsed))
- return u
- }
- user, _ := s.Bridge.GetExistingUserByMXID(ctx, userID)
- // TODO log errors?
- if user != nil {
- preferredLogin, _, _ := ctx.Value(msgconvContextKey).(*msgconvContext).Portal.FindPreferredLogin(ctx, user, true)
- if preferredLogin != nil {
- u, _ := uuid.Parse(string(preferredLogin.ID))
- return u
- }
- }
- return uuid.Nil
- },
- },
- ConvertVoiceMessages: true,
- ConvertGIFToAPNG: true,
- MaxFileSize: 50 * 1024 * 1024,
- AsyncFiles: true,
- LocationFormat: s.Config.LocationFormat,
- UpdateDisappearing: func(ctx context.Context, newTimer time.Duration) {
- portal := ctx.Value(msgconvContextKey).(*msgconvContext).Portal
- portal.Disappear.Timer = newTimer
- if newTimer == 0 {
- portal.Disappear.Type = ""
- } else {
- portal.Disappear.Type = database.DisappearingTypeAfterRead
- }
- err := portal.Save(ctx)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update portal disappearing timer in database")
- }
- },
- }
+ s.MsgConv = msgconv.NewMessageConverter(bridge, s.Config.LocationFormat)
}
func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
diff --git a/pkg/connector/dbmeta.go b/pkg/connector/dbmeta.go
index 68e21d5..14f59f2 100644
--- a/pkg/connector/dbmeta.go
+++ b/pkg/connector/dbmeta.go
@@ -18,26 +18,20 @@ package connector
import (
"maunium.net/go/mautrix/bridgev2/database"
+
+ "go.mau.fi/mautrix-signal/pkg/signalid"
)
func (s *SignalConnector) GetDBMetaTypes() database.MetaTypes {
return database.MetaTypes{
Portal: func() any {
- return &PortalMetadata{}
+ return &signalid.PortalMetadata{}
},
Ghost: nil,
Message: func() any {
- return &MessageMetadata{}
+ return &signalid.MessageMetadata{}
},
Reaction: nil,
UserLogin: nil,
}
}
-
-type PortalMetadata struct {
- Revision uint32 `json:"revision"`
-}
-
-type MessageMetadata struct {
- ContainsAttachments bool `json:"contains_attachments,omitempty"`
-}
diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go
index 6bea361..3bf809d 100644
--- a/pkg/connector/groupinfo.go
+++ b/pkg/connector/groupinfo.go
@@ -27,6 +27,7 @@ import (
"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/types"
)
@@ -174,7 +175,7 @@ func (s *SignalClient) makeGroupAvatar(meta signalmeow.GroupAvatarMeta) *bridgev
func makeRevisionUpdater(rev uint32) func(ctx context.Context, portal *bridgev2.Portal) bool {
return func(ctx context.Context, portal *bridgev2.Portal) bool {
- meta := portal.Metadata.(*PortalMetadata)
+ meta := portal.Metadata.(*signalid.PortalMetadata)
if meta.Revision < rev {
meta.Revision = rev
return true
diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go
index 2f36ae7..9e8c353 100644
--- a/pkg/connector/handlematrix.go
+++ b/pkg/connector/handlematrix.go
@@ -32,12 +32,13 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
"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"
)
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
- userID, groupID, err := parsePortalID(portalID)
+ userID, groupID, err := signalid.ParsePortalID(portalID)
if err != nil {
return err
}
@@ -77,15 +78,7 @@ func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.Porta
}
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
- mcCtx := &msgconvContext{
- Connector: s.Main,
- Intent: nil,
- Client: s,
- Portal: msg.Portal,
- ReplyTo: msg.ReplyTo,
- }
- ctx = context.WithValue(ctx, msgconvContextKey, mcCtx)
- converted, err := s.Main.MsgConv.ToSignal(ctx, msg.Event, msg.Content, msg.OrigSender != nil)
+ 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
}
@@ -94,10 +87,10 @@ func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Ma
return nil, err
}
dbMsg := &database.Message{
- ID: makeMessageID(s.Client.Store.ACI, converted.GetTimestamp()),
- SenderID: makeUserID(s.Client.Store.ACI),
+ ID: signalid.MakeMessageID(s.Client.Store.ACI, converted.GetTimestamp()),
+ SenderID: signalid.MakeUserID(s.Client.Store.ACI),
Timestamp: time.UnixMilli(int64(converted.GetTimestamp())),
- Metadata: &MessageMetadata{
+ Metadata: &signalid.MessageMetadata{
ContainsAttachments: len(converted.Attachments) > 0,
},
}
@@ -107,27 +100,20 @@ func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Ma
}
func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
- _, targetSentTimestamp, err := parseMessageID(msg.EditTarget.ID)
+ _, 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 != makeUserID(s.Client.Store.ACI) {
+ } else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
return fmt.Errorf("cannot edit other people's messages")
}
- mcCtx := &msgconvContext{
- Connector: s.Main,
- Intent: nil,
- Client: s,
- Portal: msg.Portal,
- }
+ var replyTo *database.Message
if msg.EditTarget.ReplyTo.MessageID != "" {
- var err error
- mcCtx.ReplyTo, err = s.Main.Bridge.DB.Message.GetFirstOrSpecificPartByID(ctx, msg.Portal.Receiver, msg.EditTarget.ReplyTo)
+ 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)
}
}
- ctx = context.WithValue(ctx, msgconvContextKey, mcCtx)
- converted, err := s.Main.MsgConv.ToSignal(ctx, msg.Event, msg.Content, msg.OrigSender != nil)
+ converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
if err != nil {
return err
}
@@ -138,22 +124,22 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
if err != nil {
return err
}
- msg.EditTarget.ID = makeMessageID(s.Client.Store.ACI, converted.GetTimestamp())
- msg.EditTarget.Metadata = &MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
+ msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, converted.GetTimestamp())
+ 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: makeUserID(s.Client.Store.ACI),
+ 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 := parseMessageID(msg.TargetMessage.ID)
+ targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
}
@@ -177,7 +163,7 @@ func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.M
}
func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
- targetAuthorACI, targetSentTimestamp, err := parseMessageID(msg.TargetReaction.MessageID)
+ targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
}
@@ -201,10 +187,10 @@ func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *brid
}
func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
- _, targetSentTimestamp, err := parseMessageID(msg.TargetMessage.ID)
+ _, 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 != makeUserID(s.Client.Store.ACI) {
+ } else if msg.TargetMessage.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
return fmt.Errorf("cannot delete other people's messages")
}
wrappedContent := &signalpb.Content{
@@ -237,7 +223,7 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
}
messagesToRead := map[uuid.UUID][]uint64{}
for _, msg := range dbMessages {
- userID, timestamp, err := parseMessageID(msg.ID)
+ userID, timestamp, err := signalid.ParseMessageID(msg.ID)
if err != nil {
return fmt.Errorf("failed to parse message ID %q: %w", msg.ID, err)
}
@@ -275,7 +261,7 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
}
func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error {
- userID, _, err := parsePortalID(typing.Portal.ID)
+ userID, _, err := signalid.ParsePortalID(typing.Portal.ID)
if err != nil {
return err
}
@@ -292,11 +278,11 @@ func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.
}
func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev2.Portal, gc *signalmeow.GroupChange, postUpdatePortal func()) (bool, error) {
- _, groupID, err := parsePortalID(portal.ID)
+ _, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil || groupID == "" {
return false, err
}
- gc.Revision = portal.Metadata.(*PortalMetadata).Revision + 1
+ gc.Revision = portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil {
return false, err
@@ -316,7 +302,7 @@ func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev
if postUpdatePortal != nil {
postUpdatePortal()
}
- portal.Metadata.(*PortalMetadata).Revision = revision
+ portal.Metadata.(*signalid.PortalMetadata).Revision = revision
return true, nil
}
@@ -327,7 +313,7 @@ func (s *SignalClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.M
}
func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
- _, groupID, err := parsePortalID(msg.Portal.ID)
+ _, groupID, err := signalid.ParsePortalID(msg.Portal.ID)
if err != nil || groupID == "" {
return false, err
}
diff --git a/pkg/connector/handlesignal.go b/pkg/connector/handlesignal.go
index 169338d..2f6803d 100644
--- a/pkg/connector/handlesignal.go
+++ b/pkg/connector/handlesignal.go
@@ -30,6 +30,7 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
)
@@ -63,7 +64,7 @@ func (s *SignalClient) wrapCallEvent(evt *events.Call) bridgev2.RemoteMessage {
PortalKey: s.makePortalKey(evt.Info.ChatID),
Data: evt,
CreatePortal: true,
- ID: makeMessageID(evt.Info.Sender, evt.Timestamp),
+ ID: signalid.MakeMessageID(evt.Info.Sender, evt.Timestamp),
Sender: s.makeEventSender(evt.Info.Sender),
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
ConvertMessageFunc: convertCallEvent,
@@ -76,7 +77,7 @@ func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridg
}
if data.IsRinging {
content.Body = "Incoming call"
- if userID, _, _ := parsePortalID(portal.ID); !userID.IsEmpty() {
+ if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
content.MsgType = event.MsgText
}
} else {
@@ -152,7 +153,7 @@ func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal)
if !ok || dataMsg.GroupV2 == nil {
return
}
- portalRev := portal.Metadata.(*PortalMetadata).Revision
+ portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision
if evt.Info.GroupRevision > portalRev {
toRevision := evt.Info.GroupRevision
if dataMsg.GetGroupV2().GetGroupChange() != nil {
@@ -206,7 +207,7 @@ func (evt *Bv2ChatEvent) GetID() networkid.MessageID {
if ts == 0 {
panic(fmt.Errorf("GetID() called for non-DataMessage event"))
}
- return makeMessageID(evt.Info.Sender, ts)
+ return signalid.MakeMessageID(evt.Info.Sender, ts)
}
func (evt *Bv2ChatEvent) getDataMsgTimestamp() uint64 {
@@ -251,7 +252,7 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
if targetAuthorACI != "" {
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
}
- return makeMessageID(targetAuthorUUID, targetSentTS)
+ return signalid.MakeMessageID(targetAuthorUUID, targetSentTS)
}
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
@@ -267,82 +268,35 @@ func (evt *Bv2ChatEvent) GetRemovedEmojiID() networkid.EmojiID {
}
func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
- mcCtx := &msgconvContext{
- Connector: evt.s.Main,
- Intent: intent,
- Client: evt.s,
- Portal: portal,
- }
- ctx = context.WithValue(ctx, msgconvContextKey, mcCtx)
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, dataMsg)
- converted.MergeCaption()
- var replyTo *networkid.MessageOptionalPartID
- if dataMsg.GetQuote() != nil {
- quoteAuthor, _ := uuid.Parse(dataMsg.Quote.GetAuthorAci())
- replyTo = &networkid.MessageOptionalPartID{
- MessageID: makeMessageID(quoteAuthor, dataMsg.Quote.GetId()),
- }
- }
- convertedParts := make([]*bridgev2.ConvertedMessagePart, len(converted.Parts))
- for i, part := range converted.Parts {
- convertedParts[i] = &bridgev2.ConvertedMessagePart{
- ID: makeMessagePartID(i),
- Type: part.Type,
- Content: part.Content,
- Extra: part.Extra,
- DBMetadata: &MessageMetadata{ContainsAttachments: len(dataMsg.GetAttachments()) > 0},
- }
- }
- var disappear database.DisappearingSetting
- if converted.DisappearIn != 0 {
- disappear = database.DisappearingSetting{
- Type: database.DisappearingTypeAfterRead,
- Timer: time.Duration(converted.DisappearIn) * time.Second,
- }
- dataMsgTS := time.UnixMilli(int64(dataMsg.GetTimestamp()))
+ converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, dataMsg)
+ if converted.Disappear.Type != "" {
+ evtTS := evt.GetTimestamp()
+ portal.UpdateDisappearingSetting(ctx, converted.Disappear, nil, evtTS, true, true)
if evt.Info.Sender == evt.s.Client.Store.ACI {
- disappear.DisappearAt = dataMsgTS.Add(disappear.Timer)
+ converted.Disappear.DisappearAt = evtTS.Add(converted.Disappear.Timer)
}
- portal.UpdateDisappearingSetting(ctx, disappear, nil, dataMsgTS, true, true)
}
- return &bridgev2.ConvertedMessage{
- ReplyTo: replyTo,
- Parts: convertedParts,
- Disappear: disappear,
- }, nil
+ return converted, nil
}
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
- mcCtx := &msgconvContext{
- Connector: evt.s.Main,
- Intent: intent,
- Client: evt.s,
- Portal: portal,
- }
- ctx = context.WithValue(ctx, msgconvContextKey, mcCtx)
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, editMsg.GetDataMessage())
- converted.MergeCaption()
- convertedEdit := &bridgev2.ConvertedEdit{}
+ converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, editMsg.GetDataMessage())
// TODO can anything other than the text be edited?
- lastPart := converted.Parts[len(converted.Parts)-1]
- convertedEdit.ModifiedParts = append(convertedEdit.ModifiedParts, &bridgev2.ConvertedEditPart{
- Part: existing[len(existing)-1],
- Type: lastPart.Type,
- Content: lastPart.Content,
- Extra: lastPart.Extra,
- })
- convertedEdit.ModifiedParts[0].Part.EditCount++
- convertedEdit.ModifiedParts[0].Part.ID = makeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
- return convertedEdit, nil
+ 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
}
type Bv2Receipt struct {
@@ -438,7 +392,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) {
Logger()
ctx := log.WithContext(context.TODO())
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, makeMessageID(s.Client.Store.ACI, msgTS))
+ return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS))
})
s.dispatchReceipts(evt.Sender, evt.Content.GetType(), receipts)
}
@@ -453,7 +407,7 @@ func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) {
if err != nil {
return nil, err
}
- return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, makeMessageID(aciUUID, msgInfo.GetTimestamp()))
+ return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(aciUUID, msgInfo.GetTimestamp()))
})
s.dispatchReceipts(s.Client.Store.ACI, signalpb.ReceiptMessage_READ, receipts)
}
@@ -495,7 +449,7 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
continue
}
fullContact.ContactAvatar = contact.ContactAvatar
- ghost, err := s.Main.Bridge.GetGhostByID(ctx, makeUserID(contact.ACI))
+ 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
@@ -510,7 +464,7 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
func (s *SignalClient) updateRemoteProfile(ctx context.Context, resendState bool) {
var err error
if s.Ghost == nil {
- s.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, makeUserID(s.Client.Store.ACI))
+ 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
diff --git a/pkg/connector/id.go b/pkg/connector/id.go
index 6aa4f82..49cebe6 100644
--- a/pkg/connector/id.go
+++ b/pkg/connector/id.go
@@ -17,71 +17,14 @@
package connector
import (
- "fmt"
- "strconv"
- "strings"
-
"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/signalmeow/types"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
)
-func parseUserID(userID networkid.UserID) (uuid.UUID, error) {
- serviceID, err := parseUserIDAsServiceID(userID)
- if err != nil {
- return uuid.Nil, err
- } else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
- return uuid.Nil, fmt.Errorf("invalid user ID: expected ACI type")
- } else {
- return serviceID.UUID, nil
- }
-}
-
-func parseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) {
- return libsignalgo.ServiceIDFromString(string(userID))
-}
-
-func parsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) {
- if len(portalID) == 44 {
- groupID = types.GroupIdentifier(portalID)
- } else {
- userID, err = libsignalgo.ServiceIDFromString(string(portalID))
- }
- return
-}
-
-func parseMessageID(messageID networkid.MessageID) (sender uuid.UUID, timestamp uint64, err error) {
- parts := strings.Split(string(messageID), "|")
- if len(parts) != 2 {
- err = fmt.Errorf("invalid message ID: expected two pipe-separated parts")
- return
- }
- sender, err = uuid.Parse(parts[0])
- if err != nil {
- return
- }
- timestamp, err = strconv.ParseUint(parts[1], 10, 64)
- return
-}
-
-func makeGroupPortalID(groupID types.GroupIdentifier) networkid.PortalID {
- return networkid.PortalID(groupID)
-}
-
-func makeGroupPortalKey(groupID types.GroupIdentifier) networkid.PortalKey {
- return networkid.PortalKey{
- ID: makeGroupPortalID(groupID),
- Receiver: "",
- }
-}
-
-func makeDMPortalID(serviceID libsignalgo.ServiceID) networkid.PortalID {
- return networkid.PortalID(serviceID.String())
-}
-
func (s *SignalClient) makePortalKey(chatID string) networkid.PortalKey {
key := networkid.PortalKey{ID: networkid.PortalID(chatID)}
// For non-group chats, add receiver
@@ -93,38 +36,15 @@ func (s *SignalClient) makePortalKey(chatID string) networkid.PortalKey {
func (s *SignalClient) makeDMPortalKey(serviceID libsignalgo.ServiceID) networkid.PortalKey {
return networkid.PortalKey{
- ID: makeDMPortalID(serviceID),
+ ID: signalid.MakeDMPortalID(serviceID),
Receiver: s.UserLogin.ID,
}
}
-func makeMessageID(sender uuid.UUID, timestamp uint64) networkid.MessageID {
- return networkid.MessageID(fmt.Sprintf("%s|%d", sender, timestamp))
-}
-
-func makeUserID(user uuid.UUID) networkid.UserID {
- return networkid.UserID(user.String())
-}
-
-func makeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID {
- return networkid.UserID(user.String())
-}
-
-func makeUserLoginID(user uuid.UUID) networkid.UserLoginID {
- return networkid.UserLoginID(user.String())
-}
-
func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
return bridgev2.EventSender{
IsFromMe: sender == s.Client.Store.ACI,
- SenderLogin: makeUserLoginID(sender),
- Sender: makeUserID(sender),
+ SenderLogin: signalid.MakeUserLoginID(sender),
+ Sender: signalid.MakeUserID(sender),
}
}
-
-func makeMessagePartID(index int) networkid.PartID {
- if index == 0 {
- return ""
- }
- return networkid.PartID(strconv.Itoa(index))
-}
diff --git a/pkg/connector/login.go b/pkg/connector/login.go
index 3acd926..2e79933 100644
--- a/pkg/connector/login.go
+++ b/pkg/connector/login.go
@@ -25,6 +25,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
)
@@ -144,7 +145,7 @@ func (qr *QRLogin) qrWait(ctx context.Context) (*bridgev2.LoginStep, error) {
func (qr *QRLogin) processingWait(ctx context.Context) (*bridgev2.LoginStep, error) {
defer qr.cancelChan()
- newLoginID := makeUserLoginID(qr.ProvData.ACI)
+ newLoginID := signalid.MakeUserLoginID(qr.ProvData.ACI)
select {
case resp := <-qr.ProvChan:
diff --git a/pkg/connector/msgconvproxy.go b/pkg/connector/msgconvproxy.go
deleted file mode 100644
index 8dc2db8..0000000
--- a/pkg/connector/msgconvproxy.go
+++ /dev/null
@@ -1,117 +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 .
-
-package connector
-
-import (
- "context"
- "strings"
-
- "google.golang.org/protobuf/proto"
- "maunium.net/go/mautrix/bridgev2"
- "maunium.net/go/mautrix/bridgev2/database"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- legacydb "go.mau.fi/mautrix-signal/database"
- "go.mau.fi/mautrix-signal/msgconv"
- "go.mau.fi/mautrix-signal/pkg/signalmeow"
- signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
-)
-
-type contextKey int
-
-var msgconvContextKey contextKey
-
-type msgconvContext struct {
- Connector *SignalConnector
- Intent bridgev2.MatrixAPI
- Client *SignalClient
- Portal *bridgev2.Portal
- ReplyTo *database.Message
-}
-
-type msgconvPortalMethods struct{}
-
-var _ msgconv.PortalMethods = (*msgconvPortalMethods)(nil)
-
-func (mpm *msgconvPortalMethods) UploadMatrixMedia(ctx context.Context, data []byte, fileName, contentType string) (id.ContentURIString, error) {
- mcCtx := ctx.Value(msgconvContextKey).(*msgconvContext)
- uri, _, err := mcCtx.Intent.UploadMedia(ctx, "", data, fileName, contentType)
- return uri, err
-}
-
-func (mpm *msgconvPortalMethods) DownloadMatrixMedia(ctx context.Context, uri id.ContentURIString) ([]byte, error) {
- return ctx.Value(msgconvContextKey).(*msgconvContext).Connector.Bridge.Bot.DownloadMedia(ctx, uri, nil)
-}
-
-func (mpm *msgconvPortalMethods) GetMatrixReply(ctx context.Context, msg *signalpb.DataMessage_Quote) (replyTo id.EventID, replyTargetSender id.UserID) {
- // Matrix replies are handled in bridgev2 code
- return "", ""
-}
-
-func (mpm *msgconvPortalMethods) GetSignalReply(ctx context.Context, content *event.MessageEventContent) *signalpb.DataMessage_Quote {
- mcCtx := ctx.Value(msgconvContextKey).(*msgconvContext)
- if mcCtx.ReplyTo == nil {
- return nil
- }
- quote := &signalpb.DataMessage_Quote{
- Id: proto.Uint64(uint64(mcCtx.ReplyTo.Timestamp.UnixMilli())),
- AuthorAci: proto.String(string(mcCtx.ReplyTo.SenderID)),
- Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
- }
- if mcCtx.ReplyTo.Metadata.(*MessageMetadata).ContainsAttachments {
- quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1)
- }
- return quote
-}
-
-func (mpm *msgconvPortalMethods) GetClient(ctx context.Context) *signalmeow.Client {
- return ctx.Value(msgconvContextKey).(*msgconvContext).Client.Client
-}
-
-func (mpm *msgconvPortalMethods) GetData(ctx context.Context) *legacydb.Portal {
- mcCtx := ctx.Value(msgconvContextKey).(*msgconvContext)
- portal := mcCtx.Portal
- userID, groupID, _ := parsePortalID(portal.ID)
- chatID := string(groupID)
- if chatID == "" {
- chatID = userID.String()
- }
- pk := legacydb.PortalKey{
- ChatID: chatID,
- }
- if len(chatID) != 44 {
- pk.Receiver = mcCtx.Client.Client.Store.ACI
- }
- return &legacydb.Portal{
- PortalKey: pk,
- MXID: portal.MXID,
- Name: portal.Name,
- Topic: portal.Topic,
- //AvatarPath: "",
- //AvatarHash: "",
- //AvatarURL: id.ContentURI{},
- NameSet: portal.NameSet,
- AvatarSet: portal.AvatarSet,
- TopicSet: portal.TopicSet,
- Revision: portal.Metadata.(*PortalMetadata).Revision,
- // Hack to prevent encryption while using the bridge as a "local bridge"
- Encrypted: !strings.HasSuffix(portal.Bridge.Matrix.ServerName(), ".localhost"),
- //RelayUserID: portal.Relay.UserMXID,
- ExpirationTime: uint32(portal.Disappear.Timer.Seconds()),
- }
-}
diff --git a/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go
similarity index 74%
rename from msgconv/from-matrix.go
rename to pkg/msgconv/from-matrix.go
index 9a14ead..3b61b9f 100644
--- a/msgconv/from-matrix.go
+++ b/pkg/msgconv/from-matrix.go
@@ -18,35 +18,37 @@ package msgconv
import (
"context"
- "errors"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
- "github.com/rs/zerolog/log"
- "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/msgconv/matrixfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
+ "go.mau.fi/mautrix-signal/pkg/signalmeow"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
)
-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) {
+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)
if evt.Type == event.EventSticker {
content.MsgType = event.MessageType(event.EventSticker.Type)
}
@@ -59,11 +61,23 @@ func (mc *MessageConverter) ToSignal(ctx context.Context, evt *event.Event, cont
}
dm := &signalpb.DataMessage{
Timestamp: &ts,
- Quote: mc.GetSignalReply(ctx, content),
- Preview: mc.convertURLPreviewToSignal(ctx, evt),
+ Preview: mc.convertURLPreviewToSignal(ctx, content),
}
- if expirationTime := mc.GetData(ctx).ExpirationTime; expirationTime != 0 {
- dm.ExpireTimer = proto.Uint32(uint32(expirationTime))
+ if replyTo != nil {
+ authorACI, messageID, err := signalid.ParseMessageID(replyTo.ID)
+ if err == nil {
+ dm.Quote = &signalpb.DataMessage_Quote{
+ Id: proto.Uint64(messageID),
+ AuthorAci: proto.String(authorACI.String()),
+ Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
+ }
+ if replyTo.Metadata.(*signalid.MessageMetadata).ContainsAttachments {
+ dm.Quote.Attachments = make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1)
+ }
+ }
+ }
+ if portal.Disappear.Timer > 0 {
+ dm.ExpireTimer = proto.Uint32(uint32(portal.Disappear.Timer.Seconds()))
}
if content.MsgType == event.MsgEmote && !relaybotFormatted {
content.Body = "/me " + content.Body
@@ -113,13 +127,13 @@ func (mc *MessageConverter) ToSignal(ctx context.Context, evt *event.Event, cont
case event.MsgLocation:
lat, lon, err := parseGeoURI(content.GeoURI)
if err != nil {
- log.Err(err).Msg("Invalid geo URI")
+ zerolog.Ctx(ctx).Err(err).Msg("Invalid geo URI")
return nil, err
}
locationString := fmt.Sprintf(mc.LocationFormat, lat, lon)
dm.Body = &locationString
default:
- return nil, fmt.Errorf("%w %s", ErrUnsupportedMsgType, content.MsgType)
+ return nil, fmt.Errorf("%w %s", bridgev2.ErrUnsupportedMessageType, content.MsgType)
}
return dm, nil
}
@@ -133,44 +147,33 @@ 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)
- mxc := content.URL
- if content.File != nil {
- mxc = content.File.URL
- }
- data, err := mc.DownloadMatrixMedia(ctx, mxc)
+ data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
if err != nil {
- return nil, exerrors.NewDualError(ErrMediaDownloadFailed, err)
- }
- if content.File != nil {
- err = content.File.DecryptInPlace(data)
- if err != nil {
- return nil, exerrors.NewDualError(ErrMediaDecryptFailed, err)
- }
+ return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
}
fileName := content.Body
if content.FileName != "" {
fileName = content.FileName
}
- _, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"]
mime := content.GetInfo().MimeType
- if isVoice {
+ if content.MSC3245Voice != nil && ffmpeg.Supported() {
data, err = ffmpeg.ConvertBytes(ctx, data, ".m4a", []string{}, []string{"-c:a", "aac"}, mime)
if err != nil {
return nil, err
}
mime = "audio/aac"
fileName += ".m4a"
- } else if evt.Type == event.EventSticker && mime != "image/webp" && mime != "image/png" && mime != "image/apng" {
+ } else if evt.Type == event.EventSticker {
switch mime {
case "image/webp", "image/png", "image/apng":
// allowed
case "image/gif":
- if !mc.ConvertGIFToAPNG {
+ if !ffmpeg.Supported() {
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", ErrMediaConvertFailed, err)
+ return nil, fmt.Errorf("%w (gif to apng): %w", bridgev2.ErrMediaConvertFailed, err)
}
fileName += ".apng"
mime = "image/apng"
@@ -178,12 +181,12 @@ func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event.
return nil, fmt.Errorf("unsupported content type for sticker %s", mime)
}
}
- att, err := mc.GetClient(ctx).UploadAttachment(ctx, data)
+ att, err := getClient(ctx).UploadAttachment(ctx, data)
if err != nil {
log.Err(err).Msg("Failed to upload file")
- return nil, exerrors.NewDualError(ErrMediaUploadFailed, err)
+ return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
}
- if isVoice {
+ if content.MSC3245Voice != nil && mime == "audio/aac" {
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE))
}
att.ContentType = proto.String(mime)
diff --git a/msgconv/from-signal.go b/pkg/msgconv/from-signal.go
similarity index 72%
rename from msgconv/from-signal.go
rename to pkg/msgconv/from-signal.go
index 918c749..e4ab6dc 100644
--- a/msgconv/from-signal.go
+++ b/pkg/msgconv/from-signal.go
@@ -26,49 +26,22 @@ import (
"time"
"github.com/emersion/go-vcard"
+ "github.com/google/uuid"
"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/bridgev2"
+ "maunium.net/go/mautrix/bridgev2/database"
+ "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/signalid"
"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
@@ -96,19 +69,30 @@ 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)),
+func (mc *MessageConverter) ToMatrix(
+ ctx context.Context,
+ client *signalmeow.Client,
+ portal *bridgev2.Portal,
+ intent bridgev2.MatrixAPI,
+ dm *signalpb.DataMessage,
+) *bridgev2.ConvertedMessage {
+ ctx = context.WithValue(ctx, contextKeyClient, client)
+ ctx = context.WithValue(ctx, contextKeyPortal, portal)
+ ctx = context.WithValue(ctx, contextKeyIntent, intent)
+ cm := &bridgev2.ConvertedMessage{
+ ReplyTo: nil,
+ ThreadRoot: nil,
+ Parts: make([]*bridgev2.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.GetExpireTimer() > 0 {
+ cm.Disappear.Type = database.DisappearingTypeAfterRead
+ cm.Disappear.Timer = time.Duration(dm.GetExpireTimer()) * time.Second
+ }
if dm.Sticker != nil {
cm.Parts = append(cm.Parts, mc.convertStickerToMatrix(ctx, dm.Sticker))
// Don't allow any other parts in a sticker message
@@ -139,7 +123,7 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, dm *signalpb.DataMessa
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{
+ cm.Parts = append(cm.Parts, &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -147,23 +131,28 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, dm *signalpb.DataMessa
},
})
}
- replyTo, sender := mc.GetMatrixReply(ctx, dm.Quote)
- for _, part := range cm.Parts {
- if part.Content.Mentions == nil {
- part.Content.Mentions = &event.Mentions{}
+ cm.MergeCaption()
+ for i, part := range cm.Parts {
+ part.ID = signalid.MakeMessagePartID(i)
+ part.DBMetadata = &signalid.MessageMetadata{
+ ContainsAttachments: len(dm.GetAttachments()) > 0,
}
- 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)
+ }
+ if dm.Quote != nil {
+ authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Str("author_aci", dm.Quote.GetAuthorAci()).Msg("Failed to parse quote author ACI")
+ } else {
+ cm.ReplyTo = &networkid.MessageOptionalPartID{
+ MessageID: signalid.MakeMessageID(authorACI, dm.Quote.GetId()),
}
}
}
return cm
}
-func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.Context, timer uint32, updatePortal bool) *ConvertedMessagePart {
- part := &ConvertedMessagePart{
+func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.Context, timer uint32, updatePortal bool) *bridgev2.ConvertedMessagePart {
+ part := &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -174,35 +163,36 @@ func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.C
part.Content.Body = "Disappearing messages disabled"
}
if updatePortal {
- if mc.UpdateDisappearing != nil {
- mc.UpdateDisappearing(ctx, time.Duration(timer)*time.Second)
+ portal := getPortal(ctx)
+ portal.Disappear.Timer = time.Duration(timer) * time.Second
+ if timer == 0 {
+ portal.Disappear.Type = ""
} else {
- 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")
- }
+ portal.Disappear.Type = database.DisappearingTypeAfterRead
+ }
+ err := portal.Save(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 {
+func (mc *MessageConverter) convertTextToMatrix(ctx context.Context, dm *signalpb.DataMessage) *bridgev2.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)
+ content.BeeperLinkPreviews = mc.convertURLPreviewsToBeeper(ctx, dm.Preview)
}
- return &ConvertedMessagePart{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: content,
Extra: extra,
}
}
-func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *signalpb.DataMessage_Payment) *ConvertedMessagePart {
- return &ConvertedMessagePart{
+func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *signalpb.DataMessage_Payment) *bridgev2.ConvertedMessagePart {
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -214,8 +204,8 @@ func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *s
}
}
-func (mc *MessageConverter) convertGiftBadgeToMatrix(_ context.Context, giftBadge *signalpb.DataMessage_GiftBadge) *ConvertedMessagePart {
- return &ConvertedMessagePart{
+func (mc *MessageConverter) convertGiftBadgeToMatrix(_ context.Context, giftBadge *signalpb.DataMessage_GiftBadge) *bridgev2.ConvertedMessagePart {
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -303,7 +293,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
return card
}
-func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact *signalpb.DataMessage_Contact) *ConvertedMessagePart {
+func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact *signalpb.DataMessage_Contact) *bridgev2.ConvertedMessagePart {
card := mc.convertContactToVCard(ctx, contact)
contact.Avatar = nil
extraData := map[string]any{
@@ -313,7 +303,7 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
err := vcard.NewEncoder(&buf).Encode(card)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to encode vCard")
- return &ConvertedMessagePart{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -323,30 +313,6 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
}
}
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()
@@ -368,24 +334,30 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
Size: len(data),
},
}
- if file != nil {
- file.URL = mxc
- content.File = file
- } else {
- content.URL = mxc
+ content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, content.Info.MimeType, content.Body)
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Msg("Failed to upload vCard")
+ return &bridgev2.ConvertedMessagePart{
+ Type: event.EventMessage,
+ Content: &event.MessageEventContent{
+ MsgType: event.MsgNotice,
+ Body: "Failed to upload vCard",
+ },
+ Extra: extraData,
+ }
}
- return &ConvertedMessagePart{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: content,
Extra: extraData,
}
}
-func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index int, att *signalpb.AttachmentPointer) *ConvertedMessagePart {
+func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index int, att *signalpb.AttachmentPointer) *bridgev2.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{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -396,11 +368,11 @@ func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index
return part
}
-func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker) *ConvertedMessagePart {
+func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker *signalpb.DataMessage_Sticker) *bridgev2.ConvertedMessagePart {
converted, err := mc.reuploadAttachment(ctx, sticker.GetData())
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker")
- return &ConvertedMessagePart{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
@@ -437,7 +409,7 @@ func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *sig
return &longBody, nil
}
-func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer) (*ConvertedMessagePart, error) {
+func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalpb.AttachmentPointer) (*bridgev2.ConvertedMessagePart, error) {
data, err := signalmeow.DownloadAttachment(ctx, att)
if err != nil {
return nil, fmt.Errorf("failed to download attachment: %w", err)
@@ -447,43 +419,28 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
mimeType = http.DetectContentType(data)
}
fileName := att.GetFileName()
- extra := map[string]any{}
- if mc.ConvertVoiceMessages && att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 {
+ content := &event.MessageEventContent{
+ Info: &event.FileInfo{
+ Width: int(att.GetWidth()),
+ Height: int(att.GetHeight()),
+ Size: len(data),
+ },
+ }
+ if att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 && ffmpeg.Supported() {
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mimeType)
if err != nil {
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
}
fileName += ".ogg"
mimeType = "audio/ogg"
- extra["org.matrix.msc3245.voice"] = map[string]any{}
+ content.MSC3245Voice = &event.MSC3245Voice{}
// TODO include duration here (and in info) if there's some easy way to extract it with ffmpeg
- //extra["org.matrix.msc1767.audio"] = map[string]any{"duration": ???}
+ //content.MSC1767Audio = &event.MSC1767Audio{}
}
- 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)
+ content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, fileName, mimeType)
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()
@@ -498,18 +455,13 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
default:
content.MsgType = event.MsgFile
}
+ content.Body = fileName
+ content.Info.MimeType = mimeType
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{
+ return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: content,
- Extra: extra,
}, nil
}
diff --git a/msgconv/matrixfmt/convert.go b/pkg/msgconv/matrixfmt/convert.go
similarity index 100%
rename from msgconv/matrixfmt/convert.go
rename to pkg/msgconv/matrixfmt/convert.go
diff --git a/msgconv/matrixfmt/convert_test.go b/pkg/msgconv/matrixfmt/convert_test.go
similarity index 97%
rename from msgconv/matrixfmt/convert_test.go
rename to pkg/msgconv/matrixfmt/convert_test.go
index 6f9d66e..240ca98 100644
--- a/msgconv/matrixfmt/convert_test.go
+++ b/pkg/msgconv/matrixfmt/convert_test.go
@@ -10,8 +10,8 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
- "go.mau.fi/mautrix-signal/msgconv/matrixfmt"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
)
var formatParams = &matrixfmt.HTMLParser{
diff --git a/msgconv/matrixfmt/html.go b/pkg/msgconv/matrixfmt/html.go
similarity index 99%
rename from msgconv/matrixfmt/html.go
rename to pkg/msgconv/matrixfmt/html.go
index 94571da..1e3d249 100644
--- a/msgconv/matrixfmt/html.go
+++ b/pkg/msgconv/matrixfmt/html.go
@@ -13,7 +13,7 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
)
type EntityString struct {
diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go
new file mode 100644
index 0000000..1d1fee8
--- /dev/null
+++ b/pkg/msgconv/msgconv.go
@@ -0,0 +1,107 @@
+// 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 .
+
+package msgconv
+
+import (
+ "context"
+
+ "github.com/google/uuid"
+ "maunium.net/go/mautrix/bridgev2"
+ "maunium.net/go/mautrix/bridgev2/networkid"
+ "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/pkg/signalid"
+ "go.mau.fi/mautrix-signal/pkg/signalmeow"
+)
+
+type contextKey int
+
+const (
+ contextKeyPortal contextKey = iota
+ contextKeyClient
+ contextKeyIntent
+)
+
+type MessageConverter struct {
+ Bridge *bridgev2.Bridge
+
+ SignalFmtParams *signalfmt.FormatParams
+ MatrixFmtParams *matrixfmt.HTMLParser
+
+ MaxFileSize int64
+ LocationFormat string
+}
+
+func NewMessageConverter(br *bridgev2.Bridge, locationFormat string) *MessageConverter {
+ return &MessageConverter{
+ Bridge: br,
+ SignalFmtParams: &signalfmt.FormatParams{
+ GetUserInfo: func(ctx context.Context, uuid uuid.UUID) signalfmt.UserInfo {
+ ghost, err := br.GetGhostByID(ctx, signalid.MakeUserID(uuid))
+ if err != nil {
+ // TODO log?
+ return signalfmt.UserInfo{}
+ }
+ userInfo := signalfmt.UserInfo{
+ MXID: ghost.Intent.GetMXID(),
+ Name: ghost.Name,
+ }
+ userLogin := br.GetCachedUserLoginByID(networkid.UserLoginID(uuid.String()))
+ if userLogin != nil {
+ userInfo.MXID = userLogin.UserMXID
+ // TODO find matrix user displayname?
+ }
+ return userInfo
+ },
+ },
+ MatrixFmtParams: &matrixfmt.HTMLParser{
+ GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
+ parsed, ok := br.Matrix.ParseGhostMXID(userID)
+ if ok {
+ u, _ := uuid.Parse(string(parsed))
+ return u
+ }
+ user, _ := br.GetExistingUserByMXID(ctx, userID)
+ // TODO log errors?
+ if user != nil {
+ preferredLogin, _, _ := getPortal(ctx).FindPreferredLogin(ctx, user, true)
+ if preferredLogin != nil {
+ u, _ := uuid.Parse(string(preferredLogin.ID))
+ return u
+ }
+ }
+ return uuid.Nil
+ },
+ },
+ MaxFileSize: 50 * 1024 * 1024,
+ LocationFormat: locationFormat,
+ }
+}
+
+func getClient(ctx context.Context) *signalmeow.Client {
+ return ctx.Value(contextKeyClient).(*signalmeow.Client)
+}
+
+func getPortal(ctx context.Context) *bridgev2.Portal {
+ return ctx.Value(contextKeyPortal).(*bridgev2.Portal)
+}
+
+func getIntent(ctx context.Context) bridgev2.MatrixAPI {
+ return ctx.Value(contextKeyIntent).(bridgev2.MatrixAPI)
+}
diff --git a/msgconv/signalfmt/convert.go b/pkg/msgconv/signalfmt/convert.go
similarity index 100%
rename from msgconv/signalfmt/convert.go
rename to pkg/msgconv/signalfmt/convert.go
diff --git a/msgconv/signalfmt/convert_test.go b/pkg/msgconv/signalfmt/convert_test.go
similarity index 99%
rename from msgconv/signalfmt/convert_test.go
rename to pkg/msgconv/signalfmt/convert_test.go
index 1b2d693..eb65542 100644
--- a/msgconv/signalfmt/convert_test.go
+++ b/pkg/msgconv/signalfmt/convert_test.go
@@ -26,7 +26,7 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
+ "go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
)
diff --git a/msgconv/signalfmt/html.go b/pkg/msgconv/signalfmt/html.go
similarity index 100%
rename from msgconv/signalfmt/html.go
rename to pkg/msgconv/signalfmt/html.go
diff --git a/msgconv/signalfmt/tags.go b/pkg/msgconv/signalfmt/tags.go
similarity index 100%
rename from msgconv/signalfmt/tags.go
rename to pkg/msgconv/signalfmt/tags.go
diff --git a/msgconv/signalfmt/tree.go b/pkg/msgconv/signalfmt/tree.go
similarity index 100%
rename from msgconv/signalfmt/tree.go
rename to pkg/msgconv/signalfmt/tree.go
diff --git a/msgconv/urlpreview.go b/pkg/msgconv/urlpreview.go
similarity index 53%
rename from msgconv/urlpreview.go
rename to pkg/msgconv/urlpreview.go
index 168dcb9..d2d3b23 100644
--- a/msgconv/urlpreview.go
+++ b/pkg/msgconv/urlpreview.go
@@ -18,37 +18,27 @@ 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))
+func (mc *MessageConverter) convertURLPreviewsToBeeper(ctx context.Context, preview []*signalpb.Preview) []*event.BeeperLinkPreview {
+ output := make([]*event.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{
+func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, preview *signalpb.Preview) *event.BeeperLinkPreview {
+ output := &event.BeeperLinkPreview{
MatchedURL: preview.GetUrl(),
- RespPreviewURL: mautrix.RespPreviewURL{
+ LinkPreview: event.LinkPreview{
CanonicalURL: preview.GetUrl(),
Title: preview.GetTitle(),
Description: preview.GetDescription(),
@@ -70,65 +60,34 @@ func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, previ
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 {
+func (mc *MessageConverter) convertURLPreviewToSignal(ctx context.Context, content *event.MessageEventContent) []*signalpb.Preview {
+ if len(content.BeeperLinkPreviews) == 0 {
return nil
}
- output := make([]*signalpb.Preview, len(previews))
- for i, preview := range previews {
+ output := make([]*signalpb.Preview, len(content.BeeperLinkPreviews))
+ for i, preview := range content.BeeperLinkPreviews {
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 preview.ImageURL != "" || preview.ImageEncryption != nil {
+ data, err := mc.Bridge.Bot.DownloadMedia(ctx, preview.ImageURL, preview.ImageEncryption)
if err != nil {
- log.Err(err).Int("preview_index", i).Msg("Failed to download URL preview image")
+ zerolog.Ctx(ctx).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")
+ zerolog.Ctx(ctx).Err(err).Int("preview_index", i).Msg("Failed to decrypt URL preview image")
continue
}
}
- uploaded, err := mc.GetClient(ctx).UploadAttachment(ctx, data)
+ uploaded, err := getClient(ctx).UploadAttachment(ctx, data)
if err != nil {
- log.Err(err).Int("preview_index", i).Msg("Failed to reupload URL preview image")
+ zerolog.Ctx(ctx).Err(err).Int("preview_index", i).Msg("Failed to reupload URL preview image")
continue
}
uploaded.ContentType = proto.String(preview.ImageType)
diff --git a/database/upgrades/upgrades.go b/pkg/signalid/dbmeta.go
similarity index 54%
rename from database/upgrades/upgrades.go
rename to pkg/signalid/dbmeta.go
index 20f60f4..2d85a0c 100644
--- a/database/upgrades/upgrades.go
+++ b/pkg/signalid/dbmeta.go
@@ -1,5 +1,5 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
-// Copyright (C) 2023 Tulir Asokan
+// 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
@@ -14,27 +14,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package upgrades
+package signalid
-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, 12, 0, "Unsupported version", dbutil.TxnModeOff, 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", dbutil.TxnModeOff, func(ctx context.Context, database *dbutil.Database) error {
- return nil
- })
- Table.RegisterFS(rawUpgrades)
+type PortalMetadata struct {
+ Revision uint32 `json:"revision"`
+}
+
+type MessageMetadata struct {
+ ContainsAttachments bool `json:"contains_attachments,omitempty"`
}
diff --git a/pkg/signalid/ids.go b/pkg/signalid/ids.go
new file mode 100644
index 0000000..f99bf9d
--- /dev/null
+++ b/pkg/signalid/ids.go
@@ -0,0 +1,105 @@
+// 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 .
+
+package signalid
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/google/uuid"
+ "maunium.net/go/mautrix/bridgev2/networkid"
+
+ "go.mau.fi/mautrix-signal/pkg/libsignalgo"
+ "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
+)
+
+func ParseUserID(userID networkid.UserID) (uuid.UUID, error) {
+ serviceID, err := ParseUserIDAsServiceID(userID)
+ if err != nil {
+ return uuid.Nil, err
+ } else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
+ return uuid.Nil, fmt.Errorf("invalid user ID: expected ACI type")
+ } else {
+ return serviceID.UUID, nil
+ }
+}
+
+func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) {
+ return libsignalgo.ServiceIDFromString(string(userID))
+}
+
+func ParsePortalID(portalID networkid.PortalID) (userID libsignalgo.ServiceID, groupID types.GroupIdentifier, err error) {
+ if len(portalID) == 44 {
+ groupID = types.GroupIdentifier(portalID)
+ } else {
+ userID, err = libsignalgo.ServiceIDFromString(string(portalID))
+ }
+ return
+}
+
+func ParseMessageID(messageID networkid.MessageID) (sender uuid.UUID, timestamp uint64, err error) {
+ parts := strings.Split(string(messageID), "|")
+ if len(parts) != 2 {
+ err = fmt.Errorf("invalid message ID: expected two pipe-separated parts")
+ return
+ }
+ sender, err = uuid.Parse(parts[0])
+ if err != nil {
+ return
+ }
+ timestamp, err = strconv.ParseUint(parts[1], 10, 64)
+ return
+}
+
+func MakeGroupPortalID(groupID types.GroupIdentifier) networkid.PortalID {
+ return networkid.PortalID(groupID)
+}
+
+func MakeGroupPortalKey(groupID types.GroupIdentifier) networkid.PortalKey {
+ return networkid.PortalKey{
+ ID: MakeGroupPortalID(groupID),
+ Receiver: "",
+ }
+}
+
+func MakeDMPortalID(serviceID libsignalgo.ServiceID) networkid.PortalID {
+ return networkid.PortalID(serviceID.String())
+}
+
+func MakeMessageID(sender uuid.UUID, timestamp uint64) networkid.MessageID {
+ return networkid.MessageID(fmt.Sprintf("%s|%d", sender, timestamp))
+}
+
+func MakeUserID(user uuid.UUID) networkid.UserID {
+ return networkid.UserID(user.String())
+}
+
+func MakeUserIDFromServiceID(user libsignalgo.ServiceID) networkid.UserID {
+ return networkid.UserID(user.String())
+}
+
+func MakeUserLoginID(user uuid.UUID) networkid.UserLoginID {
+ return networkid.UserLoginID(user.String())
+}
+
+func MakeMessagePartID(index int) networkid.PartID {
+ if index == 0 {
+ return ""
+ }
+ return networkid.PartID(strconv.Itoa(index))
+}
diff --git a/portal.go b/portal.go
deleted file mode 100644
index 37fabe6..0000000
--- a/portal.go
+++ /dev/null
@@ -1,3026 +0,0 @@
-// 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 .
-
-package main
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "errors"
- "fmt"
- "net/http"
- "reflect"
- "strings"
- "sync"
- "time"
-
- "github.com/google/uuid"
- "github.com/rs/zerolog"
- "go.mau.fi/util/exfmt"
- "go.mau.fi/util/jsontime"
- "go.mau.fi/util/variationselector"
- "google.golang.org/protobuf/proto"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/appservice"
- "maunium.net/go/mautrix/bridge"
- "maunium.net/go/mautrix/bridge/bridgeconfig"
- "maunium.net/go/mautrix/bridge/status"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mautrix-signal/database"
- "go.mau.fi/mautrix-signal/msgconv"
- "go.mau.fi/mautrix-signal/msgconv/matrixfmt"
- "go.mau.fi/mautrix-signal/msgconv/signalfmt"
- "go.mau.fi/mautrix-signal/pkg/libsignalgo"
- "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 (br *SignalBridge) GetPortalByMXID(mxid id.RoomID) *Portal {
- br.portalsLock.Lock()
- defer br.portalsLock.Unlock()
-
- portal, ok := br.portalsByMXID[mxid]
- if !ok {
- dbPortal, err := br.DB.Portal.GetByMXID(context.TODO(), mxid)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get portal from database")
- return nil
- }
- return br.loadPortal(context.TODO(), dbPortal, nil)
- }
-
- return portal
-}
-
-func (br *SignalBridge) GetPortalByChatID(key database.PortalKey) *Portal {
- br.portalsLock.Lock()
- defer br.portalsLock.Unlock()
- return br.unlockedGetPortalByChatID(key, true)
-}
-
-func (br *SignalBridge) GetPortalByChatIDIfExists(key database.PortalKey) *Portal {
- br.portalsLock.Lock()
- defer br.portalsLock.Unlock()
- return br.unlockedGetPortalByChatID(key, false)
-}
-
-func (br *SignalBridge) unlockedGetPortalByChatID(key database.PortalKey, createIfNotExists bool) *Portal {
- // If this PortalKey is for a group, Receiver should be empty
- if key.UserID().IsEmpty() {
- key.Receiver = uuid.Nil
- }
- portal, ok := br.portalsByID[key]
- if !ok {
- dbPortal, err := br.DB.Portal.GetByChatID(context.TODO(), key)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get portal from database")
- return nil
- }
- keyIfNotExists := &key
- if !createIfNotExists {
- keyIfNotExists = nil
- }
- return br.loadPortal(context.TODO(), dbPortal, keyIfNotExists)
- }
- return portal
-}
-
-func (br *SignalBridge) GetAllPortalsWithMXID() []*Portal {
- portals, err := br.dbPortalsToPortals(br.DB.Portal.GetAllWithMXID(context.TODO()))
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get all portals with mxid")
- return nil
- }
- return portals
-}
-
-func (br *SignalBridge) FindPrivateChatPortalsWith(userID uuid.UUID) []*Portal {
- portals, err := br.dbPortalsToPortals(br.DB.Portal.FindPrivateChatsWith(context.TODO(), userID))
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get all DM portals with user")
- return nil
- }
- return portals
-}
-
-func (br *SignalBridge) GetAllIPortals() (iportals []bridge.Portal) {
- portals, err := br.dbPortalsToPortals(br.DB.Portal.GetAllWithMXID(context.TODO()))
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get all portals with mxid")
- return nil
- }
- iportals = make([]bridge.Portal, len(portals))
- for i, portal := range portals {
- iportals[i] = portal
- }
- return iportals
-}
-
-func (br *SignalBridge) loadPortal(ctx context.Context, dbPortal *database.Portal, key *database.PortalKey) *Portal {
- if dbPortal == nil {
- if key == nil {
- return nil
- }
-
- dbPortal = br.DB.Portal.New()
- dbPortal.PortalKey = *key
- err := dbPortal.Insert(ctx)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to insert new portal")
- return nil
- }
- }
-
- portal := br.NewPortal(dbPortal)
-
- br.portalsByID[portal.PortalKey] = portal
- if portal.MXID != "" {
- br.portalsByMXID[portal.MXID] = portal
- }
-
- return portal
-}
-
-func (br *SignalBridge) dbPortalsToPortals(dbPortals []*database.Portal, err error) ([]*Portal, error) {
- if err != nil {
- return nil, err
- }
- br.portalsLock.Lock()
- defer br.portalsLock.Unlock()
-
- output := make([]*Portal, len(dbPortals))
- for index, dbPortal := range dbPortals {
- if dbPortal == nil {
- continue
- }
-
- portal, ok := br.portalsByID[dbPortal.PortalKey]
- if !ok {
- portal = br.loadPortal(context.TODO(), dbPortal, nil)
- }
-
- output[index] = portal
- }
-
- return output, nil
-}
-
-type portalSignalMessage struct {
- evt *events.ChatEvent
- user *User
-}
-
-type portalMatrixMessage struct {
- evt *event.Event
- user *User
-}
-
-type Portal struct {
- *database.Portal
-
- MsgConv *msgconv.MessageConverter
-
- bridge *SignalBridge
- log zerolog.Logger
-
- roomCreateLock sync.Mutex
- encryptLock sync.Mutex
-
- signalMessages chan portalSignalMessage
- matrixMessages chan portalMatrixMessage
-
- currentlyTyping []id.UserID
- currentlyTypingLock sync.Mutex
-
- relayUser *User
-}
-
-var signalFormatParams *signalfmt.FormatParams
-var matrixFormatParams *matrixfmt.HTMLParser
-
-func (br *SignalBridge) NewPortal(dbPortal *database.Portal) *Portal {
- log := br.ZLog.With().Str("chat_id", dbPortal.ChatID).Logger()
- if dbPortal.MXID != "" {
- log = log.With().Stringer("room_id", dbPortal.MXID).Logger()
- }
-
- portal := &Portal{
- Portal: dbPortal,
- bridge: br,
- log: log,
-
- signalMessages: make(chan portalSignalMessage, br.Config.Bridge.PortalMessageBuffer),
- matrixMessages: make(chan portalMatrixMessage, br.Config.Bridge.PortalMessageBuffer),
- }
- portal.MsgConv = &msgconv.MessageConverter{
- PortalMethods: portal,
- SignalFmtParams: signalFormatParams,
- MatrixFmtParams: matrixFormatParams,
- ConvertVoiceMessages: true,
- MaxFileSize: br.MediaConfig.UploadSize,
- LocationFormat: br.Config.Bridge.LocationFormat,
- }
- go portal.messageLoop()
-
- return portal
-}
-
-func init() {
- event.TypeMap[event.StateBridge] = reflect.TypeOf(CustomBridgeInfoContent{})
- event.TypeMap[event.StateHalfShotBridge] = reflect.TypeOf(CustomBridgeInfoContent{})
-}
-
-var (
- _ bridge.Portal = (*Portal)(nil)
- _ bridge.ReadReceiptHandlingPortal = (*Portal)(nil)
- _ bridge.TypingPortal = (*Portal)(nil)
- _ bridge.DisappearingPortal = (*Portal)(nil)
- //_ bridge.MembershipHandlingPortal = (*Portal)(nil)
- //_ bridge.MetaHandlingPortal = (*Portal)(nil)
-)
-
-func (portal *Portal) IsEncrypted() bool {
- return portal.Encrypted
-}
-
-func (portal *Portal) MarkEncrypted() {
- portal.Encrypted = true
- err := portal.Update(context.TODO())
- if err != nil {
- portal.log.Err(err).Msg("Failed to update portal in database after marking as encrypted")
- }
-}
-
-func (portal *Portal) ReceiveMatrixEvent(user bridge.User, evt *event.Event) {
- if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser || portal.HasRelaybot() {
- portal.matrixMessages <- portalMatrixMessage{user: user.(*User), evt: evt}
- }
-}
-
-func (portal *Portal) GetRelayUser() *User {
- if !portal.HasRelaybot() {
- return nil
- } else if portal.relayUser == nil {
- portal.relayUser = portal.bridge.GetUserByMXID(portal.RelayUserID)
- }
- return portal.relayUser
-}
-
-func (portal *Portal) IsPrivateChat() bool {
- return !portal.UserID().IsEmpty()
-}
-
-func (portal *Portal) IsNoteToSelf() bool {
- userID := portal.UserID()
- return !userID.IsEmpty() && userID.UUID == portal.Receiver
-}
-
-func (portal *Portal) MainIntent() *appservice.IntentAPI {
- dmPuppet := portal.GetDMPuppet()
- if dmPuppet != nil {
- return dmPuppet.DefaultIntent()
- }
-
- return portal.bridge.Bot
-}
-
-type CustomBridgeInfoContent struct {
- event.BridgeEventContent
- RoomType string `json:"com.beeper.room_type,omitempty"`
-}
-
-func (portal *Portal) getBridgeInfo() (string, CustomBridgeInfoContent) {
- bridgeInfo := event.BridgeEventContent{
- BridgeBot: portal.bridge.Bot.UserID,
- Creator: portal.MainIntent().UserID,
- Protocol: event.BridgeInfoSection{
- ID: "signal",
- DisplayName: "Signal",
- AvatarURL: portal.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
- ExternalURL: "https://signal.org/",
- },
- Channel: event.BridgeInfoSection{
- ID: portal.ChatID,
- DisplayName: portal.Name,
- AvatarURL: portal.AvatarURL.CUString(),
- },
- }
- bridgeInfoStateKey := fmt.Sprintf("fi.mau.signal://signal/%s", portal.ChatID)
- bridgeInfo.Channel.ExternalURL = fmt.Sprintf("https://signal.me/#p/%s", portal.ChatID)
- var roomType string
- if portal.IsPrivateChat() {
- roomType = "dm"
- }
- return bridgeInfoStateKey, CustomBridgeInfoContent{bridgeInfo, roomType}
-}
-
-func (portal *Portal) UpdateBridgeInfo(ctx context.Context) {
- if len(portal.MXID) == 0 {
- portal.log.Debug().Msg("Not updating bridge info: no Matrix room created")
- return
- }
- portal.log.Debug().Msg("Updating bridge info...")
- stateKey, content := portal.getBridgeInfo()
- _, err := portal.MainIntent().SendStateEvent(ctx, portal.MXID, event.StateBridge, stateKey, content)
- if err != nil {
- portal.log.Warn().Err(err).Msg("Failed to update m.bridge")
- }
- // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
- _, err = portal.MainIntent().SendStateEvent(ctx, portal.MXID, event.StateHalfShotBridge, stateKey, content)
- if err != nil {
- portal.log.Warn().Err(err).Msg("Failed to update uk.half-shot.bridge")
- }
-}
-
-func (portal *Portal) messageLoop() {
- for {
- select {
- case msg := <-portal.matrixMessages:
- portal.handleMatrixMessages(msg)
- case msg := <-portal.signalMessages:
- portal.handleSignalMessage(msg)
- }
- }
-}
-
-func (portal *Portal) handleMatrixMessages(msg portalMatrixMessage) {
- log := portal.log.With().
- Str("action", "handle matrix event").
- Stringer("event_id", msg.evt.ID).
- Str("event_type", msg.evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
-
- switch msg.evt.Type {
- case event.EventMessage, event.EventSticker:
- portal.handleMatrixMessage(ctx, msg.user, msg.evt)
- case event.EventRedaction:
- portal.handleMatrixRedaction(ctx, msg.user, msg.evt)
- case event.EventReaction:
- portal.handleMatrixReaction(ctx, msg.user, msg.evt)
- default:
- log.Warn().Str("type", msg.evt.Type.Type).Msg("Unhandled matrix message type")
- }
-}
-
-func (portal *Portal) handleMatrixMessage(ctx context.Context, sender *User, evt *event.Event) {
- log := zerolog.Ctx(ctx)
- evtTS := time.UnixMilli(evt.Timestamp)
- timings := messageTimings{
- initReceive: evt.Mautrix.ReceivedAt.Sub(evtTS),
- decrypt: evt.Mautrix.DecryptionDuration,
- totalReceive: time.Since(evtTS),
- }
- implicitRRStart := time.Now()
- portal.handleMatrixReadReceipt(sender, "", uint64(evt.Timestamp), false)
- timings.implicitRR = time.Since(implicitRRStart)
- start := time.Now()
-
- messageAge := timings.totalReceive
- ms := metricSender{portal: portal, timings: &timings, ctx: ctx}
- log.Debug().
- Stringer("sender", evt.Sender).
- Dur("age", messageAge).
- Msg("Received message")
-
- errorAfter := portal.bridge.Config.Bridge.MessageHandlingTimeout.ErrorAfter
- deadline := portal.bridge.Config.Bridge.MessageHandlingTimeout.Deadline
- isScheduled, _ := evt.Content.Raw["com.beeper.scheduled"].(bool)
- if isScheduled {
- log.Debug().Msg("Message is a scheduled message, extending handling timeouts")
- errorAfter *= 10
- deadline *= 10
- }
-
- if errorAfter > 0 {
- remainingTime := errorAfter - messageAge
- if remainingTime < 0 {
- go ms.sendMessageMetrics(evt, errTimeoutBeforeHandling, "Timeout handling", true)
- return
- } else if remainingTime < 1*time.Second {
- log.Warn().
- Dur("remaining_time", remainingTime).
- Dur("max_timeout", errorAfter).
- Msg("Message was delayed before reaching the bridge")
- }
- go func() {
- time.Sleep(remainingTime)
- ms.sendMessageMetrics(evt, errMessageTakingLong, "Timeout handling", false)
- }()
- }
-
- if deadline > 0 {
- var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(ctx, deadline)
- defer cancel()
- }
-
- timings.preproc = time.Since(start)
- start = time.Now()
-
- content, ok := evt.Content.Parsed.(*event.MessageEventContent)
- if !ok {
- log.Error().Type("content_type", content).Msg("Unexpected parsed content type")
- go ms.sendMessageMetrics(evt, fmt.Errorf("%w %T", errUnexpectedParsedContentType, evt.Content.Parsed), "Error converting", true)
- return
- }
-
- realSenderMXID := sender.MXID
- isRelay := false
- if !sender.IsLoggedIn() {
- sender = portal.GetRelayUser()
- if sender == nil {
- go ms.sendMessageMetrics(evt, errUserNotLoggedIn, "Ignoring", true)
- return
- } else if !sender.IsLoggedIn() {
- go ms.sendMessageMetrics(evt, errRelaybotNotLoggedIn, "Ignoring", true)
- return
- }
- isRelay = true
- }
-
- var editTargetMsg *database.Message
- if editTarget := content.RelatesTo.GetReplaceID(); editTarget != "" {
- var err error
- editTargetMsg, err = portal.bridge.DB.Message.GetByMXID(ctx, editTarget)
- if err != nil {
- log.Err(err).Stringer("edit_target_mxid", editTarget).Msg("Failed to get edit target message")
- go ms.sendMessageMetrics(evt, errFailedToGetEditTarget, "Error converting", true)
- return
- } else if editTargetMsg == nil {
- log.Err(err).Stringer("edit_target_mxid", editTarget).Msg("Edit target message not found")
- go ms.sendMessageMetrics(evt, errEditUnknownTarget, "Error converting", true)
- return
- } else if editTargetMsg.Sender != sender.SignalID {
- go ms.sendMessageMetrics(evt, errEditDifferentSender, "Error converting", true)
- return
- }
- if content.NewContent != nil {
- content = content.NewContent
- evt.Content.Parsed = content
- }
- }
-
- relaybotFormatted := isRelay && portal.addRelaybotFormat(ctx, realSenderMXID, evt, content)
- if content.MsgType == event.MsgNotice && !portal.bridge.Config.Bridge.BridgeNotices {
- go ms.sendMessageMetrics(evt, errMNoticeDisabled, "Error converting", true)
- return
- }
- ctx = context.WithValue(ctx, msgconvContextKeyClient, sender.Client)
- msg, err := portal.MsgConv.ToSignal(ctx, evt, content, relaybotFormatted)
- if err != nil {
- log.Err(err).Msg("Failed to convert message")
- go ms.sendMessageMetrics(evt, err, "Error converting", true)
- return
- }
- var wrappedMsg *signalpb.Content
- if editTargetMsg == nil {
- wrappedMsg = &signalpb.Content{
- DataMessage: msg,
- }
- } else {
- wrappedMsg = &signalpb.Content{
- EditMessage: &signalpb.EditMessage{
- TargetSentTimestamp: proto.Uint64(editTargetMsg.Timestamp),
- DataMessage: msg,
- },
- }
- }
-
- timings.convert = time.Since(start)
- start = time.Now()
-
- err = portal.sendSignalMessage(ctx, wrappedMsg, sender, evt.ID)
-
- timings.totalSend = time.Since(start)
- go ms.sendMessageMetrics(evt, err, "Error sending", true)
- if err == nil {
- if editTargetMsg != nil {
- err = editTargetMsg.SetTimestamp(ctx, msg.GetTimestamp())
- if err != nil {
- log.Err(err).Msg("Failed to update message timestamp in database after editing")
- }
- } else {
- portal.storeMessageInDB(ctx, evt.ID, sender.SignalID, msg.GetTimestamp(), 0)
- if portal.ExpirationTime > 0 {
- portal.addDisappearingMessage(ctx, evt.ID, uint32(portal.ExpirationTime), true)
- }
- }
- }
-}
-
-func (portal *Portal) handleMatrixRedaction(ctx context.Context, sender *User, evt *event.Event) {
- log := zerolog.Ctx(ctx)
- // Find the original signal message based on eventID
- dbMessage, err := portal.bridge.DB.Message.GetByMXID(ctx, evt.Redacts)
- if err != nil {
- log.Err(err).Msg("Failed to get redaction target message")
- }
- // Might be a reaction redaction, find the original message for the reaction
- dbReaction, err := portal.bridge.DB.Reaction.GetByMXID(ctx, evt.Redacts)
- if err != nil {
- log.Err(err).Msg("Failed to get redaction target reaction")
- }
-
- if !sender.IsLoggedIn() {
- sender = portal.GetRelayUser()
- if sender == nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errUserNotLoggedIn)
- return
- } else if !sender.IsLoggedIn() {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errRelaybotNotLoggedIn)
- return
- }
- }
-
- if dbMessage != nil {
- if dbMessage.Sender != sender.SignalID {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errRedactionTargetSentBySomeoneElse)
- return
- }
- msg := signalmeow.DataMessageForDelete(dbMessage.Timestamp)
- err = portal.sendSignalMessage(ctx, msg, sender, evt.ID)
- if err != nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, err)
- log.Err(err).Msg("Failed to send message redaction to Signal")
- return
- }
- err = dbMessage.Delete(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to delete redacted message from database")
- } else if otherParts, err := portal.bridge.DB.Message.GetAllPartsBySignalID(ctx, dbMessage.Sender, dbMessage.Timestamp, portal.Receiver); err != nil {
- log.Err(err).Msg("Failed to get other parts of redacted message from database")
- } else if len(otherParts) > 0 {
- // If there are other parts of the message, send a redaction for each of them
- for _, otherPart := range otherParts {
- _, err = portal.MainIntent().RedactEvent(ctx, portal.MXID, otherPart.MXID, mautrix.ReqRedact{
- Reason: "Other part of Signal message redacted",
- TxnID: "mxsg_partredact_" + otherPart.MXID.String(),
- })
- if err != nil {
- log.Err(err).
- Stringer("part_event_id", otherPart.MXID).
- Int("part_index", otherPart.PartIndex).
- Msg("Failed to redact other part of redacted message")
- }
- err = otherPart.Delete(ctx)
- if err != nil {
- log.Err(err).
- Stringer("part_event_id", otherPart.MXID).
- Int("part_index", otherPart.PartIndex).
- Msg("Failed to delete other part of redacted message from database")
- }
- }
- }
- portal.sendMessageStatusCheckpointSuccess(ctx, evt)
- } else if dbReaction != nil {
- if dbReaction.Author != sender.SignalID {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errUnreactTargetSentBySomeoneElse)
- return
- }
- msg := signalmeow.DataMessageForReaction(dbReaction.Emoji, dbReaction.MsgAuthor, dbReaction.MsgTimestamp, true)
- err = portal.sendSignalMessage(ctx, msg, sender, evt.ID)
- if err != nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, err)
- log.Err(err).Msg("Failed to send reaction redaction to Signal")
- return
- }
- err = dbReaction.Delete(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to delete redacted reaction from database")
- }
- portal.sendMessageStatusCheckpointSuccess(ctx, evt)
- } else {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errRedactionTargetNotFound)
- }
-}
-
-func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *User, evt *event.Event) {
- log := zerolog.Ctx(ctx)
- if !sender.IsLoggedIn() {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errCantRelayReactions)
- return
- }
- // Find the original signal message based on eventID
- relatedEventID := evt.Content.AsReaction().RelatesTo.EventID
- targetMsg, err := portal.bridge.DB.Message.GetByMXID(ctx, relatedEventID)
- if err != nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, err)
- log.Err(err).Msg("Failed to get reaction target message")
- return
- } else if targetMsg == nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, errReactionTargetNotFound)
- log.Warn().Msg("Reaction target message not found")
- return
- }
- emoji := evt.Content.AsReaction().RelatesTo.Key
- signalEmoji := variationselector.FullyQualify(emoji) // Signal seems to require fully qualified emojis
- msg := signalmeow.DataMessageForReaction(signalEmoji, targetMsg.Sender, targetMsg.Timestamp, false)
- err = portal.sendSignalMessage(ctx, msg, sender, evt.ID)
- if err != nil {
- portal.sendMessageStatusCheckpointFailed(ctx, evt, err)
- log.Error().Msg("Failed to send reaction")
- return
- }
-
- // Signal only allows one reaction from each user
- // Check if there's an existing reaction in the database for this sender and redact/delete it
- dbReaction, err := portal.bridge.DB.Reaction.GetBySignalID(
- ctx,
- targetMsg.Sender,
- targetMsg.Timestamp,
- sender.SignalID,
- portal.Receiver,
- )
- if err != nil {
- log.Err(err).Msg("Failed to get existing reaction from database")
- } else if dbReaction != nil {
- log.Debug().Stringer("existing_event_id", dbReaction.MXID).Msg("Redacting existing reaction after sending new one")
- _, err = portal.MainIntent().RedactEvent(ctx, portal.MXID, dbReaction.MXID)
- if err != nil {
- log.Err(err).Msg("Failed to redact existing reaction")
- }
- }
- if dbReaction != nil {
- dbReaction.MXID = evt.ID
- dbReaction.Emoji = signalEmoji
- err = dbReaction.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to update reaction in database")
- }
- } else {
- dbReaction = portal.bridge.DB.Reaction.New()
- dbReaction.MXID = evt.ID
- dbReaction.RoomID = portal.MXID
- dbReaction.SignalChatID = portal.ChatID
- dbReaction.SignalReceiver = portal.Receiver
- dbReaction.Author = sender.SignalID
- dbReaction.MsgAuthor = targetMsg.Sender
- dbReaction.MsgTimestamp = targetMsg.Timestamp
- dbReaction.Emoji = signalEmoji
- err = dbReaction.Insert(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to insert reaction to database")
- }
- }
-
- portal.sendMessageStatusCheckpointSuccess(ctx, evt)
-}
-
-func (portal *Portal) sendSignalMessage(ctx context.Context, msg *signalpb.Content, sender *User, evtID id.EventID) error {
- log := zerolog.Ctx(ctx).With().
- Str("action", "send signal message").
- Stringer("event_id", evtID).
- Str("portal_chat_id", portal.ChatID).
- Logger()
- ctx = log.WithContext(ctx)
-
- log.Debug().Msg("Sending event to Signal")
-
- // Check to see if portal.ChatID is a standard UUID (with dashes)
- if portal.IsPrivateChat() {
- // this is a 1:1 chat
- result := sender.Client.SendMessage(ctx, portal.UserID(), msg)
- if !result.WasSuccessful {
- return result.Error
- }
- } else {
- // this is a group chat
- groupID := types.GroupIdentifier(portal.ChatID)
- result, err := sender.Client.SendGroupMessage(ctx, groupID, msg)
- if err != nil {
- // check the start of the error string, see if it starts with "No group master key found for group identifier"
- if strings.HasPrefix(err.Error(), "No group master key found for group identifier") {
- portal.MainIntent().SendNotice(ctx, portal.MXID, "Missing group encryption key. Please ask a group member to send a message in this chat, then retry sending.")
- }
- log.Err(err).Msg("Error sending event to Signal group")
- return err
- }
- totalRecipients := len(result.FailedToSendTo) + len(result.SuccessfullySentTo)
- log = log.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.FailedToSendTo) > 0 {
- log.Error().Msg("Failed to send event to some members of Signal group")
- }
- 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 {
- 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
-}
-
-func (portal *Portal) sendMessageStatusCheckpointSuccess(ctx context.Context, evt *event.Event) {
- portal.sendDeliveryReceipt(ctx, evt.ID)
- portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, 0)
-
- var deliveredTo *[]id.UserID
- if portal.IsPrivateChat() {
- deliveredTo = &[]id.UserID{}
- }
- portal.sendStatusEvent(ctx, evt.ID, "", nil, deliveredTo)
-}
-
-func (portal *Portal) sendMessageStatusCheckpointFailed(ctx context.Context, evt *event.Event, err error) {
- portal.sendDeliveryReceipt(ctx, evt.ID)
- portal.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, err, true, 0)
- portal.sendStatusEvent(ctx, evt.ID, "", err, nil)
-}
-
-type msgconvContextKey int
-
-const (
- msgconvContextKeyIntent msgconvContextKey = iota
- msgconvContextKeyClient
-)
-
-func (portal *Portal) UploadMatrixMedia(ctx context.Context, data []byte, fileName, contentType string) (id.ContentURIString, error) {
- intent := ctx.Value(msgconvContextKeyIntent).(*appservice.IntentAPI)
- req := mautrix.ReqUploadMedia{
- ContentBytes: data,
- ContentType: contentType,
- FileName: fileName,
- }
- if portal.bridge.Config.Homeserver.AsyncMedia {
- uploaded, err := intent.UploadAsync(ctx, req)
- if err != nil {
- return "", err
- }
- return uploaded.ContentURI.CUString(), nil
- } else {
- uploaded, err := intent.UploadMedia(ctx, req)
- if err != nil {
- return "", err
- }
- return uploaded.ContentURI.CUString(), nil
- }
-}
-
-func (portal *Portal) DownloadMatrixMedia(ctx context.Context, uriString id.ContentURIString) ([]byte, error) {
- parsedURI, err := uriString.Parse()
- if err != nil {
- return nil, fmt.Errorf("malformed content URI: %w", err)
- }
- return portal.MainIntent().DownloadBytes(ctx, parsedURI)
-}
-
-func (portal *Portal) GetData(ctx context.Context) *database.Portal {
- return portal.Portal
-}
-
-func (portal *Portal) GetClient(ctx context.Context) *signalmeow.Client {
- return ctx.Value(msgconvContextKeyClient).(*signalmeow.Client)
-}
-
-func (portal *Portal) GetMatrixReply(ctx context.Context, msg *signalpb.DataMessage_Quote) (replyTo id.EventID, replyTargetSender id.UserID) {
- if msg == nil {
- return
- }
- log := zerolog.Ctx(ctx).With().
- Str("reply_target_author", msg.GetAuthorAci()).
- Uint64("reply_target_ts", msg.GetId()).
- Logger()
- if senderUUID, err := uuid.Parse(msg.GetAuthorAci()); err != nil {
- log.Err(err).Msg("Failed to parse sender UUID in Signal quote")
- } else if message, err := portal.bridge.DB.Message.GetBySignalID(ctx, senderUUID, msg.GetId(), 0, portal.Receiver); err != nil {
- log.Err(err).Msg("Failed to get reply target message from database")
- } else if message == nil {
- log.Warn().Msg("Reply target message not found")
- } else {
- replyTo = message.MXID
- targetUser := portal.bridge.GetUserBySignalID(message.Sender)
- if targetUser != nil {
- replyTargetSender = targetUser.MXID
- } else {
- replyTargetSender = portal.bridge.FormatPuppetMXID(message.Sender)
- }
- }
- return
-}
-
-func (portal *Portal) GetSignalReply(ctx context.Context, content *event.MessageEventContent) *signalpb.DataMessage_Quote {
- replyToID := content.RelatesTo.GetReplyTo()
- if len(replyToID) == 0 {
- return nil
- }
- replyToMsg, err := portal.bridge.DB.Message.GetByMXID(ctx, replyToID)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).
- Stringer("reply_to_mxid", replyToID).
- Msg("Failed to get reply target message from database")
- } else if replyToMsg == nil {
- zerolog.Ctx(ctx).Warn().
- Stringer("reply_to_mxid", replyToID).
- Msg("Reply target message not found")
- } else {
- return &signalpb.DataMessage_Quote{
- Id: proto.Uint64(replyToMsg.Timestamp),
- AuthorAci: proto.String(replyToMsg.Sender.String()),
- Type: signalpb.DataMessage_Quote_NORMAL.Enum(),
-
- // This is a hack to make Signal iOS and desktop render replies to file messages.
- // Unfortunately it also makes Signal Desktop show a file icon on replies to text messages.
- // TODO store file or text flag in database and fill this field only when replying to file messages.
- Attachments: make([]*signalpb.DataMessage_Quote_QuotedAttachment, 1),
- }
- }
- return nil
-}
-
-func (portal *Portal) handleSignalMessage(portalMessage portalSignalMessage) {
- sender := portal.bridge.GetPuppetBySignalID(portalMessage.evt.Info.Sender)
- if sender == nil {
- portal.log.Warn().
- Stringer("sender_uuid", portalMessage.evt.Info.Sender).
- Msg("Couldn't get puppet for message")
- return
- }
- var msgType string
- var timestamp uint64
- switch typedEvt := portalMessage.evt.Event.(type) {
- case *signalpb.DataMessage:
- msgType = "data"
- timestamp = typedEvt.GetTimestamp()
- portal.handleSignalDataMessage(portalMessage.user, sender, typedEvt)
- case *signalpb.TypingMessage:
- msgType = "typing"
- timestamp = typedEvt.GetTimestamp()
- portal.handleSignalTypingMessage(sender, typedEvt)
- case *signalpb.EditMessage:
- msgType = "edit"
- timestamp = typedEvt.GetTargetSentTimestamp()
- portal.handleSignalEditMessage(sender, timestamp, typedEvt.GetDataMessage())
- default:
- portal.log.Error().
- Type("data_type", typedEvt).
- Msg("Invalid inner event type inside ChatEvent")
- }
- portal.bridge.Metrics.TrackSignalMessage(time.UnixMilli(int64(timestamp)), msgType)
-}
-
-func (portal *Portal) handleSignalDataMessage(source *User, sender *Puppet, msg *signalpb.DataMessage) {
- genericCtx := portal.log.With().
- Str("action", "handle signal data message").
- Uint64("msg_ts", msg.GetTimestamp()).
- Logger().WithContext(context.TODO())
- // Always update sender info when we receive a message from them, there's caching inside the function
- sender.UpdateInfo(genericCtx, source, nil)
- // Handle earlier missed group changes here.
- if msg.GetGroupV2() != nil {
- requiredRevision := msg.GetGroupV2().GetRevision()
- if msg.GetGroupV2().GetGroupChange() != nil {
- requiredRevision = requiredRevision - 1
- }
- if portal.Revision < requiredRevision {
- err := portal.catchUpHistory(source, portal.Revision+1, requiredRevision, msg.GetTimestamp())
- if err != nil {
- portal.log.Err(err).Msg("Failed to catch up group history, trying regular update")
- portal.UpdateInfo(genericCtx, source, nil, msg.GetGroupV2().GetRevision())
- }
- }
- } else if portal.IsPrivateChat() && portal.UserID().UUID == portal.Receiver && portal.Name != NoteToSelfName {
- // Slightly hacky way to make note to self names backfill
- portal.UpdateDMInfo(genericCtx, false)
- }
-
- switch {
- case msgconv.CanConvertSignal(msg):
- portal.handleSignalNormalDataMessage(source, sender, msg)
- case msg.Reaction != nil:
- portal.handleSignalReaction(sender, msg.Reaction, msg.GetTimestamp())
- case msg.Delete != nil:
- portal.handleSignalDelete(sender, msg.Delete, msg.GetTimestamp())
- case msg.GetGroupV2().GetGroupChange() != nil:
- portal.handleSignalGroupChange(source, sender, msg.GroupV2, msg.GetTimestamp())
- case msg.StoryContext != nil, msg.GroupCallUpdate != nil:
- // ignore
- default:
- portal.log.Warn().
- Str("action", "handle signal message").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("msg_ts", msg.GetTimestamp()).
- Msg("Unrecognized content in message")
- }
-}
-
-func (portal *Portal) catchUpHistory(source *User, fromRevision uint32, toRevision uint32, ts uint64) error {
- log := portal.log.With().
- Str("action", "catchUpHistory").
- Stringer("source", source.MXID).
- Uint32("from_revision", fromRevision).
- Uint32("to_revision", toRevision).
- Logger()
- ctx := log.WithContext(context.TODO())
- groupChanges, err := source.Client.GetGroupHistoryPage(ctx, portal.GroupID(), fromRevision, false)
- if err != nil {
- log.Err(err).Msg("Failed to get GroupChanges")
- return err
- }
- for _, groupChangeState := range groupChanges {
- sender := portal.bridge.GetPuppetBySignalID(groupChangeState.GroupChange.SourceACI)
- portal.applySignalGroupChange(ctx, source, sender, groupChangeState.GroupChange, ts)
- // for revision > toRevision, we should have received a group change already
- if groupChangeState.GroupChange.Revision == toRevision {
- break
- }
- }
- return nil
-}
-
-func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, groupMeta *signalpb.GroupContextV2, ts uint64) {
- log := portal.log.With().
- Str("action", "handle signal group change").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("change_ts", ts).
- Uint32("new_revision", groupMeta.GetRevision()).
- Logger()
- ctx := log.WithContext(context.TODO())
- groupChange, err := source.Client.DecryptGroupChange(ctx, groupMeta)
- if err != nil {
- log.Err(err).Msg("Handling GroupChange failed")
- return
- }
- portal.applySignalGroupChange(ctx, source, sender, groupChange, ts)
-}
-
-func (portal *Portal) applySignalGroupChange(ctx context.Context, source *User, sender *Puppet, groupChange *signalmeow.GroupChange, ts uint64) {
- log := zerolog.Ctx(ctx)
- if groupChange.Revision <= portal.Revision {
- return
- }
- portal.Revision = groupChange.Revision
- if groupChange.ModifyTitle != nil {
- portal.updateName(ctx, *groupChange.ModifyTitle, sender)
- }
- if groupChange.ModifyDescription != nil {
- portal.updateTopic(ctx, *groupChange.ModifyDescription, sender)
- }
- if groupChange.ModifyAvatar != nil {
- portal.updateAvatarWithInfo(ctx, source, groupChange, sender)
- }
- if groupChange.ModifyDisappearingMessagesDuration != nil {
- portal.updateExpirationTimer(ctx, *groupChange.ModifyDisappearingMessagesDuration)
- }
- intent := sender.IntentFor(portal)
- modifyRoles := groupChange.ModifyMemberRoles
- var err error
- for _, deleteBannedMember := range groupChange.DeleteBannedMembers {
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, *&deleteBannedMember.UUID, event.MembershipLeave, "unbanned")
- if err != nil {
- log.Warn().Stringer("signal_user_id", deleteBannedMember).Msg("Couldn't get puppet for unban")
- }
- }
- for _, addMember := range groupChange.AddMembers {
- modifyRoles = append(modifyRoles, &signalmeow.RoleMember{ACI: addMember.ACI, Role: addMember.Role})
- var puppet *Puppet
- if addMember.JoinFromInviteLink {
- puppet = portal.bridge.GetPuppetBySignalID(addMember.ACI)
- if puppet != nil {
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(addMember.ACI)
- if user != nil {
- portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, user.MXID, event.MembershipInvite, "Joined via invite Link")
- }
- }
- _, err = puppet.IntentFor(portal).SendCustomMembershipEvent(ctx, portal.MXID, puppet.IntentFor(portal).UserID, event.MembershipJoin, "")
- if errors.Is(err, mautrix.MForbidden) {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, puppet.IntentFor(portal).UserID, event.MembershipInvite, "Joined via invite Link")
- } else if err == nil {
- continue
- }
- }
- } else {
- puppet, err = portal.sendMembershipForPuppetAndUser(ctx, sender, addMember.ACI, event.MembershipInvite, "added")
- }
- if err != nil {
- log.Err(err).Stringer("signal_user_id", addMember.ACI).Msg("Couldn't get puppet for invite")
- return
- }
- _, err = puppet.IntentFor(portal).SendCustomMembershipEvent(ctx, portal.MXID, puppet.IntentFor(portal).UserID, event.MembershipJoin, "")
- if err != nil {
- log.Err(err).Stringer("mxid", puppet.MXID).Msg("Failed to join user")
- }
- }
- bannedMembers := make(map[uuid.UUID]bool)
- for _, addBannedMember := range groupChange.AddBannedMembers {
- if addBannedMember.ServiceID.Type == libsignalgo.ServiceIDTypePNI {
- continue
- }
- bannedMembers[addBannedMember.ServiceID.UUID] = true
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, addBannedMember.ServiceID.UUID, event.MembershipBan, "banned")
- if err != nil {
- log.Warn().Stringer("signal_user_id", addBannedMember.ServiceID.UUID).Msg("Couldn't get puppet for ban")
- }
- }
- for _, deleteMember := range groupChange.DeleteMembers {
- if bannedMembers[*deleteMember] {
- continue
- }
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, *deleteMember, event.MembershipLeave, "deleted")
- if err != nil {
- log.Warn().Stringer("signal_user_id", deleteMember).Msg("Couldn't get puppet for removal")
- }
- }
- for _, deletePendingMember := range groupChange.DeletePendingMembers {
- if deletePendingMember.Type == libsignalgo.ServiceIDTypePNI {
- continue
- }
- if bannedMembers[deletePendingMember.UUID] {
- continue
- }
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, deletePendingMember.UUID, event.MembershipLeave, "invite withdrawn")
- if err != nil {
- log.Warn().Stringer("signal_user_id", deletePendingMember).Msg("Couldn't get puppet for removal")
- }
- }
- for _, deleteRequestingMember := range groupChange.DeleteRequestingMembers {
- if bannedMembers[*deleteRequestingMember] {
- continue
- }
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, *deleteRequestingMember, event.MembershipLeave, "request rejected")
- if err != nil {
- log.Warn().Stringer("signal_user_id", deleteRequestingMember).Msg("Couldn't get puppet for removal")
- }
- }
- for _, promotePendingMember := range groupChange.PromotePendingMembers {
- puppet := portal.bridge.GetPuppetBySignalID(promotePendingMember.ACI)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", promotePendingMember.ACI).Msg("Couldn't get puppet for invite")
- continue
- }
- puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID)
- }
- for _, promotePendingPniAciMember := range groupChange.PromotePendingPniAciMembers {
- puppet := portal.bridge.GetPuppetBySignalID(promotePendingPniAciMember.ACI)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", promotePendingPniAciMember.ACI).Msg("Couldn't get puppet for invite")
- continue
- }
- puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID)
- }
- for _, addPendingMember := range groupChange.AddPendingMembers {
- if addPendingMember.ServiceID.Type == libsignalgo.ServiceIDTypePNI {
- continue
- }
- _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, addPendingMember.ServiceID.UUID, event.MembershipInvite, "invited")
- if err != nil {
- log.Warn().Stringer("signal_user_id", addPendingMember.ServiceID).Msg("Couldn't get puppet for invite")
- }
- modifyRoles = append(modifyRoles, &signalmeow.RoleMember{ACI: addPendingMember.ServiceID.UUID, Role: addPendingMember.Role})
- }
- for _, promoteRequestingMember := range groupChange.PromoteRequestingMembers {
- puppet, err := portal.sendMembershipForPuppetAndUser(ctx, sender, promoteRequestingMember.ACI, event.MembershipInvite, "accepted")
- if err == nil {
- err = puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID)
- if err != nil {
- log.Warn().Stringer("signal_user_id", promoteRequestingMember.ACI).Msg("failed to join puppet")
- }
- } else {
- log.Warn().Stringer("signal_user_id", promoteRequestingMember.ACI).Msg("Couldn't get puppet for join")
- }
- modifyRoles = append(modifyRoles, &signalmeow.RoleMember{ACI: promoteRequestingMember.ACI, Role: promoteRequestingMember.Role})
- }
- for _, addRequestingMember := range groupChange.AddRequestingMembers {
- // sender and target should be the same SignalID
- puppet := portal.bridge.GetPuppetBySignalID(addRequestingMember.ACI)
- if puppet != nil {
- portal.sendMembershipWithPuppet(ctx, sender, puppet.IntentFor(portal).UserID, event.MembershipKnock, "knocked")
- }
- }
-
- if groupChange.ModifyAttributesAccess != nil || groupChange.ModifyAnnouncementsOnly != nil || groupChange.ModifyMemberAccess != nil || len(modifyRoles) > 0 {
- levels, err := portal.MainIntent().PowerLevels(ctx, portal.MXID)
- if err != nil {
- log.Err(err).Msg("Couldn't get power levels")
- } else {
- for _, modifyRole := range modifyRoles {
- puppet := portal.bridge.GetPuppetBySignalID(modifyRole.ACI)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", modifyRole.ACI).Msg("Couldn't get puppet for power level change")
- continue
- }
- powerLevel := 0
- if modifyRole.Role == signalmeow.GroupMember_ADMINISTRATOR {
- powerLevel = 50
- }
- levels.EnsureUserLevel(puppet.IntentFor(portal).UserID, powerLevel)
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(modifyRole.ACI)
- if user != nil {
- levels.EnsureUserLevel(user.MXID, powerLevel)
- }
- }
- }
- if groupChange.ModifyAnnouncementsOnly != nil {
- levels.EventsDefault = 0
- if *groupChange.ModifyAnnouncementsOnly {
- levels.EventsDefault = 50
- }
- }
- if groupChange.ModifyAttributesAccess != nil {
- level := 0
- if *groupChange.ModifyAttributesAccess == signalmeow.AccessControl_ADMINISTRATOR {
- level = 50
- }
- levels.StateDefaultPtr = &level
- }
- if groupChange.ModifyMemberAccess != nil {
- level := 0
- if *groupChange.ModifyMemberAccess == signalmeow.AccessControl_ADMINISTRATOR {
- level = 50
- }
- levels.InvitePtr = &level
- }
- _, err = intent.SetPowerLevels(ctx, portal.MXID, levels)
- if errors.Is(err, mautrix.MForbidden) {
- _, err = portal.MainIntent().SetPowerLevels(ctx, portal.MXID, levels)
- }
- if err != nil {
- log.Err(err).Msg("Couldn't set power levels")
- }
- }
- }
- if groupChange.ModifyAddFromInviteLinkAccess != nil {
- joinRule := event.JoinRuleInvite
- if *groupChange.ModifyAddFromInviteLinkAccess == signalmeow.AccessControl_ADMINISTRATOR {
- joinRule = event.JoinRuleKnock
- } else if *groupChange.ModifyAddFromInviteLinkAccess == signalmeow.AccessControl_ANY && portal.bridge.Config.Bridge.PublicPortals {
- joinRule = event.JoinRulePublic
- }
- _, err = intent.SendMassagedStateEvent(ctx, portal.MXID, event.StateJoinRules, "", &event.JoinRulesEventContent{JoinRule: joinRule}, int64(ts))
- if errors.Is(err, mautrix.MForbidden) {
- _, err = portal.MainIntent().SendMassagedStateEvent(ctx, portal.MXID, event.StateJoinRules, "", &event.JoinRulesEventContent{JoinRule: joinRule}, int64(ts))
- }
- if err != nil {
- log.Err(err).Msg("Couldn't set join rule")
- }
- }
- err = portal.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save portal in database after processing group change")
- }
- portal.UpdateBridgeInfo(ctx)
-}
-
-func (portal *Portal) sendMembershipForPuppetAndUser(ctx context.Context, sender *Puppet, target uuid.UUID, membership event.Membership, action string) (puppet *Puppet, err error) {
- puppet = portal.bridge.GetPuppetBySignalID(target)
- if puppet == nil {
- err = fmt.Errorf("couldn't get Puppet for Signal uuid %s", target)
- return
- }
- err = portal.sendMembershipWithPuppet(ctx, sender, puppet.IntentFor(portal).UserID, membership, action)
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(target)
- if user != nil {
- err = portal.sendMembershipWithPuppet(ctx, sender, user.MXID, membership, action)
- }
- }
- return
-}
-
-func (portal *Portal) sendMembershipWithPuppet(ctx context.Context, sender *Puppet, target id.UserID, membership event.Membership, action string) (err error) {
- _, err = sender.IntentFor(portal).SendCustomMembershipEvent(ctx, portal.MXID, target, membership, "")
- if errors.Is(err, mautrix.MForbidden) {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, target, membership, fmt.Sprintf("%s by %s", action, sender.GetDisplayname()))
- }
- if err != nil {
- zerolog.Ctx(ctx).Warn().Stringer("Membership Action failed for user", target).Msg(action)
- }
- return
-}
-
-func (portal *Portal) handleSignalReaction(sender *Puppet, react *signalpb.DataMessage_Reaction, ts uint64) {
- log := portal.log.With().
- Str("action", "handle signal reaction").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("target_msg_ts", react.GetTargetSentTimestamp()).
- Str("target_msg_sender", react.GetTargetAuthorAci()).
- Bool("remove", react.GetRemove()).
- Logger()
- ctx := log.WithContext(context.TODO())
- targetSenderUUID, err := uuid.Parse(react.GetTargetAuthorAci())
- if err != nil {
- log.Err(err).Msg("Failed to parse target message sender UUID")
- return
- }
- targetMsg, err := portal.bridge.DB.Message.GetBySignalID(ctx, targetSenderUUID, react.GetTargetSentTimestamp(), 0, portal.Receiver)
- if err != nil {
- log.Err(err).Msg("Failed to get target message from database")
- return
- } else if targetMsg == nil {
- log.Warn().Msg("Target message not found")
- return
- }
- existingReaction, err := portal.bridge.DB.Reaction.GetBySignalID(ctx, targetMsg.Sender, targetMsg.Timestamp, sender.SignalID, portal.Receiver)
- if err != nil {
- log.Err(err).Msg("Failed to get existing reaction from database")
- return
- } else if existingReaction != nil && existingReaction.Emoji == react.GetEmoji() {
- log.Debug().Msg("Ignoring duplicate reaction")
- return
- }
- intent := sender.IntentFor(portal)
- if existingReaction != nil {
- _, err = intent.RedactEvent(ctx, portal.MXID, existingReaction.MXID, mautrix.ReqRedact{
- TxnID: "mxsg_unreact_" + existingReaction.MXID.String(),
- })
- if errors.Is(err, mautrix.MForbidden) {
- log.Debug().Err(err).Msg("Failed to redact reaction with ghost, retrying with main intent")
- _, err = portal.MainIntent().RedactEvent(ctx, portal.MXID, existingReaction.MXID, mautrix.ReqRedact{
- TxnID: "mxsg_unreact_" + existingReaction.MXID.String(),
- })
- }
- if err != nil {
- log.Err(err).Msg("Failed to redact reaction")
- }
- if react.GetRemove() {
- err = existingReaction.Delete(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to remove reaction from database after redacting")
- }
- return
- }
- } else if react.GetRemove() {
- log.Warn().Msg("Existing reaction for removal not found")
- return
- }
- // Create a new message event with the reaction
- content := &event.ReactionEventContent{
- RelatesTo: event.RelatesTo{
- Type: event.RelAnnotation,
- Key: variationselector.Add(react.GetEmoji()),
- EventID: targetMsg.MXID,
- },
- }
- resp, err := portal.sendMatrixEvent(ctx, intent, event.EventReaction, content, nil, int64(ts))
- if err != nil {
- log.Err(err).Msg("Failed to send reaction")
- return
- }
- if existingReaction == nil {
- dbReaction := portal.bridge.DB.Reaction.New()
- dbReaction.MXID = resp.EventID
- dbReaction.RoomID = portal.MXID
- dbReaction.SignalChatID = portal.ChatID
- dbReaction.SignalReceiver = portal.Receiver
- dbReaction.Author = sender.SignalID
- dbReaction.MsgAuthor = targetMsg.Sender
- dbReaction.MsgTimestamp = targetMsg.Timestamp
- dbReaction.Emoji = react.GetEmoji()
- err = dbReaction.Insert(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to insert reaction to database")
- }
- } else {
- existingReaction.Emoji = react.GetEmoji()
- existingReaction.MXID = resp.EventID
- err = existingReaction.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to update reaction in database")
- }
- }
-}
-
-func (portal *Portal) handleSignalDelete(sender *Puppet, delete *signalpb.DataMessage_Delete, ts uint64) {
- log := portal.log.With().
- Str("action", "handle signal delete").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("target_msg_ts", delete.GetTargetSentTimestamp()).
- Uint64("delete_ts", ts).
- Logger()
- ctx := log.WithContext(context.TODO())
- targetMsg, err := portal.bridge.DB.Message.GetAllPartsBySignalID(ctx, sender.SignalID, delete.GetTargetSentTimestamp(), portal.Receiver)
- if err != nil {
- log.Err(err).Msg("Failed to get target message from database")
- return
- } else if len(targetMsg) == 0 {
- log.Warn().Msg("Target message not found")
- return
- }
- intent := sender.IntentFor(portal)
- for _, part := range targetMsg {
- _, err = intent.RedactEvent(ctx, portal.MXID, part.MXID, mautrix.ReqRedact{
- TxnID: "mxsg_delete_" + part.MXID.String(),
- })
- if err != nil {
- log.Err(err).
- Int("part_index", part.PartIndex).
- Stringer("event_id", part.MXID).
- Msg("Failed to redact message")
- }
- err = part.Delete(ctx)
- if err != nil {
- log.Err(err).
- Int("part_index", part.PartIndex).
- Msg("Failed to delete message from database")
- }
- }
-}
-
-func (portal *Portal) handleSignalNormalDataMessage(source *User, sender *Puppet, msg *signalpb.DataMessage) {
- log := portal.log.With().
- Str("action", "handle signal message").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("msg_ts", msg.GetTimestamp()).
- Logger()
- ctx := log.WithContext(context.TODO())
- if portal.MXID == "" {
- log.Debug().Msg("Creating Matrix room from incoming message")
- if err := portal.CreateMatrixRoom(ctx, source, msg.GetGroupV2().GetRevision()); err != nil {
- log.Error().Err(err).Msg("Failed to create portal room")
- return
- }
- } else if !portal.ensureUserInvited(ctx, source) {
- log.Warn().Stringer("user_id", source.MXID).Msg("Failed to ensure source user is joined to portal")
- }
-
- existingMessage, err := portal.bridge.DB.Message.GetBySignalID(ctx, sender.SignalID, msg.GetTimestamp(), 0, portal.Receiver)
- if err != nil {
- log.Err(err).Msg("Failed to check if message was already bridged")
- return
- } else if existingMessage != nil {
- log.Debug().Msg("Ignoring duplicate message")
- return
- }
-
- intent := sender.IntentFor(portal)
- ctx = context.WithValue(ctx, msgconvContextKeyIntent, intent)
- converted := portal.MsgConv.ToMatrix(ctx, msg)
- if portal.bridge.Config.Bridge.CaptionInMessage {
- converted.MergeCaption()
- }
- for i, part := range converted.Parts {
- resp, err := portal.sendMatrixEvent(ctx, intent, part.Type, part.Content, part.Extra, int64(converted.Timestamp))
- if err != nil {
- log.Err(err).Int("part_index", i).Msg("Failed to send message to Matrix")
- continue
- }
- portal.storeMessageInDB(ctx, resp.EventID, sender.SignalID, converted.Timestamp, i)
- if converted.DisappearIn != 0 {
- portal.addDisappearingMessage(ctx, resp.EventID, converted.DisappearIn, sender.SignalID == source.SignalID)
- // Ensure portal expiration timer is correct in DMs
- if portal.implicitlyUpdateExpirationTimer(ctx, converted.DisappearIn) {
- log.Info().Uint32("new_time", converted.DisappearIn).Msg("Implicitly updated expiration timer")
- err := portal.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save portal in database after implicitly updating group info")
- }
- }
- }
- }
-}
-
-func (portal *Portal) handleSignalEditMessage(sender *Puppet, timestamp uint64, msg *signalpb.DataMessage) {
- log := portal.log.With().
- Str("action", "handle signal edit").
- Stringer("sender_uuid", sender.SignalID).
- Uint64("target_msg_ts", timestamp).
- Uint64("edit_msg_ts", msg.GetTimestamp()).
- Logger()
- if portal.MXID == "" {
- log.Debug().Msg("Dropping edit message in chat with no portal")
- return
- }
- ctx := log.WithContext(context.TODO())
- targetMessage, err := portal.bridge.DB.Message.GetAllPartsBySignalID(ctx, sender.SignalID, timestamp, portal.Receiver)
- if err != nil {
- log.Err(err).Msg("Failed to get target message")
- return
- } else if len(targetMessage) == 0 {
- log.Debug().Msg("Target message not found (edit may have been already handled)")
- return
- }
-
- intent := sender.IntentFor(portal)
- ctx = context.WithValue(ctx, msgconvContextKeyIntent, intent)
- converted := portal.MsgConv.ToMatrix(ctx, msg)
- if portal.bridge.Config.Bridge.CaptionInMessage {
- converted.MergeCaption()
- }
- if len(converted.Parts) != len(targetMessage) {
- log.Error().
- Int("target_parts", len(targetMessage)).
- Int("new_parts", len(converted.Parts)).
- Msg("Mismatched number of parts in edit")
- return
- }
- for i, part := range converted.Parts {
- part.Content.SetEdit(targetMessage[i].MXID)
- if part.Extra != nil {
- part.Extra = map[string]any{
- "m.new_content": part.Extra,
- }
- }
- _, err = portal.sendMatrixEvent(ctx, intent, part.Type, part.Content, part.Extra, int64(converted.Timestamp))
- if err != nil {
- log.Err(err).Int("part_index", i).Msg("Failed to send edit to Matrix")
- }
- }
- err = targetMessage[0].SetTimestamp(ctx, msg.GetTimestamp())
- if err != nil {
- log.Err(err).Msg("Failed to update message edit timestamp in database")
- }
-}
-
-const SignalTypingTimeout = 15 * time.Second
-
-func (portal *Portal) handleSignalTypingMessage(sender *Puppet, msg *signalpb.TypingMessage) {
- if portal.MXID == "" {
- portal.log.Debug().Msg("Dropping typing message in chat with no portal")
- return
- }
- ctx := context.TODO()
- intent := sender.IntentFor(portal)
- // Don't bridge double puppeted typing notifications to avoid echoing
- if intent.IsCustomPuppet {
- return
- }
- var err error
- switch msg.GetAction() {
- case signalpb.TypingMessage_STARTED:
- _, err = intent.UserTyping(ctx, portal.MXID, true, SignalTypingTimeout)
- case signalpb.TypingMessage_STOPPED:
- _, err = intent.UserTyping(ctx, portal.MXID, false, 0)
- }
- if err != nil {
- portal.log.Err(err).
- Stringer("user_id", sender.SignalID).
- Msg("Failed to handle Signal typing notification")
- }
-}
-
-func (portal *Portal) storeMessageInDB(ctx context.Context, eventID id.EventID, senderSignalID uuid.UUID, timestamp uint64, partIndex int) {
- dbMessage := portal.bridge.DB.Message.New()
- dbMessage.MXID = eventID
- dbMessage.RoomID = portal.MXID
- dbMessage.Sender = senderSignalID
- dbMessage.Timestamp = timestamp
- dbMessage.PartIndex = partIndex
- dbMessage.SignalChatID = portal.ChatID
- dbMessage.SignalReceiver = portal.Receiver
- err := dbMessage.Insert(ctx)
- if err != nil {
- portal.log.Err(err).Msg("Failed to insert message into database")
- }
-}
-
-func (portal *Portal) addDisappearingMessage(ctx context.Context, eventID id.EventID, expireInSeconds uint32, startTimerNow bool) {
- portal.bridge.disappearingMessagesManager.AddDisappearingMessage(ctx, eventID, portal.MXID, time.Duration(expireInSeconds)*time.Second, startTimerNow)
-}
-
-func (portal *Portal) MarkDelivered(ctx context.Context, msg *database.Message) {
- if !portal.IsPrivateChat() {
- return
- }
- portal.bridge.SendRawMessageCheckpoint(&status.MessageCheckpoint{
- EventID: msg.MXID,
- RoomID: portal.MXID,
- Step: status.MsgStepRemote,
- Timestamp: jsontime.UnixMilliNow(),
- Status: status.MsgStatusDelivered,
- ReportedBy: status.MsgReportedByBridge,
- })
- portal.sendStatusEvent(ctx, msg.MXID, "", nil, &[]id.UserID{portal.MainIntent().UserID})
-}
-
-type customReadReceipt struct {
- Timestamp int64 `json:"ts,omitempty"`
- DoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
-}
-
-type customReadMarkers struct {
- mautrix.ReqSetReadMarkers
- ReadExtra customReadReceipt `json:"com.beeper.read.extra"`
- FullyReadExtra customReadReceipt `json:"com.beeper.fully_read.extra"`
-}
-
-func (portal *Portal) SendReadReceipt(ctx context.Context, sender *Puppet, msg *database.Message) error {
- intent := sender.IntentFor(portal)
- if intent.IsCustomPuppet {
- extra := customReadReceipt{DoublePuppetSource: portal.bridge.Name}
- return intent.SetReadMarkers(ctx, portal.MXID, &customReadMarkers{
- ReqSetReadMarkers: mautrix.ReqSetReadMarkers{
- Read: msg.MXID,
- FullyRead: msg.MXID,
- },
- ReadExtra: extra,
- FullyReadExtra: extra,
- })
- } else {
- return intent.MarkRead(ctx, portal.MXID, msg.MXID)
- }
-}
-
-func typingDiff(prev, new []id.UserID) (started, stopped []id.UserID) {
-OuterNew:
- for _, userID := range new {
- for _, previousUserID := range prev {
- if userID == previousUserID {
- continue OuterNew
- }
- }
- started = append(started, userID)
- }
-OuterPrev:
- for _, userID := range prev {
- for _, previousUserID := range new {
- if userID == previousUserID {
- continue OuterPrev
- }
- }
- stopped = append(stopped, userID)
- }
- return
-}
-
-func (portal *Portal) setTyping(userIDs []id.UserID, isTyping bool) {
- for _, userID := range userIDs {
- user := portal.bridge.GetUserByMXID(userID)
- if user == nil || !user.IsLoggedIn() {
- continue
- }
-
- // Check to see if portal.ChatID is a standard UUID (with dashes)
- // Note: not handling sending to a group right now, since that will
- // require SenderKey sending to not be terrible
- dmUserID := portal.UserID()
- if !dmUserID.IsEmpty() && dmUserID.Type == libsignalgo.ServiceIDTypeACI {
- // this is a 1:1 chat
- portal.log.Debug().Msg("Sending Typing event to Signal")
- ctx := context.TODO()
- typingMessage := signalmeow.TypingMessage(isTyping)
- result := user.Client.SendMessage(ctx, portal.UserID(), typingMessage)
- if !result.WasSuccessful {
- portal.log.Err(result.FailedSendResult.Error).Msg("Error sending event to Signal")
- }
- }
- }
-}
-
-func (portal *Portal) HandleMatrixTyping(newTyping []id.UserID) {
- if portal.IsNoteToSelf() {
- return
- }
-
- portal.currentlyTypingLock.Lock()
- defer portal.currentlyTypingLock.Unlock()
- startedTyping, stoppedTyping := typingDiff(portal.currentlyTyping, newTyping)
- portal.currentlyTyping = newTyping
- portal.setTyping(startedTyping, true)
- portal.setTyping(stoppedTyping, false)
-}
-
-func (portal *Portal) HandleMatrixReadReceipt(brSender bridge.User, eventID id.EventID, receipt event.ReadReceipt) {
- portal.handleMatrixReadReceipt(brSender.(*User), eventID, uint64(receipt.Timestamp.UnixMilli()), true)
-}
-
-func (portal *Portal) handleMatrixReadReceipt(sender *User, eventID id.EventID, maxTimestamp uint64, isExplicit bool) {
- if !sender.IsLoggedIn() {
- return
- }
- logWith := portal.log.With().
- Stringer("event_id", eventID).
- Stringer("sender", sender.MXID).
- Bool("explicit", isExplicit)
- if isExplicit {
- logWith = logWith.Str("action", "handle matrix read receipt")
- }
- log := logWith.Logger()
- log.Debug().Msg("Handling Matrix read receipt")
- portal.ScheduleDisappearing()
- ctx := log.WithContext(context.TODO())
-
- if isExplicit {
- dbMessage, _ := portal.bridge.DB.Message.GetByMXID(ctx, eventID)
- if dbMessage != nil {
- maxTimestamp = dbMessage.Timestamp
- }
- }
- prevLastReadTS := sender.GetLastReadTS(ctx, portal.PortalKey)
- if maxTimestamp <= prevLastReadTS {
- log.Debug().
- Uint64("prev_last_read_ts", prevLastReadTS).
- Uint64("max_timestamp", maxTimestamp).
- Msg("Ignoring read receipt older than last read timestamp")
- return
- }
- minTimestamp := prevLastReadTS
- if minTimestamp == 0 {
- minTimestamp = maxTimestamp - 2000
- }
- dbMessages, err := portal.bridge.DB.Message.GetAllBetweenTimestamps(ctx, portal.PortalKey, minTimestamp, maxTimestamp)
- if err != nil {
- log.Err(err).Msg("Failed to get messages between timestamps to mark as read")
- return
- }
- messagesToRead := map[uuid.UUID][]uint64{}
- for _, msg := range dbMessages {
- messagesToRead[msg.Sender] = append(messagesToRead[msg.Sender], msg.Timestamp)
- }
- // Always update last read ts for non-explicit read receipts, because that means there's a message about to be sent
- if (len(dbMessages) > 0 || !isExplicit) && maxTimestamp != prevLastReadTS {
- sender.SetLastReadTS(ctx, portal.PortalKey, maxTimestamp)
- }
- if isExplicit || len(messagesToRead) > 0 {
- log.Debug().
- Any("targets", messagesToRead).
- Uint64("prev_last_read_ts", prevLastReadTS).
- Uint64("min_timestamp", minTimestamp).
- Uint64("max_timestamp", maxTimestamp).
- 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 == sender.SignalID {
- 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 := sender.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(destination), signalmeow.ReadReceptMessageForTimestamps(messages))
- cancel()
- if !result.WasSuccessful {
- log.Err(result.FailedSendResult.Error).
- Stringer("destination", destination).
- Uints64("message_ids", messages).
- Msg("Failed to send read receipt to Signal")
- } else {
- log.Debug().
- Stringer("destination", destination).
- Uints64("message_ids", messages).
- Msg("Sent read receipt to Signal")
- }
- }
-}
-
-func (portal *Portal) sendMainIntentMessage(ctx context.Context, content *event.MessageEventContent) (*mautrix.RespSendEvent, error) {
- return portal.sendMatrixEvent(ctx, portal.MainIntent(), event.EventMessage, content, nil, 0)
-}
-
-func (portal *Portal) encrypt(ctx context.Context, intent *appservice.IntentAPI, content *event.Content, eventType event.Type) (event.Type, error) {
- if !portal.Encrypted || portal.bridge.Crypto == nil {
- return eventType, nil
- }
- intent.AddDoublePuppetValue(content)
- // TODO maybe the locking should be inside mautrix-go?
- portal.encryptLock.Lock()
- defer portal.encryptLock.Unlock()
- err := portal.bridge.Crypto.Encrypt(ctx, portal.MXID, eventType, content)
- if err != nil {
- return eventType, fmt.Errorf("failed to encrypt event: %w", err)
- }
- return event.EventEncrypted, nil
-}
-
-func (portal *Portal) sendMatrixEvent(ctx context.Context, intent *appservice.IntentAPI, eventType event.Type, content any, extraContent map[string]any, timestamp int64) (*mautrix.RespSendEvent, error) {
- wrappedContent := event.Content{Parsed: content, Raw: extraContent}
- if eventType != event.EventReaction {
- var err error
- eventType, err = portal.encrypt(ctx, intent, &wrappedContent, eventType)
- if err != nil {
- return nil, err
- }
- }
-
- _, _ = intent.UserTyping(ctx, portal.MXID, false, 0)
- return intent.SendMassagedMessageEvent(ctx, portal.MXID, eventType, &wrappedContent, timestamp)
-}
-
-func (portal *Portal) getEncryptionEventContent() (evt *event.EncryptionEventContent) {
- evt = &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}
- if rot := portal.bridge.Config.Bridge.Encryption.Rotation; rot.EnableCustom {
- evt.RotationPeriodMillis = rot.Milliseconds
- evt.RotationPeriodMessages = rot.Messages
- }
- return
-}
-
-func (portal *Portal) shouldSetDMRoomMetadata() bool {
- return !portal.IsPrivateChat() ||
- portal.bridge.Config.Bridge.PrivateChatPortalMeta == "always" ||
- (portal.IsEncrypted() && portal.bridge.Config.Bridge.PrivateChatPortalMeta != "never")
-}
-
-func (portal *Portal) ensureUserInvited(ctx context.Context, user *User) bool {
- return user.ensureInvited(ctx, portal.MainIntent(), portal.MXID, portal.IsPrivateChat())
-}
-
-func (portal *Portal) CreateMatrixRoom(ctx context.Context, user *User, groupRevision uint32) error {
- portal.roomCreateLock.Lock()
- defer portal.roomCreateLock.Unlock()
- if portal.MXID != "" {
- portal.log.Debug().Msg("Not creating room: already exists")
- return nil
- }
- portal.log.Debug().Msg("Creating matrix room")
-
- intent := portal.MainIntent()
-
- if err := intent.EnsureRegistered(ctx); err != nil {
- portal.log.Error().Err(err).Msg("failed to ensure registered")
- return err
- }
-
- bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
- initialState := []*event.Event{{
- Type: event.StateBridge,
- Content: event.Content{Parsed: bridgeInfo},
- StateKey: &bridgeInfoStateKey,
- }, {
- // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
- Type: event.StateHalfShotBridge,
- Content: event.Content{Parsed: bridgeInfo},
- StateKey: &bridgeInfoStateKey,
- }}
-
- if !portal.AvatarURL.IsEmpty() {
- initialState = append(initialState, &event.Event{
- Type: event.StateRoomAvatar,
- Content: event.Content{Parsed: &event.RoomAvatarEventContent{
- URL: portal.AvatarURL.CUString(),
- }},
- })
- }
-
- creationContent := make(map[string]interface{})
- if !portal.bridge.Config.Bridge.FederateRooms {
- creationContent["m.federate"] = false
- }
-
- var invite []id.UserID
- autoJoinInvites := portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites)
- if autoJoinInvites {
- invite = append(invite, user.MXID)
- }
-
- if portal.bridge.Config.Bridge.Encryption.Default {
- initialState = append(initialState, &event.Event{
- Type: event.StateEncryption,
- Content: event.Content{
- Parsed: portal.getEncryptionEventContent(),
- },
- })
- portal.Encrypted = true
-
- if portal.IsPrivateChat() && portal.MainIntent() != portal.bridge.Bot {
- invite = append(invite, portal.bridge.Bot.UserID)
- }
- }
-
- var dmPuppet *Puppet
- var groupInfo *signalmeow.Group
- if portal.IsPrivateChat() {
- dmPuppet = portal.GetDMPuppet()
- if dmPuppet != nil {
- dmPuppet.UpdateInfo(ctx, user, nil)
- portal.UpdateDMInfo(ctx, false)
- } else {
- portal.UpdatePNIDMInfo(ctx, user)
- }
- } else {
- groupInfo = portal.UpdateGroupInfo(ctx, user, nil, groupRevision, true)
- if groupInfo == nil {
- portal.log.Error().Msg("Didn't get group info after updating portal")
- return errors.New("failed to get group info")
- }
- for member := range portal.SyncParticipants(ctx, user, groupInfo) {
- invite = append(invite, member)
- }
- }
-
- req := &mautrix.ReqCreateRoom{
- Visibility: "private",
- Name: portal.Name,
- Topic: portal.Topic,
- Invite: invite,
- Preset: "private_chat",
- IsDirect: portal.IsPrivateChat(),
- InitialState: initialState,
- CreationContent: creationContent,
-
- BeeperAutoJoinInvites: autoJoinInvites,
- }
- resp, err := intent.CreateRoom(ctx, req)
- if err != nil {
- portal.log.Warn().Err(err).Msg("failed to create room")
- return err
- }
- portal.log = portal.log.With().Stringer("room_id", resp.RoomID).Logger()
-
- portal.NameSet = len(req.Name) > 0
- portal.TopicSet = len(req.Topic) > 0
- portal.AvatarSet = !portal.AvatarURL.IsEmpty()
- portal.MXID = resp.RoomID
- portal.bridge.portalsLock.Lock()
- portal.bridge.portalsByMXID[portal.MXID] = portal
- portal.bridge.portalsLock.Unlock()
- err = portal.Update(ctx)
- if err != nil {
- portal.log.Err(err).Msg("Failed to save portal room ID")
- return err
- }
- portal.log.Info().Msg("Created matrix room for portal")
-
- if !autoJoinInvites {
- if !portal.IsPrivateChat() {
- portal.SyncParticipants(ctx, user, groupInfo)
- } else if portal.Encrypted {
- err = portal.bridge.Bot.EnsureJoined(ctx, portal.MXID, appservice.EnsureJoinedParams{BotOverride: portal.MainIntent().Client})
- if err != nil {
- portal.log.Error().Err(err).Msg("Failed to ensure bridge bot is joined to private chat portal")
- }
- }
- user.ensureInvited(ctx, portal.MainIntent(), portal.MXID, portal.IsPrivateChat())
- }
- user.syncChatDoublePuppetDetails(portal, true)
- go portal.addToPersonalSpace(portal.log.WithContext(context.TODO()), user)
-
- if dmPuppet != nil {
- user.UpdateDirectChats(ctx, map[id.UserID][]id.RoomID{
- dmPuppet.MXID: {portal.MXID},
- })
- }
-
- return nil
-}
-
-func (portal *Portal) GetDMPuppet() *Puppet {
- userID := portal.UserID()
- if userID.IsEmpty() || userID.Type != libsignalgo.ServiceIDTypeACI {
- return nil
- }
- return portal.bridge.GetPuppetBySignalID(userID.UUID)
-}
-
-func (portal *Portal) UpdateInfo(ctx context.Context, source *User, groupInfo *signalmeow.Group, revision uint32) {
- if portal.IsPrivateChat() {
- portal.UpdateDMInfo(ctx, false)
- return
- }
- groupInfo = portal.UpdateGroupInfo(ctx, source, groupInfo, revision, false)
- if groupInfo != nil {
- members := portal.SyncParticipants(ctx, source, groupInfo)
- portal.updatePowerLevelsAndJoinRule(ctx, groupInfo, members)
- }
-}
-
-const PrivateChatTopic = "Signal private chat"
-const NoteToSelfName = "Signal Note to Self"
-
-func (portal *Portal) PostReIDUpdate(ctx context.Context, user *User) {
- _, err := portal.bridge.Bot.SetPowerLevel(ctx, portal.MXID, portal.MainIntent().UserID, 100)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update ghost power level after portal re-ID")
- }
- portal.GetDMPuppet().UpdateInfo(ctx, user, nil)
- portal.UpdateDMInfo(ctx, true)
- if !portal.Encrypted {
- _, _ = portal.bridge.Bot.LeaveRoom(ctx, portal.MXID)
- }
-}
-
-func (portal *Portal) UpdateDMInfo(ctx context.Context, forceSave bool) {
- log := zerolog.Ctx(ctx).With().
- Str("function", "UpdateDMInfo").
- Logger()
- log.Trace().Msg("Updating portal info")
- ctx = log.WithContext(ctx)
- puppet := portal.GetDMPuppet()
-
- update := forceSave
- if portal.UserID().UUID == portal.Receiver {
- noteToSelfAvatar := portal.bridge.Config.Bridge.NoteToSelfAvatar.ParseOrIgnore()
- avatarHash := sha256.Sum256([]byte(noteToSelfAvatar.String()))
-
- update = portal.updateName(ctx, NoteToSelfName, nil) || update
- update = portal.updateAvatarWithMXC(ctx, "notetoself", hex.EncodeToString(avatarHash[:]), noteToSelfAvatar) || update
- } else if portal.shouldSetDMRoomMetadata() {
- update = portal.updateName(ctx, puppet.Name, nil) || update
- update = portal.updateAvatarWithMXC(ctx, puppet.AvatarPath, puppet.AvatarHash, puppet.AvatarURL) || update
- } else {
- // Clear name/avatar if they're set in a DM that shouldn't have them set
- if portal.Name != "" && portal.NameSet {
- update = portal.updateName(ctx, "", nil) || update
- }
- // Avatar is currently never set in PNI portals
- //if !portal.AvatarURL.IsEmpty() && portal.AvatarSet {
- // update = true
- // portal.AvatarURL = id.ContentURI{}
- // portal.AvatarHash = ""
- // portal.AvatarPath = ""
- // portal.updateAvatarInRoom(ctx, nil)
- //}
- }
- topic := PrivateChatTopic
- if portal.bridge.Config.Bridge.NumberInTopic && puppet.Number != "" {
- topic = fmt.Sprintf("%s with %s", topic, puppet.Number)
- }
- update = portal.updateTopic(ctx, topic, nil) || update
- if update {
- err := portal.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save portal in database after updating group info")
- }
- portal.UpdateBridgeInfo(ctx)
- }
-}
-
-func (portal *Portal) UpdatePNIDMInfo(ctx context.Context, user *User) {
- portalUserID := portal.UserID()
- if portalUserID.Type != libsignalgo.ServiceIDTypePNI {
- return
- }
- log := zerolog.Ctx(ctx)
- update := false
- recipient, err := user.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, uuid.Nil, portalUserID.UUID, nil)
- if err != nil {
- log.Err(err).Msg("Failed to get PNI DM recipient entry")
- }
- if recipient == nil {
- recipient = &types.Recipient{PNI: portalUserID.UUID}
- }
- topic := PrivateChatTopic
- name := portalUserID.UUID.String()
- if recipient.E164 != "" {
- topic = fmt.Sprintf("%s with %s", topic, recipient.E164)
- name = recipient.E164
- }
- if recipient.ContactName != "" {
- name = recipient.ContactName
- }
- update = portal.updateTopic(ctx, topic, nil) || update
- update = portal.updateName(ctx, name, nil) || update
- if update {
- err = portal.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save portal in database after updating group info")
- }
- portal.UpdateBridgeInfo(ctx)
- }
-}
-
-func (portal *Portal) updatePowerLevelsAndJoinRule(ctx context.Context, info *signalmeow.Group, members map[id.UserID]int) {
- log := zerolog.Ctx(ctx).With().
- Str("function", "updatePowerLevelsAndJoinRule").
- Logger()
- log.Trace().Msg("Updating power levels and join rule")
- joinRuleContent := event.JoinRulesEventContent{}
- err := portal.MainIntent().StateEvent(ctx, portal.MXID, event.StateJoinRules, "", &joinRuleContent)
- if err != nil {
- log.Err(err).Msg("Failed to get join rule")
- return
- }
- joinRule := joinRuleContent.JoinRule
- newJoinRule := event.JoinRuleInvite
- levels, err := portal.MainIntent().PowerLevels(ctx, portal.MXID)
- if err != nil {
- log.Err(err).Msg("Failed to get power levels")
- return
- }
- botLevel := levels.GetUserLevel(portal.MainIntent().UserID)
- changed := false
- for mxid, level := range members {
- oldLevel := levels.GetUserLevel(mxid)
- difference := oldLevel - level
- if oldLevel < botLevel && (difference < 0 || difference > 49) {
- changed = levels.EnsureUserLevel(mxid, level) || changed
- }
- }
- newEventsDefault := 0
- if info.AnnouncementsOnly {
- newEventsDefault = 50
- }
- if newEventsDefault != levels.EventsDefault {
- levels.EventsDefault = newEventsDefault
- changed = true
- }
- if info.AccessControl != nil {
- level := 0
- if info.AccessControl.Attributes == signalmeow.AccessControl_ADMINISTRATOR {
- level = 50
- }
- changed = levels.EnsureEventLevel(event.StateRoomName, level) || changed
- changed = levels.EnsureEventLevel(event.StateTopic, level) || changed
- changed = levels.EnsureEventLevel(event.StateRoomAvatar, level) || changed
- level = 0
- if info.AccessControl.Members == signalmeow.AccessControl_ADMINISTRATOR {
- level = 50
- }
- if levels.InvitePtr == nil || *levels.InvitePtr != level {
- levels.InvitePtr = &level
- changed = true
- }
- if info.AccessControl.AddFromInviteLink == signalmeow.AccessControl_ADMINISTRATOR {
- newJoinRule = event.JoinRuleKnock
- } else if info.AccessControl.AddFromInviteLink == signalmeow.AccessControl_ANY && (portal.bridge.Config.Bridge.PublicPortals || joinRule == event.JoinRulePublic) {
- newJoinRule = event.JoinRulePublic
- }
- }
- if newJoinRule != joinRule {
- _, err = portal.MainIntent().SendStateEvent(ctx, portal.MXID, event.StateJoinRules, "", &event.JoinRulesEventContent{JoinRule: joinRule})
- if err != nil {
- log.Err(err).Msg("Failed to set join rule")
- }
- }
- if changed {
- _, err = portal.MainIntent().SetPowerLevels(ctx, portal.MXID, levels)
- if err != nil {
- log.Err(err).Msg("Failed to set power levels")
- }
- }
-}
-
-func (portal *Portal) UpdateGroupInfo(ctx context.Context, source *User, info *signalmeow.Group, revision uint32, forceFetch bool) *signalmeow.Group {
- logWith := zerolog.Ctx(ctx).With().
- Str("function", "UpdateGroupInfo").
- Uint32("revision", revision).
- Stringer("source_user_mxid", source.MXID)
- if info != nil {
- logWith = logWith.Uint32("info_revision", info.Revision)
- }
- log := logWith.Logger()
- if info == nil {
- if revision <= portal.Revision && !forceFetch {
- log.Debug().Msg("Not fetching group info to update portal: given revision is not newer")
- return nil
- }
- log.Debug().Msg("Fetching group info to update portal")
- var err error
- info, err = source.Client.RetrieveGroupByID(ctx, portal.GroupID(), revision)
- if err != nil {
- log.Err(err).
- Stringer("source_user_id", source.MXID).
- Msg("Failed to fetch group info")
- return nil
- }
- }
- if portal.Revision > info.Revision {
- log.Debug().Uint32("current_revision", portal.Revision).Msg("Not updating portal with data from older revision")
- return info
- }
- logEvt := log.Trace()
- if portal.Revision != info.Revision {
- logEvt = log.Debug()
- }
- logEvt.Uint32("current_revision", portal.Revision).Msg("Updating portal info")
- ctx = log.WithContext(ctx)
- update := false
- if portal.Revision < info.Revision {
- portal.Revision = info.Revision
- update = true
- }
- update = portal.updateName(ctx, info.Title, nil) || update
- update = portal.updateTopic(ctx, info.Description, nil) || update
- update = portal.updateAvatarWithInfo(ctx, source, info, nil) || update
- update = portal.updateExpirationTimer(ctx, info.DisappearingMessagesDuration) || update
- if update {
- err := portal.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save portal in database after updating group info")
- }
- portal.UpdateBridgeInfo(ctx)
- }
- return info
-}
-
-func (portal *Portal) updateExpirationTimer(ctx context.Context, newExpirationTimer uint32) bool {
- if portal.ExpirationTime == newExpirationTimer {
- return false
- }
- portal.ExpirationTime = newExpirationTimer
- if portal.MXID != "" {
- msg := portal.MsgConv.ConvertDisappearingTimerChangeToMatrix(ctx, newExpirationTimer, false)
- _, err := portal.sendMainIntentMessage(ctx, msg.Content)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to send notice about disappearing message timer changing")
- }
- }
- return true
-}
-
-func (portal *Portal) implicitlyUpdateExpirationTimer(ctx context.Context, newExpirationTimer uint32) bool {
- if portal.ExpirationTime == newExpirationTimer {
- return false
- }
- portal.ExpirationTime = newExpirationTimer
- if portal.MXID != "" {
- msg := portal.MsgConv.ConvertDisappearingTimerChangeToMatrix(ctx, newExpirationTimer, false)
- msg.Content.Body = fmt.Sprintf("Automatically enabled disappearing message timer (%s) because incoming message is disappearing", exfmt.Duration(time.Duration(newExpirationTimer)*time.Second))
- _, err := portal.sendMainIntentMessage(ctx, msg.Content)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to send notice about disappearing message timer changing implicitly")
- }
- }
- return true
-}
-
-func (portal *Portal) updateName(ctx context.Context, newName string, sender *Puppet) bool {
- if portal.Name == newName && (portal.NameSet || portal.MXID == "") {
- return false
- }
- portal.Name = newName
- portal.NameSet = false
- if portal.MXID != "" {
- intent := portal.MainIntent()
- if sender != nil {
- intent = sender.IntentFor(portal)
- }
- _, err := intent.SetRoomName(ctx, portal.MXID, portal.Name)
- if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
- _, err = portal.MainIntent().SetRoomName(ctx, portal.MXID, portal.Name)
- }
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update room name")
- } else {
- portal.NameSet = true
- }
- }
- return true
-}
-
-func (portal *Portal) updateTopic(ctx context.Context, newTopic string, sender *Puppet) bool {
- if portal.Topic == newTopic && (portal.TopicSet || portal.MXID == "") {
- return false
- }
- portal.Topic = newTopic
- portal.TopicSet = false
- if portal.MXID != "" {
- intent := portal.MainIntent()
- if sender != nil {
- intent = sender.IntentFor(portal)
- }
- _, err := intent.SetRoomTopic(ctx, portal.MXID, portal.Topic)
- if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
- _, err = portal.MainIntent().SetRoomTopic(ctx, portal.MXID, portal.Topic)
- }
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update room topic")
- } else {
- portal.TopicSet = true
- }
- }
- return true
-}
-
-func (portal *Portal) updateAvatarWithInfo(ctx context.Context, source *User, group signalmeow.GroupAvatarMeta, sender *Puppet) bool {
- // If the avatar path is different, the avatar probably changed
- avatarPath := group.GetAvatarPath()
- if avatarPath == nil {
- return false
- }
- if portal.AvatarPath == *avatarPath &&
- // If the avatar mxc isn't set, we need to reupload it (except if the avatar is unset in Signal)
- (!portal.AvatarURL.IsEmpty() || *avatarPath == "") &&
- // If the avatar isn't set in the room, we need to update the room state (except if there's no Matrix room yet)
- (portal.AvatarSet || portal.MXID == "") {
- return false
- }
- if *avatarPath == "" {
- portal.AvatarPath = ""
- portal.AvatarSet = false
- portal.AvatarURL = id.ContentURI{}
- portal.AvatarHash = ""
- // Just clear the Matrix room avatar and return
- portal.updateAvatarInRoom(ctx, sender)
- return true
- }
- log := zerolog.Ctx(ctx)
- log.Debug().Str("avatar_path", portal.AvatarPath).Msg("Downloading new group avatar from Signal")
- avatarBytes, err := source.Client.DownloadGroupAvatar(ctx, group)
- if err != nil {
- log.Err(err).Msg("Failed to download new avatar for portal")
- return true
- }
- hash := sha256.Sum256(avatarBytes)
- newAvatarHash := hex.EncodeToString(hash[:])
- if portal.AvatarHash == newAvatarHash && (portal.AvatarSet || portal.MXID == "") {
- // No need to change anything else, but save the new path to the database
- return true
- }
- portal.AvatarPath = *avatarPath
- portal.AvatarSet = false
- portal.AvatarURL = id.ContentURI{}
- portal.AvatarHash = newAvatarHash
- log.Debug().Str("avatar_hash", portal.AvatarHash).Msg("Uploading new group avatar to Matrix")
- resp, err := portal.MainIntent().UploadBytes(ctx, avatarBytes, http.DetectContentType(avatarBytes))
- if err != nil {
- log.Err(err).Msg("Failed to upload new avatar for portal")
- } else {
- portal.AvatarURL = resp.ContentURI
- portal.updateAvatarInRoom(ctx, sender)
- }
- return true
-}
-
-func (portal *Portal) updateAvatarWithMXC(ctx context.Context, newAvatarPath, newAvatarHash string, newAvatarURI id.ContentURI) bool {
- if portal.AvatarHash == newAvatarHash && (portal.AvatarSet || portal.MXID == "") {
- return false
- }
- portal.AvatarPath = newAvatarPath
- portal.AvatarHash = newAvatarHash
- portal.AvatarURL = newAvatarURI
- portal.AvatarSet = false
- portal.updateAvatarInRoom(ctx, nil)
- return true
-}
-
-func (portal *Portal) updateAvatarInRoom(ctx context.Context, sender *Puppet) {
- if portal.MXID == "" || portal.AvatarSet {
- return
- }
- zerolog.Ctx(ctx).Debug().
- Str("avatar_path", portal.AvatarPath).
- Str("avatar_hash", portal.AvatarHash).
- Stringer("avatar_mxc", portal.AvatarURL).
- Msg("Updating avatar in Matrix room")
- intent := portal.MainIntent()
- if sender != nil {
- intent = sender.IntentFor(portal)
- }
- _, err := intent.SetRoomAvatar(ctx, portal.MXID, portal.AvatarURL)
- if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
- _, err = portal.MainIntent().SetRoomAvatar(ctx, portal.MXID, portal.AvatarURL)
- }
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update room avatar")
- } else {
- portal.AvatarSet = true
- }
-}
-
-func (portal *Portal) SyncParticipants(ctx context.Context, source *User, info *signalmeow.Group) map[id.UserID]int {
- log := zerolog.Ctx(ctx)
- userIDs := make(map[id.UserID]int)
- currentMembers := make(map[id.UserID]event.Membership)
- var err error
- if portal.MXID != "" {
- memberEventData, err := portal.MainIntent().Members(ctx, portal.MXID, mautrix.ReqMembers{})
- if err != nil {
- log.Err(err).Msg("couldn't get portal members")
- return nil
- }
- for _, evt := range memberEventData.Chunk {
- evt.Content.ParseRaw(event.StateMember)
- currentMembers[id.UserID(*evt.StateKey)] = evt.Content.AsMember().Membership
- }
- }
- for _, member := range info.Members {
- puppet := portal.bridge.GetPuppetBySignalID(member.ACI)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", member.ACI).Msg("Couldn't get puppet for group member")
- continue
- }
- puppet.UpdateInfo(ctx, source, nil)
- intent := puppet.IntentFor(portal)
- if member.ACI != source.SignalID && portal.MXID != "" {
- userIDs[intent.UserID] = ((int)(member.Role) >> 1) * 50
- }
- delete(currentMembers, intent.UserID)
- if portal.MXID != "" {
- if currentMembers[intent.UserID] != event.MembershipJoin {
- err := intent.EnsureJoined(ctx, portal.MXID)
- if err != nil {
- log.Err(err).Stringer("signal_user_id", member.ACI).Msg("Failed to ensure user is joined to portal")
- }
- }
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(member.ACI)
- if user != nil {
- delete(currentMembers, user.MXID)
- userIDs[user.MXID] = ((int)(member.Role) >> 1) * 50
- currentMembership := currentMembers[user.MXID]
- if currentMembership == event.MembershipJoin || currentMembership == event.MembershipInvite {
- continue
- }
- user.ensureInvited(ctx, intent, portal.MXID, false)
- }
- }
- }
- }
- if portal.MXID == "" {
- return userIDs
- }
- for _, pendingMember := range info.PendingMembers {
- if pendingMember.ServiceID.Type == libsignalgo.ServiceIDTypePNI {
- continue
- }
- puppet := portal.bridge.GetPuppetBySignalID(pendingMember.ServiceID.UUID)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", pendingMember.ServiceID.UUID).Msg("Couldn't get puppet for group member")
- continue
- }
- mxid := puppet.IntentFor(portal).UserID
- membership := currentMembers[mxid]
- if membership == event.MembershipJoin || membership == event.MembershipBan {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipLeave, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to leave")
- }
- }
- if membership != event.MembershipInvite {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipInvite, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to invite")
- }
- }
- userIDs[mxid] = ((int)(pendingMember.Role) >> 1) * 50
- delete(currentMembers, mxid)
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(pendingMember.ServiceID.UUID)
- if user == nil {
- continue
- }
- mxid = user.MXID
- membership := currentMembers[mxid]
- err = nil
- if membership == event.MembershipJoin || membership == event.MembershipBan {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipLeave, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to leave")
- }
- }
- if membership != event.MembershipInvite {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipInvite, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to invite")
- }
- }
- userIDs[mxid] = ((int)(pendingMember.Role) >> 1) * 50
- delete(currentMembers, mxid)
- }
- }
- for _, requestingMember := range info.RequestingMembers {
- puppet := portal.bridge.GetPuppetBySignalID(requestingMember.ACI)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", requestingMember.ACI).Msg("Couldn't get puppet for group member")
- continue
- }
- mxid := puppet.IntentFor(portal).UserID
- membership := currentMembers[mxid]
- if membership == event.MembershipJoin || membership == event.MembershipBan {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipLeave, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to leave")
- }
- }
- if membership != event.MembershipKnock {
- _, err = puppet.IntentFor(portal).SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipKnock, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to knock")
- }
- }
- delete(currentMembers, mxid)
- }
- for _, bannedMember := range info.BannedMembers {
- if bannedMember.ServiceID.Type == libsignalgo.ServiceIDTypePNI {
- continue
- }
- puppet := portal.bridge.GetPuppetBySignalID(bannedMember.ServiceID.UUID)
- if puppet == nil {
- log.Warn().Stringer("signal_user_id", bannedMember.ServiceID.UUID).Msg("Couldn't get puppet for group member")
- continue
- }
- mxid := puppet.IntentFor(portal).UserID
- if currentMembers[mxid] != event.MembershipBan {
- _, err := portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipBan, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to ban")
- }
- }
- delete(currentMembers, mxid)
- if puppet.customIntent == nil {
- user := portal.bridge.GetUserBySignalID(bannedMember.ServiceID.UUID)
- if user == nil {
- continue
- }
- mxid = user.MXID
- if currentMembers[mxid] != event.MembershipBan {
- _, err = portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipBan, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to ban")
- }
- }
- delete(currentMembers, mxid)
- }
- }
- for mxid, membership := range currentMembers {
- if membership == event.MembershipLeave {
- continue
- }
- puppet := portal.bridge.GetPuppetByMXID(mxid)
- if puppet != nil {
- _, err := portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipLeave, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to leave")
- }
- } else {
- user := portal.bridge.GetUserByMXIDIfExists(mxid)
- if user != nil {
- if user.IsLoggedIn() {
- _, err := portal.MainIntent().SendCustomMembershipEvent(ctx, portal.MXID, mxid, event.MembershipLeave, "")
- if err != nil {
- log.Err(err).Stringer("mxid", mxid).Msg("Couldn't change membership to leave")
- }
- }
- }
- }
- }
- return userIDs
-}
-
-func (portal *Portal) getBridgeInfoStateKey() string {
- return fmt.Sprintf("net.maunium.signal://signal/%s", portal.ChatID)
-}
-
-func (portal *Portal) ScheduleDisappearing() {
- portal.bridge.disappearingMessagesManager.ScheduleDisappearingForRoom(context.TODO(), portal.MXID)
-}
-
-func (portal *Portal) addToPersonalSpace(ctx context.Context, user *User) bool {
- spaceID := user.GetSpaceRoom(ctx)
- if len(spaceID) == 0 || user.IsInSpace(ctx, portal.PortalKey) {
- return false
- }
- _, err := portal.bridge.Bot.SendStateEvent(ctx, spaceID, event.StateSpaceChild, portal.MXID.String(), &event.SpaceChildEventContent{
- Via: []string{portal.bridge.Config.Homeserver.Domain},
- })
- if err != nil {
- zerolog.Ctx(ctx).Err(err).
- Stringer("user_id", user.MXID).
- Stringer("space_id", spaceID).
- Msg("Failed to add room to user's personal filtering space")
- return false
- } else {
- zerolog.Ctx(ctx).Debug().
- Stringer("user_id", user.MXID).
- Stringer("space_id", spaceID).
- Msg("Added room to user's personal filtering space")
- user.MarkInSpace(ctx, portal.PortalKey)
- return true
- }
-}
-
-func (portal *Portal) HasRelaybot() bool {
- return portal.bridge.Config.Bridge.Relay.Enabled && len(portal.RelayUserID) > 0
-}
-
-func (portal *Portal) addRelaybotFormat(ctx context.Context, userID id.UserID, evt *event.Event, content *event.MessageEventContent) bool {
- member := portal.MainIntent().Member(ctx, portal.MXID, userID)
- if member == nil {
- member = &event.MemberEventContent{}
- }
- // Stickers can't have captions, so force them into images when relaying
- if evt.Type == event.EventSticker {
- content.MsgType = event.MsgImage
- evt.Type = event.EventMessage
- }
- content.EnsureHasHTML()
- data, err := portal.bridge.Config.Bridge.Relay.FormatMessage(content, userID, *member)
- if err != nil {
- portal.log.Err(err).Msg("Failed to apply relaybot format")
- }
- content.FormattedBody = data
- // Force FileName field so the formatted body is used as a caption
- if content.FileName == "" {
- content.FileName = content.Body
- }
- return true
-}
-
-func (portal *Portal) Delete() {
- err := portal.Portal.Delete(context.TODO())
- if err != nil {
- portal.log.Err(err).Msg("Failed to delete portal from db")
- }
- portal.bridge.portalsLock.Lock()
- portal.unlockedDeleteCache()
- portal.bridge.portalsLock.Unlock()
-}
-
-func (portal *Portal) unlockedDelete() {
- err := portal.Portal.Delete(context.TODO())
- if err != nil {
- portal.log.Err(err).Msg("Failed to delete portal from db")
- }
- portal.unlockedDeleteCache()
-}
-
-func (portal *Portal) unlockedDeleteCache() {
- delete(portal.bridge.portalsByID, portal.PortalKey)
- if len(portal.MXID) > 0 {
- delete(portal.bridge.portalsByMXID, portal.MXID)
- }
- if portal.Receiver == uuid.Nil {
- portal.bridge.usersLock.Lock()
- for _, user := range portal.bridge.usersBySignalID {
- user.RemoveInSpaceCache(portal.PortalKey)
- }
- portal.bridge.usersLock.Unlock()
- } else {
- user := portal.bridge.GetUserBySignalID(portal.Receiver)
- if user != nil {
- user.RemoveInSpaceCache(portal.PortalKey)
- }
- }
-}
-
-func (portal *Portal) Cleanup(ctx context.Context, puppetsOnly bool) {
- portal.bridge.CleanupRoom(ctx, &portal.log, portal.MainIntent(), portal.MXID, puppetsOnly)
-}
-
-func (br *SignalBridge) CleanupRoom(ctx context.Context, log *zerolog.Logger, intent *appservice.IntentAPI, mxid id.RoomID, puppetsOnly bool) {
- if len(mxid) == 0 {
- return
- }
- if br.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
- err := intent.BeeperDeleteRoom(ctx, mxid)
- if err == nil || errors.Is(err, mautrix.MNotFound) {
- return
- }
- log.Warn().Err(err).Msg("Failed to delete room using beeper yeet endpoint, falling back to normal behavior")
- }
- members, err := intent.JoinedMembers(ctx, mxid)
- if err != nil {
- log.Err(err).Msg("Failed to get portal members for cleanup")
- return
- }
- for member := range members.Joined {
- if member == intent.UserID {
- continue
- }
- puppet := br.GetPuppetByMXID(member)
- if puppet != nil {
- _, err = puppet.DefaultIntent().LeaveRoom(ctx, mxid)
- if err != nil {
- log.Err(err).Msg("Failed to leave as puppet while cleaning up portal")
- }
- } else if !puppetsOnly {
- _, err = intent.KickUser(ctx, mxid, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"})
- if err != nil {
- log.Err(err).Msg("Failed to kick user while cleaning up portal")
- }
- }
- }
- _, err = intent.LeaveRoom(ctx, mxid)
- if err != nil {
- log.Err(err).Msg("Failed to leave room while cleaning up portal")
- }
-}
-
-func (portal *Portal) HandleMatrixLeave(brSender bridge.User, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix leave").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- if portal.IsPrivateChat() {
- log.Info().Msg("User left private chat portal, cleaning up and deleting...")
- portal.Delete()
- portal.Cleanup(ctx, false)
- return
- } else if portal.bridge.Config.Bridge.BridgeMatrixLeave {
- portal.deleteMember(sender, sender.SignalID, evt)
- }
- portal.CleanupIfEmpty(ctx)
-}
-func (portal *Portal) HandleMatrixKick(brSender bridge.User, ghost bridge.Ghost, evt *event.Event) {
- portal.deleteMember(brSender.(*User), ghost.(*Puppet).SignalID, evt)
-}
-func (portal *Portal) deleteMember(sender *User, target uuid.UUID, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix kick/leave").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- groupChange := &signalmeow.GroupChange{DeleteMembers: []*uuid.UUID{&target}}
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error deleting Member from Signal")
- return
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-func (portal *Portal) HandleMatrixInvite(brSender bridge.User, brGhost bridge.Ghost, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix invite").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- puppet := brGhost.(*Puppet)
- role := signalmeow.GroupMember_DEFAULT
- levels, err := portal.MainIntent().PowerLevels(ctx, portal.MXID)
- if err != nil {
- log.Err(err).Msg("Couldn't get power levels")
- if levels.GetUserLevel(puppet.IntentFor(portal).UserID) >= 50 {
- role = signalmeow.GroupMember_ADMINISTRATOR
- }
- }
- groupChange := &signalmeow.GroupChange{AddMembers: []*signalmeow.AddMember{{
- GroupMember: signalmeow.GroupMember{
- ACI: puppet.SignalID,
- Role: role,
- },
- }}}
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error inviting user on Signal")
- }
- puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID)
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixAcceptKnock(brSender bridge.User, brGhost bridge.Ghost, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix accept knock").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- puppet := brGhost.(*Puppet)
- role := signalmeow.GroupMember_DEFAULT
- levels, err := portal.MainIntent().PowerLevels(ctx, portal.MXID)
- if err != nil {
- log.Err(err).Msg("Couldn't get power levels")
- if levels.GetUserLevel(puppet.IntentFor(portal).UserID) >= 50 {
- role = signalmeow.GroupMember_ADMINISTRATOR
- }
- }
- groupChange := &signalmeow.GroupChange{PromoteRequestingMembers: []*signalmeow.RoleMember{{
- ACI: puppet.SignalID,
- Role: role,
- }}}
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error accepting join request on Signal")
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixRejectKnock(brSender bridge.User, brGhost bridge.Ghost, evt *event.Event) {
- portal.removeRequestingMember(brSender.(*User), brGhost.(*Puppet).SignalID, evt)
-}
-
-func (portal *Portal) HandleMatrixRetractKnock(brSender bridge.User, evt *event.Event) {
- portal.removeRequestingMember(brSender.(*User), brSender.(*User).SignalID, evt)
-}
-
-func (portal *Portal) removeRequestingMember(sender *User, target uuid.UUID, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix knock -> leave").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- groupChange := &signalmeow.GroupChange{DeleteRequestingMembers: []*uuid.UUID{&target}}
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error removing requesting member")
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixKnock(brSender bridge.User, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix knock").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- log.Debug().Msg("Knocks aren't implemented yet :(")
-}
-
-func (portal *Portal) HandleMatrixBan(brSender bridge.User, brGhost bridge.Ghost, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix ban").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- puppet := brGhost.(*Puppet)
- groupChange := &signalmeow.GroupChange{AddBannedMembers: []*signalmeow.BannedMember{{
- ServiceID: libsignalgo.NewACIServiceID(puppet.SignalID),
- Timestamp: uint64(time.Now().UnixMilli()),
- }}}
- switch prevMembership := evt.Unsigned.PrevContent.AsMember().Membership; prevMembership {
- case event.MembershipJoin:
- groupChange.DeleteMembers = []*uuid.UUID{&puppet.SignalID}
- case event.MembershipKnock:
- groupChange.DeleteRequestingMembers = []*uuid.UUID{&puppet.SignalID}
- case event.MembershipInvite:
- serviceID := libsignalgo.NewACIServiceID(puppet.SignalID)
- groupChange.DeletePendingMembers = []*libsignalgo.ServiceID{&serviceID}
- }
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error banning on Signal")
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixUnban(brSender bridge.User, brGhost bridge.Ghost, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix unban").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- puppet := brGhost.(*Puppet)
- serviceID := libsignalgo.NewACIServiceID(puppet.SignalID)
- groupChange := &signalmeow.GroupChange{DeleteBannedMembers: []*libsignalgo.ServiceID{&serviceID}}
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error unbanning on Signal")
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixPowerLevels(brSender bridge.User, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix power levels").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- if !sender.IsLoggedIn() {
- log.Warn().Msg("Can't change power levels: user is not logged in")
- return
- }
- evt.Content.ParseRaw(event.StatePowerLevels)
- levels := evt.Content.AsPowerLevels()
- var prevLevels *event.PowerLevelsEventContent
- if evt.Unsigned.PrevContent != nil {
- evt.Unsigned.PrevContent.ParseRaw(event.StatePowerLevels)
- prevLevels = evt.Unsigned.PrevContent.AsPowerLevels()
- } else {
- prevLevels = &event.PowerLevelsEventContent{}
- }
- groupChange := &signalmeow.GroupChange{}
- var role signalmeow.GroupMemberRole
- for user, level := range levels.Users {
- prevLevel := prevLevels.GetUserLevel(user)
- if (level >= 50 && prevLevel < 50) || (level < 50 && prevLevel >= 50) {
- puppet := portal.bridge.GetPuppetByMXID(user)
- if puppet == nil {
- log.Warn().Stringer("mxid", user).Msg("Couldn't get puppet for power level change")
- continue
- }
- role = signalmeow.GroupMember_DEFAULT
- if level >= 50 {
- role = signalmeow.GroupMember_ADMINISTRATOR
- }
- groupChange.ModifyMemberRoles = append(groupChange.ModifyMemberRoles, &signalmeow.RoleMember{
- ACI: puppet.SignalID,
- Role: role,
- })
- }
- }
- if levels.EventsDefault >= 50 && prevLevels.EventsDefault < 50 {
- announcementsOnly := true
- groupChange.ModifyAnnouncementsOnly = &announcementsOnly
- } else if levels.EventsDefault < 50 && prevLevels.EventsDefault >= 50 {
- announcementsOnly := false
- groupChange.ModifyAnnouncementsOnly = &announcementsOnly
- }
- if levels.StateDefault() >= 50 && prevLevels.StateDefault() < 50 {
- attributesAccess := signalmeow.AccessControl_ADMINISTRATOR
- groupChange.ModifyAttributesAccess = &attributesAccess
- } else if levels.StateDefault() < 50 && prevLevels.StateDefault() >= 50 {
- attributesAccess := signalmeow.AccessControl_MEMBER
- groupChange.ModifyAttributesAccess = &attributesAccess
- }
- if levels.Invite() >= 50 && prevLevels.Invite() < 50 {
- memberAccess := signalmeow.AccessControl_ADMINISTRATOR
- groupChange.ModifyMemberAccess = &memberAccess
- } else if levels.Invite() < 50 && prevLevels.Invite() >= 50 {
- memberAccess := signalmeow.AccessControl_MEMBER
- groupChange.ModifyMemberAccess = &memberAccess
- }
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error changing group access control")
- return
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixJoinRule(brSender bridge.User, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix join rule").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- if !sender.IsLoggedIn() {
- log.Warn().Msg("Can't change join rule: user is not logged in")
- return
- }
- evt.Content.ParseRaw(event.StateJoinRules)
- joinRule := evt.Content.AsJoinRules().JoinRule
- groupChange := &signalmeow.GroupChange{}
- addFromInviteLinkAccess := signalmeow.AccessControl_UNSATISFIABLE
- if joinRule == event.JoinRuleKnock {
- addFromInviteLinkAccess = signalmeow.AccessControl_ADMINISTRATOR
- } else if joinRule == event.JoinRulePublic {
- addFromInviteLinkAccess = signalmeow.AccessControl_ANY
- }
- groupChange.ModifyAddFromInviteLinkAccess = &addFromInviteLinkAccess
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error updating group access control")
- return
- }
- portal.Revision = revision
- portal.Update(ctx)
-}
-
-func (portal *Portal) HandleMatrixMeta(brSender bridge.User, evt *event.Event) {
- log := portal.log.With().
- Str("action", "handle matrix meta").
- Stringer("event_id", evt.ID).
- Str("event_type", evt.Type.String()).
- Logger()
- ctx := log.WithContext(context.TODO())
- sender := brSender.(*User)
- if !sender.IsLoggedIn() {
- log.Warn().Msg("Can't change room info: user is not logged in")
- return
- }
-
- var err error
- groupChange := &signalmeow.GroupChange{Revision: portal.Revision + 1}
- var avatarPath *string
- var avatarHash string
- var avatarURL id.ContentURI
- var avatarChanged bool
- switch content := evt.Content.Parsed.(type) {
- case *event.RoomNameEventContent:
- if content.Name == portal.Name {
- return
- }
- portal.Name = content.Name
- groupChange.ModifyTitle = &content.Name
- case *event.TopicEventContent:
- if content.Topic == portal.Topic {
- return
- }
- portal.Topic = content.Topic
- groupChange.ModifyDescription = &content.Topic
- case *event.RoomAvatarEventContent:
- url := content.URL.ParseOrIgnore()
- if url == portal.AvatarURL {
- return
- }
- var data []byte
- if !url.IsEmpty() {
- data, err = portal.MainIntent().DownloadBytes(ctx, url)
- if err != nil {
- log.Err(err).Stringer("Failed to download updated avatar %s", url)
- return
- }
- log.Debug().Stringers("%s set the group avatar to %s", []fmt.Stringer{sender.MXID, url})
- } else {
- log.Debug().Stringer("%s removed the group avatar", sender.MXID)
- }
- avatarPath, err = sender.Client.UploadGroupAvatar(ctx, data, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Failed to upload group avatar")
- return
- }
- groupChange.ModifyAvatar = avatarPath
- hash := sha256.Sum256(data)
- avatarHash = hex.EncodeToString(hash[:])
- avatarChanged = true
- avatarURL = url
- }
- revision, err := sender.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- log.Err(err).Msg("Error updating group attributes")
- return
- }
- if avatarChanged {
- log.Debug().Msg("Successfully updated group avatar")
- portal.AvatarSet = true
- portal.AvatarPath = *avatarPath
- portal.AvatarHash = avatarHash
- portal.AvatarURL = avatarURL
- portal.UpdateBridgeInfo(ctx)
- }
- portal.Revision = revision
- portal.Update(ctx)
- log.Info().Msg("finished updating group")
-}
-
-func (portal *Portal) CleanupIfEmpty(ctx context.Context) {
- log := portal.log.With().
- Str("action", "Clean up if empty").
- Logger()
- users, err := portal.GetMatrixUsers(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to get Matrix user list to determine if portal needs to be cleaned up")
- return
- }
-
- if len(users) == 0 {
- log.Info().Msg("Room seems to be empty, cleaning up...")
- portal.Delete()
- portal.Cleanup(ctx, false)
- }
-}
-
-func (portal *Portal) GetMatrixUsers(ctx context.Context) ([]id.UserID, error) {
- members, err := portal.MainIntent().JoinedMembers(ctx, portal.MXID)
- if err != nil {
- return nil, fmt.Errorf("failed to get member list: %w", err)
- }
- var users []id.UserID
- for userID := range members.Joined {
- _, isPuppet := portal.bridge.ParsePuppetMXID(userID)
- if !isPuppet && userID != portal.bridge.Bot.UserID {
- users = append(users, userID)
- }
- }
- return users, nil
-}
-
-func (portal *Portal) GetInviteLink(ctx context.Context, source *User) (string, error) {
- info, err := source.Client.RetrieveGroupByID(ctx, portal.GroupID(), portal.Revision)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).
- Stringer("source_user_id", source.MXID).
- Msg("Failed to fetch group info")
- return "", err
- }
- inviteLinkPassword, err := info.GetInviteLink()
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to get invite link")
- }
- return inviteLinkPassword, nil
-}
-
-func (portal *Portal) ResetInviteLink(ctx context.Context, source *User) error {
- inviteLinkPassword := signalmeow.GenerateInviteLinkPassword()
- groupChange := &signalmeow.GroupChange{ModifyInviteLinkPassword: &inviteLinkPassword}
- revision, err := source.Client.UpdateGroup(ctx, groupChange, portal.GroupID())
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Error setting invite link password")
- return err
- }
- portal.Revision = revision
- return portal.Update(ctx)
-}
-
-func (portal *Portal) GetEncryptionEventContent() (evt *event.EncryptionEventContent) {
- evt = &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}
- if rot := portal.bridge.Config.Bridge.Encryption.Rotation; rot.EnableCustom {
- evt.RotationPeriodMillis = rot.Milliseconds
- evt.RotationPeriodMessages = rot.Messages
- }
- return
-}
diff --git a/provisioning.go b/provisioning.go
deleted file mode 100644
index 46b52d1..0000000
--- a/provisioning.go
+++ /dev/null
@@ -1,640 +0,0 @@
-// 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 .
-
-package main
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- _ "net/http/pprof"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "github.com/google/uuid"
- "github.com/gorilla/mux"
- "github.com/rs/zerolog"
- "github.com/rs/zerolog/hlog"
- "go.mau.fi/util/requestlog"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mautrix-signal/legacyprovision"
- "go.mau.fi/mautrix-signal/pkg/libsignalgo"
- "go.mau.fi/mautrix-signal/pkg/signalmeow"
- "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
-)
-
-type provisioningContextKey int
-
-const (
- provisioningUserKey provisioningContextKey = iota
-)
-
-type provisioningHandle struct {
- id int
- context context.Context
- cancel context.CancelFunc
- channel <-chan signalmeow.ProvisioningResponse
-}
-
-type ProvisioningAPI struct {
- bridge *SignalBridge
- log zerolog.Logger
- provisioningHandles []*provisioningHandle
- provisioningUsers map[string]int
- provisioningMutexes map[string]*sync.Mutex
-}
-
-func (prov *ProvisioningAPI) Init() {
- prov.log.Debug().Str("prefix", prov.bridge.Config.Bridge.Provisioning.Prefix).Msg("Enabling provisioning API")
- prov.provisioningUsers = make(map[string]int)
- prov.provisioningMutexes = make(map[string]*sync.Mutex)
- r := prov.bridge.AS.Router.PathPrefix(prov.bridge.Config.Bridge.Provisioning.Prefix).Subrouter()
- r.Use(hlog.NewHandler(prov.log))
- r.Use(requestlog.AccessLogger(true))
- r.Use(prov.AuthMiddleware)
- r.HandleFunc("/v2/whoami", prov.WhoAmI).Methods(http.MethodGet)
- r.HandleFunc("/v2/link/new", prov.LinkNew).Methods(http.MethodPost)
- r.HandleFunc("/v2/link/wait/scan", prov.LinkWaitForScan).Methods(http.MethodPost)
- r.HandleFunc("/v2/link/wait/account", prov.LinkWaitForAccount).Methods(http.MethodPost)
- r.HandleFunc("/v2/logout", prov.Logout).Methods(http.MethodPost)
- r.HandleFunc("/v2/resolve_identifier/{phonenum}", prov.ResolveIdentifier).Methods(http.MethodGet)
- r.HandleFunc("/v2/pm/{phonenum}", prov.StartPM).Methods(http.MethodPost)
-
- if prov.bridge.Config.Bridge.Provisioning.DebugEndpoints {
- prov.log.Debug().Msg("Enabling debug API at /debug")
- r := prov.bridge.AS.Router.PathPrefix("/debug").Subrouter()
- r.Use(prov.AuthMiddleware)
- r.PathPrefix("/pprof").Handler(http.DefaultServeMux)
- }
-}
-
-func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- auth := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
- if auth != prov.bridge.Config.Bridge.Provisioning.SharedSecret {
- zerolog.Ctx(r.Context()).Warn().Msg("Authentication token does not match shared secret")
- legacyprovision.JSONResponse(w, http.StatusForbidden, &mautrix.RespError{
- Err: "Authentication token does not match shared secret",
- ErrCode: mautrix.MForbidden.ErrCode,
- })
- return
- }
- userID := r.URL.Query().Get("user_id")
- user := prov.bridge.GetUserByMXID(id.UserID(userID))
- h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), provisioningUserKey, user)))
- })
-}
-
-func (prov *ProvisioningAPI) resolveIdentifier(ctx context.Context, user *User, inputPhone string) (int, *legacyprovision.ResolveIdentifierResponse, error) {
- if user.Client == nil {
- return http.StatusUnauthorized, nil, errors.New("not currently connected to Signal")
- }
- e164Number, err := strconv.ParseUint(numberCleaner.Replace(inputPhone), 10, 64)
- if err != nil {
- return http.StatusBadRequest, nil, fmt.Errorf("error parsing phone number: %w", err)
- }
- e164String := fmt.Sprintf("+%d", e164Number)
- var aci, pni uuid.UUID
- var recipient *types.Recipient
- if recipient, err = user.Client.ContactByE164(ctx, e164String); err != nil {
- return http.StatusInternalServerError, nil, fmt.Errorf("error looking up number in local contact list: %w", err)
- } else if recipient != nil {
- aci = recipient.ACI
- pni = recipient.PNI
- } else if resp, err := user.Client.LookupPhone(ctx, e164Number); err != nil {
- return http.StatusInternalServerError, 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 http.StatusNotFound, nil, errors.New("user not found on Signal")
- }
- recipient, err = user.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
- }
- zerolog.Ctx(ctx).Debug().
- Uint64("e164", e164Number).
- Stringer("aci", aci).
- Stringer("pni", pni).
- Msg("Found DM target user")
-
- var targetServiceID libsignalgo.ServiceID
- var otherUserInfo *legacyprovision.ResolveIdentifierResponseOtherUser
- if aci != uuid.Nil {
- targetServiceID = libsignalgo.NewACIServiceID(aci)
- puppet := prov.bridge.GetPuppetBySignalID(aci)
- otherUserInfo = &legacyprovision.ResolveIdentifierResponseOtherUser{
- MXID: puppet.MXID,
- DisplayName: puppet.Name,
- AvatarURL: puppet.AvatarURL,
- }
- } else {
- targetServiceID = libsignalgo.NewPNIServiceID(pni)
- // TODO fill other user displayname/avatar if there's a contact entry?
- }
- portal := user.GetPortalByChatID(targetServiceID.String())
-
- return http.StatusOK, &legacyprovision.ResolveIdentifierResponse{
- RoomID: portal.MXID,
- ChatID: legacyprovision.ResolveIdentifierResponseChatID{
- UUID: targetServiceID.String(),
- Number: e164String,
- },
- OtherUser: otherUserInfo,
- }, nil
-}
-
-func (prov *ProvisioningAPI) ResolveIdentifier(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
- phoneNum := mux.Vars(r)["phonenum"]
-
- log := prov.log.With().
- Str("action", "resolve_identifier").
- Stringer("user_id", user.MXID).
- Str("phone_num", phoneNum).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("resolving identifier")
-
- status, resp, err := prov.resolveIdentifier(ctx, user, phoneNum)
- if err != nil {
- errCode := "M_INTERNAL"
- if status == http.StatusNotFound {
- log.Debug().Msg("contact not found")
- errCode = "M_NOT_FOUND"
- } else {
- log.Err(err).Msg("error looking up contact")
- }
- legacyprovision.JSONResponse(w, status, legacyprovision.Error{
- Success: false,
- Error: err.Error(),
- ErrCode: errCode,
- })
- return
- }
- legacyprovision.JSONResponse(w, status, legacyprovision.Response{
- Success: true,
- Status: "ok",
- ResolveIdentifierResponse: resp,
- })
-}
-
-func (prov *ProvisioningAPI) StartPM(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
- phoneNum := mux.Vars(r)["phonenum"]
-
- log := prov.log.With().
- Str("action", "start_pm").
- Stringer("user_id", user.MXID).
- Str("phone_num", phoneNum).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("starting private message")
-
- status, resp, err := prov.resolveIdentifier(ctx, user, phoneNum)
- if err != nil {
- errCode := "M_INTERNAL"
- if status == http.StatusNotFound {
- log.Debug().Msg("contact not found")
- errCode = "M_NOT_FOUND"
- } else {
- log.Err(err).Msg("error looking up contact")
- }
- legacyprovision.JSONResponse(w, status, legacyprovision.Error{
- Success: false,
- Error: err.Error(),
- ErrCode: errCode,
- })
- return
- }
-
- portal := user.GetPortalByChatID(resp.ChatID.UUID)
- if portal.MXID == "" {
- if err := portal.CreateMatrixRoom(r.Context(), user, 0); err != nil {
- log.Err(err).Msg("error looking up contact")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: "Error creating Matrix room",
- ErrCode: "M_INTERNAL",
- })
- return
- }
- resp.JustCreated = true
- resp.RoomID = portal.MXID
- }
- if resp.JustCreated {
- status = http.StatusCreated
- }
-
- legacyprovision.JSONResponse(w, status, legacyprovision.Response{
- Success: true,
- Status: "ok",
- ResolveIdentifierResponse: resp,
- })
-}
-
-func (prov *ProvisioningAPI) mutexForUser(user *User) *sync.Mutex {
- if _, ok := prov.provisioningMutexes[user.MXID.String()]; !ok {
- prov.provisioningMutexes[user.MXID.String()] = &sync.Mutex{}
- }
- return prov.provisioningMutexes[user.MXID.String()]
-}
-
-func (prov *ProvisioningAPI) newOrExistingSession(user *User) (newSessionLoggedIn bool, handle *provisioningHandle, err error) {
- prov.mutexForUser(user).Lock()
- defer prov.mutexForUser(user).Unlock()
-
- if existingSessionID, ok := prov.provisioningUsers[user.MXID.String()]; ok {
- provisioningHandle := prov.provisioningHandles[existingSessionID]
- return false, provisioningHandle, nil
- }
-
- provChan, err := user.Login()
- if err != nil {
- return false, nil, fmt.Errorf("Error logging in: %w", err)
- }
- provisioningCtx, cancel := context.WithCancel(context.TODO())
- handle = &provisioningHandle{
- context: provisioningCtx,
- cancel: cancel,
- channel: provChan,
- }
- prov.provisioningHandles = append(prov.provisioningHandles, handle)
- handle.id = len(prov.provisioningHandles) - 1
- prov.provisioningUsers[user.MXID.String()] = handle.id
- return true, handle, nil
-}
-
-func (prov *ProvisioningAPI) existingSession(user *User) (handle *provisioningHandle) {
- prov.mutexForUser(user).Lock()
- defer prov.mutexForUser(user).Unlock()
-
- if existingSessionID, ok := prov.provisioningUsers[user.MXID.String()]; ok {
- provisioningHandle := prov.provisioningHandles[existingSessionID]
- return provisioningHandle
- }
- return nil
-}
-
-func (prov *ProvisioningAPI) clearSession(ctx context.Context, user *User) {
- log := zerolog.Ctx(ctx).With().Str("function", "clearSession").Logger()
- prov.mutexForUser(user).Lock()
- defer prov.mutexForUser(user).Unlock()
-
- if existingSessionID, ok := prov.provisioningUsers[user.MXID.String()]; ok {
- log.Debug().Int("existing_session_id", existingSessionID).Msg("clearing existing session")
- if existingSessionID >= len(prov.provisioningHandles) {
- log.Warn().Msg("session does not exist")
- return
- }
- if prov.provisioningHandles[existingSessionID].cancel != nil {
- prov.provisioningHandles[existingSessionID].cancel()
- }
- prov.provisioningHandles[existingSessionID] = nil
- delete(prov.provisioningUsers, user.MXID.String())
- } else {
- prov.log.Debug().Msg("no session found")
- }
-}
-
-func (prov *ProvisioningAPI) loginOrSendError(ctx context.Context, w http.ResponseWriter, user *User) (*provisioningHandle, error) {
- newSessionLoggedIn, handle, err := prov.newOrExistingSession(user)
- if err != nil {
- return nil, err
- }
- if !newSessionLoggedIn {
- zerolog.Ctx(ctx).Debug().
- Int("existing_provisioning_handle", handle.id).
- Msg("user already has pending provisioning request, cancelling")
- prov.clearSession(ctx, user)
- _, handle, err = prov.newOrExistingSession(user)
- if err != nil {
- return nil, fmt.Errorf("error logging in after cancelling existing session: %w", err)
- }
- }
- return handle, nil
-}
-
-func (prov *ProvisioningAPI) checkSessionAndReturnHandle(ctx context.Context, w http.ResponseWriter, currentSession int) *provisioningHandle {
- log := zerolog.Ctx(ctx).With().Str("function", "checkSessionAndReturnHandle").Logger()
- user := ctx.Value(provisioningUserKey).(*User)
- handle := prov.existingSession(user)
- if handle == nil {
- log.Warn().Msg("no session found")
- legacyprovision.JSONResponse(w, http.StatusNotFound, legacyprovision.Error{
- Success: false,
- Error: "No session found",
- ErrCode: "M_NOT_FOUND",
- })
- return nil
- }
- if handle.id != currentSession {
- log.Warn().
- Int("handle_id", handle.id).
- Int("current_session", currentSession).
- Msg("session_id does not match user's session_id")
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "session_id does not match user's session_id",
- ErrCode: "M_BAD_JSON",
- })
- return nil
- }
- return handle
-}
-
-func (prov *ProvisioningAPI) WhoAmI(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
- log := prov.log.With().
- Str("action", "whoami").
- Stringer("user_id", user.MXID).
- Logger()
- log.Debug().Msg("getting whoami")
-
- data := legacyprovision.WhoAmIResponse{
- Permissions: int(user.PermissionLevel),
- MXID: user.MXID.String(),
- }
- if user.IsLoggedIn() {
- data.Signal = &legacyprovision.WhoAmIResponseSignal{
- Number: user.SignalUsername,
- UUID: user.SignalID.String(),
- Ok: user.Client.IsConnected(),
- }
- puppet := user.bridge.GetPuppetBySignalID(user.SignalID)
- if puppet != nil {
- data.Signal.Name = puppet.Name
- }
- }
- legacyprovision.JSONResponse(w, http.StatusOK, data)
-}
-
-func (prov *ProvisioningAPI) LinkNew(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
- log := prov.log.With().
- Str("action", "link_new").
- Stringer("user_id", user.MXID).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("starting login")
-
- handle, err := prov.loginOrSendError(ctx, w, user)
- if err != nil {
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: err.Error(),
- ErrCode: "M_INTERNAL",
- })
- return
- }
-
- log = log.With().Int("session_id", handle.id).Logger()
- log.Debug().Msg("waiting for provisioning response")
-
- select {
- case resp := <-handle.channel:
- if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
- log.Err(resp.Err).Msg("Error getting provisioning URL")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: resp.Err.Error(),
- ErrCode: "M_INTERNAL",
- })
- return
- }
- if resp.State != signalmeow.StateProvisioningURLReceived {
- log.Err(resp.Err).Stringer("state", resp.State).Msg("unexpected state")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: fmt.Sprintf("Unexpected state %s", resp.State.String()),
- ErrCode: "M_INTERNAL",
- })
- return
- }
-
- log.Debug().Str("provisioning_url", resp.ProvisioningURL).Msg("provisioning URL received")
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
- Success: true,
- Status: "provisioning_url_received",
- SessionID: fmt.Sprintf("%d", handle.id),
- URI: resp.ProvisioningURL,
- })
- case <-time.After(30 * time.Second):
- log.Warn().Msg("Timeout waiting for provisioning response (new)")
- legacyprovision.JSONResponse(w, http.StatusGatewayTimeout, legacyprovision.Error{
- Success: false,
- Error: "Timeout waiting for provisioning response (new)",
- ErrCode: "M_TIMEOUT",
- })
- }
-}
-
-func (prov *ProvisioningAPI) LinkWaitForScan(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
-
- var body legacyprovision.LinkWaitForScanRequest
- err := json.NewDecoder(r.Body).Decode(&body)
- if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "Error decoding JSON body",
- ErrCode: "M_BAD_JSON",
- })
- return
- }
- sessionID, err := strconv.Atoi(body.SessionID)
- if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "Error decoding session ID in JSON body",
- ErrCode: "M_BAD_JSON",
- })
- return
- }
-
- log := prov.log.With().
- Str("action", "link_wait_for_scan").
- Stringer("user_id", user.MXID).
- Str("session_id", body.SessionID).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("waiting for scan")
-
- handle := prov.checkSessionAndReturnHandle(ctx, w, sessionID)
- if handle == nil {
- return
- }
-
- select {
- case resp := <-handle.channel:
- if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
- log.Err(resp.Err).Msg("Error waiting for scan")
- // If context was cancelled be chill
- if errors.Is(resp.Err, context.Canceled) {
- log.Debug().Msg("Context cancelled waiting for scan")
- return
- }
- // If we error waiting for the scan, treat it as a normal error not 5xx
- // so that the client will retry quietly. Also, it's really not an internal
- // error, sitting with a WS open waiting for a scan is inherently flaky.
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: resp.Err.Error(),
- ErrCode: "M_INTERNAL",
- })
- return
- }
- if resp.State != signalmeow.StateProvisioningDataReceived {
- log.Err(resp.Err).Stringer("state", resp.State).Msg("unexpected state")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: fmt.Sprintf("Unexpected state %s", resp.State.String()),
- ErrCode: "M_INTERNAL",
- })
- return
- }
- log.Debug().Msg("provisioning data received")
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
- Success: true,
- Status: "provisioning_data_received",
- })
-
- // Update user with SignalID
- if resp.ProvisioningData.ACI != uuid.Nil {
- user.saveSignalID(ctx, resp.ProvisioningData.ACI, resp.ProvisioningData.Number)
- }
- return
- case <-time.After(45 * time.Second):
- log.Warn().Msg("Timeout waiting for provisioning response (scan)")
- // Using 400 here to match the old bridge
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "Timeout waiting for QR code scan",
- ErrCode: "M_BAD_REQUEST",
- })
- return
- }
-}
-
-func (prov *ProvisioningAPI) LinkWaitForAccount(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
-
- var body legacyprovision.LinkWaitForAccountRequest
- err := json.NewDecoder(r.Body).Decode(&body)
- if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "Error decoding JSON body",
- ErrCode: "M_BAD_JSON",
- })
- return
- }
- sessionID, err := strconv.Atoi(body.SessionID)
- if err != nil {
- legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
- Success: false,
- Error: "Error decoding session ID in JSON body",
- ErrCode: "M_BAD_JSON",
- })
- return
- }
- deviceName := body.DeviceName
-
- log := prov.log.With().
- Str("action", "link_wait_for_account").
- Stringer("user_id", user.MXID).
- Int("session_id", sessionID).
- Str("device_name", deviceName).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("waiting for account")
-
- handle := prov.checkSessionAndReturnHandle(ctx, w, sessionID)
- if handle == nil {
- return
- }
-
- select {
- case resp := <-handle.channel:
- if resp.Err != nil || resp.State == signalmeow.StateProvisioningError {
- log.Err(resp.Err).Msg("Error waiting for account")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: resp.Err.Error(),
- ErrCode: "M_INTERNAL",
- })
- return
- }
- if resp.State != signalmeow.StateProvisioningPreKeysRegistered {
- log.Err(resp.Err).Stringer("state", resp.State).Msg("unexpected state")
- legacyprovision.JSONResponse(w, http.StatusInternalServerError, legacyprovision.Error{
- Success: false,
- Error: fmt.Sprintf("Unexpected state %s", resp.State.String()),
- ErrCode: "M_INTERNAL",
- })
- return
- }
-
- log.Debug().Msg("prekeys registered")
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
- Success: true,
- Status: "prekeys_registered",
- UUID: user.SignalID.String(),
- Number: user.SignalUsername,
- })
-
- // Connect to Signal!!
- user.Connect()
- return
- case <-time.After(30 * time.Second):
- log.Warn().Msg("Timeout waiting for provisioning response (account)")
- legacyprovision.JSONResponse(w, http.StatusGatewayTimeout, legacyprovision.Error{
- Success: false,
- Error: "Timeout waiting for provisioning response (account)",
- ErrCode: "M_TIMEOUT",
- })
- return
- }
-}
-
-func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
- user := r.Context().Value(provisioningUserKey).(*User)
- log := prov.log.With().
- Str("action", "logout").
- Stringer("user_id", user.MXID).
- Logger()
- ctx := log.WithContext(r.Context())
- log.Debug().Msg("Logout called (but not logging out)")
-
- prov.clearSession(ctx, user)
-
- // For now do nothing - we need this API to return 200 to be compatible with
- // the old Signal bridge, which needed a call to Logout before allowing LinkNew
- // to be called, but we don't actually want to logout, we want to allow a reconnect.
- legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
- Success: true,
- Status: "logged_out",
- })
-}
diff --git a/puppet.go b/puppet.go
deleted file mode 100644
index 857312e..0000000
--- a/puppet.go
+++ /dev/null
@@ -1,411 +0,0 @@
-// 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 .
-
-package main
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "net/http"
- "regexp"
-
- "github.com/google/uuid"
- "github.com/rs/zerolog"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/appservice"
- "maunium.net/go/mautrix/bridge"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mautrix-signal/database"
- "go.mau.fi/mautrix-signal/pkg/signalmeow/types"
-)
-
-func (br *SignalBridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
- signalID, ok := br.ParsePuppetMXID(mxid)
- if !ok {
- return nil
- }
-
- return br.GetPuppetBySignalID(signalID)
-}
-
-func (br *SignalBridge) GetPuppetBySignalIDString(id string) *Puppet {
- parsed, err := uuid.Parse(id)
- if err != nil {
- return nil
- }
- return br.GetPuppetBySignalID(parsed)
-}
-
-func (br *SignalBridge) GetPuppetBySignalID(id uuid.UUID) *Puppet {
- if id == uuid.Nil {
- br.ZLog.Warn().Msg("Trying to get puppet with empty signal_user_id")
- return nil
- }
-
- br.puppetsLock.Lock()
- defer br.puppetsLock.Unlock()
-
- puppet, ok := br.puppets[id]
- if !ok {
- dbPuppet, err := br.DB.Puppet.GetBySignalID(context.TODO(), id)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get puppet from database")
- return nil
- }
- return br.loadPuppet(context.TODO(), dbPuppet, &id)
- }
- return puppet
-}
-
-func (br *SignalBridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet {
- br.puppetsLock.Lock()
- defer br.puppetsLock.Unlock()
-
- puppet, ok := br.puppetsByCustomMXID[mxid]
- if !ok {
- dbPuppet, err := br.DB.Puppet.GetByCustomMXID(context.TODO(), mxid)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get puppet from database")
- return nil
- }
- return br.loadPuppet(context.TODO(), dbPuppet, nil)
- }
- return puppet
-}
-
-func (br *SignalBridge) GetAllPuppetsWithCustomMXID() []*Puppet {
- puppets, err := br.DB.Puppet.GetAllWithCustomMXID(context.TODO())
- if err != nil {
- br.ZLog.Error().Err(err).Msg("Failed to get all puppets with custom MXID")
- return nil
- }
- return br.dbPuppetsToPuppets(puppets)
-}
-
-func (br *SignalBridge) FormatPuppetMXID(u uuid.UUID) id.UserID {
- return id.NewUserID(
- br.Config.Bridge.FormatUsername(u.String()),
- br.Config.Homeserver.Domain,
- )
-}
-
-func (br *SignalBridge) loadPuppet(ctx context.Context, dbPuppet *database.Puppet, u *uuid.UUID) *Puppet {
- if dbPuppet == nil {
- if u == nil {
- return nil
- }
- dbPuppet = br.DB.Puppet.New()
- dbPuppet.SignalID = *u
- err := dbPuppet.Insert(ctx)
- if err != nil {
- br.ZLog.Error().Err(err).Stringer("signal_user_id", *u).Msg("Failed to insert new puppet")
- return nil
- }
- }
-
- puppet := br.NewPuppet(dbPuppet)
- br.puppets[puppet.SignalID] = puppet
- if puppet.CustomMXID != "" {
- br.puppetsByCustomMXID[puppet.CustomMXID] = puppet
- }
- return puppet
-}
-
-func (br *SignalBridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet {
- br.puppetsLock.Lock()
- defer br.puppetsLock.Unlock()
-
- output := make([]*Puppet, len(dbPuppets))
- for index, dbPuppet := range dbPuppets {
- if dbPuppet == nil {
- continue
- }
- puppet, ok := br.puppets[dbPuppet.SignalID]
- if !ok {
- puppet = br.loadPuppet(context.TODO(), dbPuppet, nil)
- }
- output[index] = puppet
- }
- return output
-}
-
-func (br *SignalBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
- return &Puppet{
- Puppet: dbPuppet,
- bridge: br,
- log: br.ZLog.With().Stringer("signal_user_id", dbPuppet.SignalID).Logger(),
-
- MXID: br.FormatPuppetMXID(dbPuppet.SignalID),
- }
-}
-
-func (br *SignalBridge) ParsePuppetMXID(mxid id.UserID) (uuid.UUID, bool) {
- if userIDRegex == nil {
- pattern := fmt.Sprintf(
- "^@%s:%s$",
- // The "SignalID" portion of the MXID is a (lowercase) UUID
- br.Config.Bridge.FormatUsername("([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"),
- br.Config.Homeserver.Domain,
- )
- br.ZLog.Debug().Str("pattern", pattern).Msg("Compiling userIDRegex")
-
- userIDRegex = regexp.MustCompile(pattern)
- }
-
- match := userIDRegex.FindStringSubmatch(string(mxid))
- if len(match) == 2 {
- parsed, err := uuid.Parse(match[1])
- if err != nil {
- return uuid.Nil, false
- }
- return parsed, true
- }
-
- return uuid.Nil, false
-}
-
-type Puppet struct {
- *database.Puppet
-
- bridge *SignalBridge
- log zerolog.Logger
-
- MXID id.UserID
-
- customIntent *appservice.IntentAPI
- customUser *User
-}
-
-var userIDRegex *regexp.Regexp
-
-var (
- _ bridge.Ghost = (*Puppet)(nil)
- _ bridge.GhostWithProfile = (*Puppet)(nil)
-)
-
-func (puppet *Puppet) GetMXID() id.UserID {
- return puppet.MXID
-}
-
-func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
- return puppet.bridge.AS.Intent(puppet.MXID)
-}
-
-func (puppet *Puppet) CustomIntent() *appservice.IntentAPI {
- if puppet == nil {
- return nil
- }
- return puppet.customIntent
-}
-
-func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
- if puppet != nil {
- if puppet.customIntent == nil || portal.UserID().UUID == puppet.SignalID {
- return puppet.DefaultIntent()
- }
- return puppet.customIntent
- }
- return nil
-}
-
-func (puppet *Puppet) GetDisplayname() string {
- return puppet.Name
-}
-
-func (puppet *Puppet) GetAvatarURL() id.ContentURI {
- return puppet.AvatarURL
-}
-
-func (puppet *Puppet) UpdateInfo(ctx context.Context, source *User, contactAvatar *types.ContactAvatar) {
- log := zerolog.Ctx(ctx).With().
- Str("function", "Puppet.UpdateInfo").
- Stringer("signal_user_id", puppet.SignalID).
- Logger()
- ctx = log.WithContext(ctx)
- var err error
- log.Debug().Msg("Fetching contact info to update puppet")
- info, err := source.Client.ContactByACI(ctx, puppet.SignalID)
- if err != nil {
- log.Err(err).Msg("Failed to fetch contact info")
- return
- }
- if !puppet.bridge.Config.Bridge.UseOutdatedProfiles && puppet.ProfileFetchedAt.After(info.Profile.FetchedAt) {
- log.Debug().
- Time("contact_profile_fetched_at", info.Profile.FetchedAt).
- Time("puppet_profile_fetched_at", puppet.ProfileFetchedAt).
- Msg("Ignoring outdated contact info")
- return
- }
- if contactAvatar != nil {
- info.ContactAvatar = *contactAvatar
- }
-
- log.Trace().Msg("Updating puppet info")
-
- update := false
- if puppet.ProfileFetchedAt.IsZero() && !info.Profile.FetchedAt.IsZero() {
- update = true
- }
- puppet.ProfileFetchedAt = info.Profile.FetchedAt
- if info.E164 != "" && puppet.Number != info.E164 {
- puppet.Number = info.E164
- update = true
- }
- update = puppet.updateName(ctx, info) || update
- update = puppet.updateAvatar(ctx, source, info) || update
- if update {
- puppet.ContactInfoSet = false
- puppet.UpdateContactInfo(ctx)
- err = puppet.Update(ctx)
- if err != nil {
- log.Err(err).Msg("Failed to save puppet to database after updating")
- }
- go puppet.updatePortalMeta(ctx)
- log.Debug().Msg("Puppet info updated")
- }
-}
-func (puppet *Puppet) UpdateContactInfo(ctx context.Context) {
- if !puppet.bridge.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) || puppet.ContactInfoSet {
- return
- }
-
- identifiers := []string{
- fmt.Sprintf("signal:%s", puppet.SignalID),
- }
- if puppet.Number != "" {
- identifiers = append(identifiers, fmt.Sprintf("tel:%s", puppet.Number))
- }
- contactInfo := map[string]any{
- "com.beeper.bridge.identifiers": identifiers,
- "com.beeper.bridge.remote_id": puppet.SignalID.String(),
- "com.beeper.bridge.service": "signal",
- "com.beeper.bridge.network": "signal",
- }
- err := puppet.DefaultIntent().BeeperUpdateProfile(ctx, contactInfo)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to store custom contact info in profile")
- } else {
- puppet.ContactInfoSet = true
- }
-}
-
-func (puppet *Puppet) updatePortalMeta(ctx context.Context) {
- for _, portal := range puppet.bridge.FindPrivateChatPortalsWith(puppet.SignalID) {
- // Get room create lock to prevent races between receiving contact info and room creation.
- portal.roomCreateLock.Lock()
- portal.UpdateDMInfo(ctx, false)
- portal.roomCreateLock.Unlock()
- }
-}
-
-func (puppet *Puppet) updateAvatar(ctx context.Context, source *User, info *types.Recipient) bool {
- var avatarData []byte
- var avatarContentType string
- log := zerolog.Ctx(ctx)
- if puppet.bridge.Config.Bridge.UseContactAvatars && info.ContactAvatar.Hash != "" {
- if puppet.AvatarHash == info.ContactAvatar.Hash && puppet.AvatarSet {
- return false
- }
- avatarData = info.ContactAvatar.Image
- avatarContentType = info.ContactAvatar.ContentType
- if avatarData == nil {
- // TODO what to do? 🤔
- return false
- }
- puppet.AvatarSet = false
- puppet.AvatarPath = ""
- } else {
- if puppet.AvatarPath == info.Profile.AvatarPath && puppet.AvatarSet {
- return false
- }
- if info.Profile.AvatarPath == "" {
- puppet.AvatarURL = id.ContentURI{}
- puppet.AvatarPath = ""
- puppet.AvatarHash = ""
- puppet.AvatarSet = false
- err := puppet.DefaultIntent().SetAvatarURL(ctx, puppet.AvatarURL)
- if err != nil {
- log.Err(err).Msg("Failed to remove user avatar")
- return true
- }
- log.Debug().Msg("Avatar removed")
- puppet.AvatarSet = true
- return true
- }
- var err error
- avatarData, err = source.Client.DownloadUserAvatar(ctx, info.Profile.AvatarPath, info.Profile.Key)
- if err != nil {
- log.Err(err).
- Str("profile_avatar_path", info.Profile.AvatarPath).
- Msg("Failed to download new user avatar")
- return true
- }
- avatarContentType = http.DetectContentType(avatarData)
- }
- hash := sha256.Sum256(avatarData)
- newHash := hex.EncodeToString(hash[:])
- if puppet.AvatarHash == newHash && puppet.AvatarSet {
- log.Debug().
- Str("avatar_hash", newHash).
- Str("new_avatar_path", puppet.AvatarPath).
- Msg("Avatar path changed, but hash didn't")
- // Path changed, but actual avatar didn't
- return true
- }
- puppet.AvatarPath = info.Profile.AvatarPath
- puppet.AvatarHash = newHash
- puppet.AvatarSet = false
- puppet.AvatarURL = id.ContentURI{}
- resp, err := puppet.DefaultIntent().UploadBytes(ctx, avatarData, avatarContentType)
- if err != nil {
- log.Err(err).
- Str("avatar_hash", puppet.AvatarHash).
- Msg("Failed to upload new user avatar")
- return true
- }
- puppet.AvatarURL = resp.ContentURI
- err = puppet.DefaultIntent().SetAvatarURL(ctx, puppet.AvatarURL)
- if err != nil {
- log.Err(err).Msg("Failed to update user avatar")
- return true
- }
- log.Debug().
- Str("avatar_hash", newHash).
- Stringer("avatar_mxc", resp.ContentURI).
- Msg("Avatar updated successfully")
- puppet.AvatarSet = true
- return true
-}
-
-func (puppet *Puppet) updateName(ctx context.Context, contact *types.Recipient) bool {
- // TODO set name quality
- newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
- if puppet.NameSet && puppet.Name == newName {
- return false
- }
- puppet.Name = newName
- puppet.NameSet = false
- err := puppet.DefaultIntent().SetDisplayName(ctx, newName)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to update user displayname")
- } else {
- puppet.NameSet = true
- }
- return true
-}
diff --git a/user.go b/user.go
deleted file mode 100644
index dc16f98..0000000
--- a/user.go
+++ /dev/null
@@ -1,1020 +0,0 @@
-// 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 .
-
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "sync"
- "time"
-
- "github.com/google/uuid"
- "github.com/rs/zerolog"
- "golang.org/x/exp/maps"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/appservice"
- "maunium.net/go/mautrix/bridge"
- "maunium.net/go/mautrix/bridge/bridgeconfig"
- "maunium.net/go/mautrix/bridge/status"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-
- "go.mau.fi/mautrix-signal/database"
- "go.mau.fi/mautrix-signal/pkg/libsignalgo"
- "go.mau.fi/mautrix-signal/pkg/signalmeow"
- "go.mau.fi/mautrix-signal/pkg/signalmeow/events"
- signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
-)
-
-var (
- ErrNotConnected = errors.New("not connected")
- ErrNotLoggedIn = errors.New("not logged in")
-)
-
-func (br *SignalBridge) GetUserByMXID(userID id.UserID) *User {
- return br.maybeGetUserByMXID(userID, &userID)
-}
-
-func (br *SignalBridge) GetUserByMXIDIfExists(userID id.UserID) *User {
- return br.maybeGetUserByMXID(userID, nil)
-}
-
-func (br *SignalBridge) maybeGetUserByMXID(userID id.UserID, userIDPtr *id.UserID) *User {
- if userID == br.Bot.UserID || br.IsGhost(userID) {
- return nil
- }
- br.usersLock.Lock()
- defer br.usersLock.Unlock()
-
- user, ok := br.usersByMXID[userID]
- if !ok {
- dbUser, err := br.DB.User.GetByMXID(context.TODO(), userID)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get user from database")
- return nil
- }
- return br.loadUser(context.TODO(), dbUser, userIDPtr)
- }
- return user
-}
-
-func (br *SignalBridge) GetUserBySignalID(id uuid.UUID) *User {
- br.usersLock.Lock()
- defer br.usersLock.Unlock()
- return br.unlockedGetUserBySignalID(id)
-}
-
-func (br *SignalBridge) unlockedGetUserBySignalID(id uuid.UUID) *User {
- user, ok := br.usersBySignalID[id]
- if !ok {
- dbUser, err := br.DB.User.GetBySignalID(context.TODO(), id)
- if err != nil {
- br.ZLog.Err(err).Msg("Failed to get user from database")
- return nil
- }
- return br.loadUser(context.TODO(), dbUser, nil)
- }
- return user
-}
-
-func (br *SignalBridge) GetAllLoggedInUsers() []*User {
- br.usersLock.Lock()
- defer br.usersLock.Unlock()
-
- dbUsers, err := br.DB.User.GetAllLoggedIn(context.TODO())
- if err != nil {
- br.ZLog.Err(err).Msg("Error getting all logged in users")
- return nil
- }
- users := make([]*User, len(dbUsers))
-
- for idx, dbUser := range dbUsers {
- user, ok := br.usersByMXID[dbUser.MXID]
- if !ok {
- user = br.loadUser(context.TODO(), dbUser, nil)
- }
- users[idx] = user
- }
- return users
-}
-
-func (br *SignalBridge) loadUser(ctx context.Context, dbUser *database.User, mxid *id.UserID) *User {
- if dbUser == nil {
- if mxid == nil {
- return nil
- }
- dbUser = br.DB.User.New()
- dbUser.MXID = *mxid
- err := dbUser.Insert(ctx)
- if err != nil {
- br.ZLog.Err(err).Msg("Error creating user %s")
- return nil
- }
- }
-
- user := br.NewUser(dbUser)
- br.usersByMXID[user.MXID] = user
- if user.SignalID != uuid.Nil {
- br.usersBySignalID[user.SignalID] = user
- }
- if user.ManagementRoom != "" {
- br.managementRoomsLock.Lock()
- br.managementRooms[user.ManagementRoom] = user
- br.managementRoomsLock.Unlock()
- }
- return user
-}
-
-func (br *SignalBridge) NewUser(dbUser *database.User) *User {
- user := &User{
- User: dbUser,
- bridge: br,
- log: br.ZLog.With().Stringer("user_id", dbUser.MXID).Logger(),
-
- PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID),
- }
- user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin
- user.BridgeState = br.NewBridgeStateQueue(user)
- return user
-}
-
-type User struct {
- *database.User
-
- sync.Mutex
-
- bridge *SignalBridge
- log zerolog.Logger
-
- Admin bool
- PermissionLevel bridgeconfig.PermissionLevel
-
- Client *signalmeow.Client
-
- BridgeState *bridge.BridgeStateQueue
-
- spaceMembershipChecked bool
- spaceCreateLock sync.Mutex
-}
-
-var (
- _ bridge.User = (*User)(nil)
- _ status.BridgeStateFiller = (*User)(nil)
-)
-
-func (user *User) GetPermissionLevel() bridgeconfig.PermissionLevel {
- return user.PermissionLevel
-}
-
-func (user *User) IsLoggedIn() bool {
- user.Lock()
- defer user.Unlock()
-
- return user.Client != nil && user.Client.IsLoggedIn()
-}
-
-func (user *User) GetManagementRoomID() id.RoomID {
- return user.ManagementRoom
-}
-
-func (user *User) SetManagementRoom(roomID id.RoomID) {
- user.bridge.managementRoomsLock.Lock()
- defer user.bridge.managementRoomsLock.Unlock()
-
- existing, ok := user.bridge.managementRooms[roomID]
- if ok {
- existing.ManagementRoom = ""
- err := existing.Update(context.TODO())
- if err != nil {
- existing.log.Err(err).Msg("Failed to update user when removing management room")
- }
- }
-
- user.ManagementRoom = roomID
- user.bridge.managementRooms[user.ManagementRoom] = user
- err := user.Update(context.TODO())
- if err != nil {
- user.log.Error().Err(err).Msg("Error setting management room")
- }
-}
-
-func (user *User) GetIDoublePuppet() bridge.DoublePuppet {
- p := user.bridge.GetPuppetByCustomMXID(user.MXID)
- if p == nil || p.CustomIntent() == nil {
- return nil
- }
- return p
-}
-
-func (user *User) GetIGhost() bridge.Ghost {
- p := user.bridge.GetPuppetBySignalID(user.SignalID)
- if p == nil {
- return nil
- }
- return p
-}
-
-func (user *User) ensureInvited(ctx context.Context, intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) (ok bool) {
- log := user.log.With().Str("action", "ensure_invited").Stringer("room_id", roomID).Logger()
- if user.bridge.StateStore.IsMembership(ctx, roomID, user.MXID, event.MembershipJoin) {
- ok = true
- return
- }
- extraContent := make(map[string]interface{})
- if isDirect {
- extraContent["is_direct"] = true
- }
- customPuppet := user.bridge.GetPuppetByCustomMXID(user.MXID)
- if customPuppet != nil && customPuppet.CustomIntent() != nil {
- extraContent["fi.mau.will_auto_accept"] = true
- }
- _, err := intent.InviteUser(ctx, roomID, &mautrix.ReqInviteUser{UserID: user.MXID}, extraContent)
- var httpErr mautrix.HTTPError
- if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
- err = user.bridge.StateStore.SetMembership(ctx, roomID, user.MXID, event.MembershipJoin)
- if err != nil {
- log.Warn().Err(err).Msg("Failed to update membership in state store")
- }
- ok = true
- return
- } else if err != nil {
- log.Warn().Err(err).Msg("Failed to invite user to room")
- } else {
- ok = true
- }
-
- if customPuppet != nil && customPuppet.CustomIntent() != nil {
- err = customPuppet.CustomIntent().EnsureJoined(ctx, roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
- if err != nil {
- log.Warn().Err(err).Msg("Failed to auto-join custom puppet")
- ok = false
- } else {
- ok = true
- }
- }
- return
-}
-
-func (user *User) GetSpaceRoom(ctx context.Context) id.RoomID {
- if !user.bridge.Config.Bridge.PersonalFilteringSpaces {
- return ""
- }
-
- if len(user.SpaceRoom) == 0 {
- user.spaceCreateLock.Lock()
- defer user.spaceCreateLock.Unlock()
- if len(user.SpaceRoom) > 0 {
- return user.SpaceRoom
- }
-
- resp, err := user.bridge.Bot.CreateRoom(ctx, &mautrix.ReqCreateRoom{
- Visibility: "private",
- Name: "Signal",
- Topic: "Your Signal bridged chats",
- InitialState: []*event.Event{{
- Type: event.StateRoomAvatar,
- Content: event.Content{
- Parsed: &event.RoomAvatarEventContent{
- URL: user.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
- },
- },
- }},
- CreationContent: map[string]interface{}{
- "type": event.RoomTypeSpace,
- },
- PowerLevelOverride: &event.PowerLevelsEventContent{
- Users: map[id.UserID]int{
- user.bridge.Bot.UserID: 9001,
- user.MXID: 50,
- },
- },
- })
-
- if err != nil {
- user.log.Err(err).Msg("Failed to auto-create space room")
- } else {
- user.SpaceRoom = resp.RoomID
- err = user.Update(context.TODO())
- if err != nil {
- user.log.Err(err).Msg("Failed to save user in database after creating space room")
- }
- user.ensureInvited(ctx, user.bridge.Bot, user.SpaceRoom, false)
- }
- } else if !user.spaceMembershipChecked {
- user.ensureInvited(ctx, user.bridge.Bot, user.SpaceRoom, false)
- }
- user.spaceMembershipChecked = true
-
- return user.SpaceRoom
-}
-
-func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
- doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID)
- if doublePuppet == nil {
- return
- }
- if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(portal.MXID) == 0 {
- return
- }
-
- // TODO: Get chat setting from Signal and sync them here
- //if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate {
- // chat, err := user.Client.Store.ChatSettings.GetChatSettings(portal.Key().ChatID)
- // if err != nil {
- // user.log.Warn().Err(err).Msgf("Failed to get settings of %s", portal.Key().ChatID)
- // return
- // }
- // intent := doublePuppet.CustomIntent()
- // if portal.Key.JID == types.StatusBroadcastJID && justCreated {
- // if user.bridge.Config.Bridge.MuteStatusBroadcast {
- // user.updateChatMute(intent, portal, time.Now().Add(365*24*time.Hour))
- // }
- // if len(user.bridge.Config.Bridge.StatusBroadcastTag) > 0 {
- // user.updateChatTag(intent, portal, user.bridge.Config.Bridge.StatusBroadcastTag, true)
- // }
- // return
- // } else if !chat.Found {
- // return
- // }
- // user.updateChatMute(intent, portal, chat.MutedUntil)
- // user.updateChatTag(intent, portal, user.bridge.Config.Bridge.ArchiveTag, chat.Archived)
- // user.updateChatTag(intent, portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned)
- //}
-}
-
-func (user *User) GetMXID() id.UserID {
- return user.MXID
-}
-func (user *User) GetRemoteID() string {
- return user.SignalID.String()
-}
-func (user *User) GetRemoteName() string {
- return user.SignalUsername
-}
-
-func (user *User) startupTryConnect(retryCount int) {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
-
- // Make sure user has the Signal device populated
- user.populateSignalDevice()
-
- user.log.Debug().Msg("Connecting to Signal")
- ctx := user.log.WithContext(context.Background())
- statusChan, err := user.Client.StartReceiveLoops(ctx)
-
- if err != nil {
- user.log.Error().Err(err).Msg("Error connecting on startup")
- if errors.Is(err, ErrNotLoggedIn) {
- user.log.Warn().Msg("Not logged in, clearing Signal device keys")
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
- user.clearKeysAndDisconnect()
- } else if retryCount < 6 {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
- retryInSeconds := 2 << retryCount
- user.log.Debug().Int("retry_in_seconds", retryInSeconds).Msg("Sleeping and retrying connection")
- time.Sleep(time.Duration(retryInSeconds) * time.Second)
- user.startupTryConnect(retryCount + 1)
- } else {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
- }
- }
-
- if statusChan == nil {
- user.log.Error().Msg("statusChan is nil after Connect")
- return
- }
- // After Connect returns, all bridge states are triggered by events on the statusChan
- go func() {
- var peekedConnectionStatus signalmeow.SignalConnectionStatus
- for {
- var connectionStatus signalmeow.SignalConnectionStatus
- if peekedConnectionStatus.Event != signalmeow.SignalConnectionEventNone {
- user.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 {
- user.log.Debug().Msg("statusChan channel closed")
- return
- }
- }
-
- err := connectionStatus.Err
- switch connectionStatus.Event {
- case signalmeow.SignalConnectionEventConnected:
- user.log.Debug().Msg("Sending Connected BridgeState")
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
- user.bridge.Metrics.TrackConnectionState(user.SignalID, true)
- user.bridge.Metrics.TrackLoginState(user.SignalID, true)
-
- case signalmeow.SignalConnectionEventDisconnected:
- user.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 {
- user.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 {
- user.log.Debug().Msg("Sending TransientDisconnect BridgeState")
- if err == nil {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect})
- } else {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: "unknown-websocket-error", Message: err.Error()})
- }
- user.bridge.Metrics.TrackConnectionState(user.SignalID, false)
- }
- }
-
- case signalmeow.SignalConnectionEventLoggedOut:
- user.log.Debug().Msg("Sending BadCredentials BridgeState")
- if err == nil {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
- } else {
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: err.Error()})
- }
- user.bridge.Metrics.TrackConnectionState(user.SignalID, false)
- user.bridge.Metrics.TrackLoginState(user.SignalID, false)
- user.clearKeysAndDisconnect()
- if managementRoom := user.GetManagementRoomID(); managementRoom != "" {
- _, _ = user.bridge.Bot.SendText(ctx, managementRoom, "You've been logged out of Signal")
- }
-
- case signalmeow.SignalConnectionEventError:
- user.log.Debug().Msg("Sending UnknownError BridgeState")
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: "unknown-websocket-error", Message: err.Error()})
- user.bridge.Metrics.TrackConnectionState(user.SignalID, false)
-
- case signalmeow.SignalConnectionCleanShutdown:
- if user.Client.IsLoggedIn() {
- user.log.Debug().Msg("Clean Shutdown - sending no BridgeState")
- } else {
- user.log.Debug().Msg("Clean Shutdown, but logged out - Sending BadCredentials BridgeState")
- user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
- }
- user.bridge.Metrics.TrackConnectionState(user.SignalID, false)
- }
- }
- }()
-}
-
-func (user *User) clearKeysAndDisconnect() {
- // We need to clear out keys associated with the Signal device that no longer has valid credentials
- user.log.Debug().Msg("Clearing out Signal device keys")
- err := user.Client.ClearKeysAndDisconnect(context.TODO())
- if err != nil {
- user.log.Err(err).Msg("Error clearing device keys")
- }
-}
-
-func (br *SignalBridge) StartUsers() {
- br.ZLog.Debug().Msg("Starting users")
-
- usersWithToken := br.GetAllLoggedInUsers()
- for _, u := range usersWithToken {
- device := u.populateSignalDevice()
- if device == nil || !device.IsLoggedIn() {
- br.ZLog.Warn().Stringer("user_id", u.MXID).Msg("No device found for user, skipping Connect and sending BadCredentials BridgeState")
- u.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"})
- continue
- }
- go u.Connect()
- }
- if len(usersWithToken) == 0 {
- br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
- }
-
- br.ZLog.Debug().Msg("Starting custom puppets")
- for _, customPuppet := range br.GetAllPuppetsWithCustomMXID() {
- go func(puppet *Puppet) {
- br.ZLog.Debug().Stringer("user_id", puppet.CustomMXID).Msg("Starting custom puppet")
-
- if err := puppet.StartCustomMXID(true); err != nil {
- puppet.log.Error().Err(err).Msg("Failed to start custom puppet")
- }
- }(customPuppet)
- }
-}
-
-func (user *User) Login() (<-chan signalmeow.ProvisioningResponse, error) {
- user.Lock()
- defer user.Unlock()
-
- provChan := signalmeow.PerformProvisioning(context.TODO(), user.bridge.MeowStore, user.bridge.Config.Signal.DeviceName)
-
- return provChan, nil
-}
-
-func (user *User) Connect() {
- user.startupTryConnect(0)
-}
-
-func (user *User) saveSignalID(ctx context.Context, id uuid.UUID, number string) {
- user.bridge.usersLock.Lock()
- defer user.bridge.usersLock.Unlock()
- if user.SignalID == id && user.SignalUsername == number {
- return
- }
- if user.SignalID != id {
- existingUser := user.bridge.unlockedGetUserBySignalID(id)
- if existingUser != nil {
- // TODO this doesn't clear the signal store properly
- // the store also only has the uuid as primary key, even though it should have uuid + device id
- zerolog.Ctx(ctx).Warn().
- Stringer("previous_user", existingUser.MXID).
- Stringer("signal_uuid", id).
- Msg("Another user is already logged in with same UUID, logging out previous user")
- existingUser.bridge.Metrics.TrackLoginState(user.SignalID, false)
- _ = existingUser.Disconnect()
- existingUser.SignalID = uuid.Nil
- existingUser.SignalUsername = ""
- err := existingUser.Update(ctx)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to clear previous user's signal UUID")
- }
- }
- }
- user.SignalID = id
- user.SignalUsername = number
- user.bridge.usersBySignalID[id] = user
- err := user.Update(ctx)
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to save user's signal UUID")
- }
-}
-
-func (user *User) populateSignalDevice() *signalmeow.Client {
- user.Lock()
- defer user.Unlock()
- log := user.log.With().
- Str("action", "populate signal device").
- Stringer("signal_id", user.SignalID).
- Logger()
-
- if user.SignalID == uuid.Nil {
- return nil
- }
- // TODO clear client on logout properly so that populating can skip creating if it already exists
- /*else if user.Client != nil {
- return user.Client
- }*/
-
- device, err := user.bridge.MeowStore.DeviceByACI(context.TODO(), user.SignalID)
- if err != nil {
- log.Err(err).Msg("Failed to get device from database")
- return nil
- } else if device == nil {
- log.Err(ErrNotLoggedIn).Msg("No device found for user")
- return nil
- }
-
- user.Client = &signalmeow.Client{
- Store: device,
- EventHandler: user.eventHandler,
- }
- go user.tryAutomaticDoublePuppeting()
- return user.Client
-}
-
-func (user *User) handleReceipt(evt *events.Receipt) {
- log := user.log.With().
- Str("action", "handle receipt").
- Stringer("receipt_type", evt.Content.GetType()).
- Stringer("sender_uuid", evt.Sender).
- Logger()
- ctx := log.WithContext(context.TODO())
- messages, err := user.bridge.DB.Message.GetManyBySignalID(ctx, user.SignalID, evt.Content.GetTimestamp(), user.SignalID, false)
- if err != nil {
- log.Err(err).Msg("Failed to get receipt target messages from database")
- return
- }
- sender := user.bridge.GetPuppetBySignalID(evt.Sender)
- missingMessageIDMap := make(map[uint64]struct{}, len(evt.Content.GetTimestamp()))
- for _, msg := range evt.Content.GetTimestamp() {
- missingMessageIDMap[msg] = struct{}{}
- }
- foundMessageIDs := make([]uint64, len(messages))
- switch evt.Content.GetType() {
- case signalpb.ReceiptMessage_READ:
- messageMap := make(map[string]*database.Message)
- for i, msg := range messages {
- foundMessageIDs[i] = msg.Timestamp
- delete(missingMessageIDMap, msg.Timestamp)
- // The database returns messages from newest to oldest, so only include the first message per chat
- _, ok := messageMap[msg.SignalChatID]
- if !ok {
- messageMap[msg.SignalChatID] = msg
- }
- }
- log.Debug().
- Uints64("found_message_ids", foundMessageIDs).
- Uints64("not_found_message_ids", maps.Keys(missingMessageIDMap)).
- Int("chat_count", len(messageMap)).
- Msg("Received read receipt")
- for _, msg := range messageMap {
- portal := user.GetPortalByChatID(msg.SignalChatID)
- if portal == nil {
- continue
- }
- err = portal.SendReadReceipt(ctx, sender, msg)
- if err != nil {
- log.Err(err).Msg("Failed to send read receipt")
- }
- }
- case signalpb.ReceiptMessage_DELIVERY:
- messageMap := make(map[string][]*database.Message)
- for i, msg := range messages {
- foundMessageIDs[i] = msg.Timestamp
- delete(missingMessageIDMap, msg.Timestamp)
- messageMap[msg.SignalChatID] = append(messageMap[msg.SignalChatID], msg)
- }
- log.Debug().
- Uints64("found_message_ids", foundMessageIDs).
- Uints64("not_found_message_ids", maps.Keys(missingMessageIDMap)).
- Int("chat_count", len(messageMap)).
- Msg("Received delivery receipt")
- for _, msgs := range messageMap {
- portal := user.GetPortalByChatID(msgs[0].SignalChatID)
- if portal == nil {
- continue
- }
- // There should always only be 1 part, but use the last part to be safe
- portal.MarkDelivered(ctx, msgs[len(msgs)-1])
- }
- }
-}
-
-func (user *User) handleReadSelf(evt *events.ReadSelf) {
- ctx := context.TODO()
- messagesByChat := map[string]*database.Message{}
- for _, part := range evt.Messages {
- log := user.log.With().
- Str("action", "handle read self").
- Str("sender_uuid", part.GetSenderAci()).
- Uint64("msg_timestamp", part.GetTimestamp()).
- Logger()
- ctx := log.WithContext(context.TODO())
- if senderUUID, err := uuid.Parse(part.GetSenderAci()); err != nil {
- log.Err(err).Msg("Failed to parse sender UUID")
- } else if msg, err := user.bridge.DB.Message.GetLastPartBySignalIDWithUnknownReceiver(ctx, senderUUID, part.GetTimestamp(), user.SignalID); err != nil {
- log.Err(err).Msg("Failed to get message from database")
- } else if msg == nil {
- log.Warn().Msg("Message not found in database")
- } else if existingMsg, ok := messagesByChat[msg.SignalChatID]; ok && existingMsg.Timestamp > msg.Timestamp {
- log.Trace().
- Str("chat_id", msg.SignalChatID).
- Uint64("newer_msg", existingMsg.Timestamp).
- Msg("Receipt event contains a newer message, skipping this one")
- } else {
- log.Trace().Str("chat_id", msg.SignalChatID).Msg("Received own read receipt")
- messagesByChat[msg.SignalChatID] = msg
- }
- }
- puppet := user.bridge.GetPuppetBySignalID(user.SignalID)
- for _, msg := range messagesByChat {
- portal := user.GetPortalByChatID(msg.SignalChatID)
- if portal == nil {
- continue
- }
- user.log.Debug().
- Str("action", "handle read self").
- Str("chat_id", msg.SignalChatID).
- Uint64("msg_timestamp", msg.Timestamp).
- Stringer("msg_mxid", msg.MXID).
- Msg("Bridging own read receipt")
- portal.ScheduleDisappearing()
- user.SetLastReadTS(ctx, portal.PortalKey, msg.Timestamp)
- err := portal.SendReadReceipt(ctx, puppet, msg)
- if err != nil {
- user.log.Err(err).Stringer("mxid", msg.MXID).Msg("Failed to send read receipt")
- }
- }
-}
-
-func (user *User) handleContactList(evt *events.ContactList) {
- ctx := user.log.With().Str("action", "handle contact list").Logger().WithContext(context.TODO())
- for _, contact := range evt.Contacts {
- if contact.ACI == uuid.Nil {
- continue
- }
- puppet := user.bridge.GetPuppetBySignalID(contact.ACI)
- if puppet == nil {
- continue
- }
- puppet.UpdateInfo(ctx, user, &contact.ContactAvatar)
- }
-}
-
-func (user *User) handleACIFound(evt *events.ACIFound) {
- log := user.log.With().
- Str("action", "handle aci found").
- Stringer("aci", evt.ACI.UUID).
- Stringer("pni", evt.PNI.UUID).
- Logger()
- log.Debug().Msg("Handling ACI found event")
- defer func() {
- log.Debug().Msg("Finished handling ACI found event")
- }()
- ctx := log.WithContext(context.TODO())
- user.bridge.portalsLock.Lock()
- defer user.bridge.portalsLock.Unlock()
- pniPortal := user.bridge.unlockedGetPortalByChatID(database.PortalKey{
- ChatID: evt.PNI.String(),
- Receiver: user.SignalID,
- }, false)
- if pniPortal == nil {
- log.Debug().Msg("PNI portal doesn't exist, ignoring event")
- return
- }
- pniPortal.roomCreateLock.Lock()
- defer pniPortal.roomCreateLock.Unlock()
- if pniPortal.MXID == "" {
- log.Info().Msg("PNI portal doesn't have Matrix room, deleting row")
- pniPortal.unlockedDelete()
- return
- }
- log.UpdateContext(func(c zerolog.Context) zerolog.Context {
- return c.Stringer("pni_portal_mxid", pniPortal.MXID)
- })
- aciPortal := user.bridge.unlockedGetPortalByChatID(database.PortalKey{
- ChatID: evt.ACI.String(),
- Receiver: user.SignalID,
- }, false)
- if aciPortal == nil {
- log.Info().Msg("ACI portal doesn't exist, re-ID'ing PNI portal")
- err := pniPortal.unlockedReID(ctx, evt.ACI.String())
- if err != nil {
- log.Err(err).Msg("Failed to re-ID PNI portal")
- } else {
- go pniPortal.PostReIDUpdate(ctx, user)
- }
- return
- }
- aciPortal.roomCreateLock.Lock()
- defer aciPortal.roomCreateLock.Unlock()
- if aciPortal.MXID == "" {
- log.Info().Msg("ACI portal row exists, but doesn't have a Matrix room. Deleting ACI portal row and re-ID'ing PNI portal")
- aciPortal.unlockedDelete()
- err := pniPortal.unlockedReID(ctx, evt.ACI.String())
- if err != nil {
- log.Err(err).Msg("Failed to re-ID PNI portal")
- } else {
- go pniPortal.PostReIDUpdate(ctx, user)
- }
- } else {
- log.UpdateContext(func(c zerolog.Context) zerolog.Context {
- return c.Stringer("aci_portal_mxid", aciPortal.MXID)
- })
- log.Info().Msg("Both ACI and PNI portal have Matrix room, tombstoning PNI portal")
- pniPortal.unlockedDelete()
- go func() {
- _, err := pniPortal.MainIntent().SendStateEvent(ctx, pniPortal.MXID, event.StateTombstone, "", &event.TombstoneEventContent{
- Body: fmt.Sprintf("This room has been merged"),
- ReplacementRoom: aciPortal.MXID,
- })
- if err != nil {
- log.Err(err).Msg("Failed to send tombstone to PNI portal")
- }
- pniPortal.Cleanup(ctx, err == nil)
- }()
- }
-}
-
-func (portal *Portal) unlockedReID(ctx context.Context, newID string) error {
- err := portal.Portal.ReID(ctx, newID)
- if err != nil {
- return err
- }
- delete(portal.bridge.portalsByID, portal.PortalKey)
- portal.PortalKey.ChatID = newID
- portal.bridge.portalsByID[portal.PortalKey] = portal
- err = portal.MainIntent().EnsureJoined(ctx, portal.MXID, appservice.EnsureJoinedParams{IgnoreCache: true})
- if err != nil {
- return fmt.Errorf("failed to ensure ghost is joined to portal: %w", err)
- }
- return nil
-}
-
-func (user *User) eventHandler(rawEvt events.SignalEvent) {
- switch evt := rawEvt.(type) {
- case *events.ChatEvent:
- portal := user.GetPortalByChatID(evt.Info.ChatID)
- if portal != nil {
- portal.signalMessages <- portalSignalMessage{user: user, evt: evt}
- } else {
- user.log.Warn().Str("chat_id", evt.Info.ChatID).Msg("Couldn't get portal, dropping message")
- }
- case *events.DecryptionError:
- portal := user.GetPortalByChatID(evt.Sender.String())
- if portal == nil {
- user.log.Warn().Stringer("chat_id", evt.Sender).Msg("Couldn't get portal for decryption error")
- return
- }
- content := &event.MessageEventContent{MsgType: event.MsgNotice}
- name := user.bridge.GetPuppetBySignalID(evt.Sender).Name
- if name == "" {
- name = "This user"
- }
- content.Body = fmt.Sprintf("%s sent a message that couldn't be decrypted. It may have been in this chat or a group chat. Please check your Signal app", name)
- portal.sendMainIntentMessage(context.TODO(), content)
- case *events.Receipt:
- user.handleReceipt(evt)
- case *events.ReadSelf:
- user.handleReadSelf(evt)
- case *events.Call:
- portal := user.GetPortalByChatID(evt.Info.ChatID)
- content := &event.MessageEventContent{MsgType: event.MsgNotice}
- if evt.IsRinging {
- content.Body = "Incoming call"
- if portal.IsPrivateChat() {
- content.MsgType = event.MsgText
- }
- } else {
- content.Body = "Call ended"
- }
- portal.sendMainIntentMessage(context.TODO(), content)
- case *events.ContactList:
- user.handleContactList(evt)
- case *events.ACIFound:
- user.handleACIFound(evt)
- default:
- user.log.Warn().Type("event_type", evt).Msg("Unrecognized event type from signalmeow")
- }
-}
-
-func (user *User) GetPortalByChatID(signalID string) *Portal {
- return user.bridge.GetPortalByChatID(database.PortalKey{
- ChatID: signalID,
- Receiver: user.SignalID,
- })
-}
-
-func (user *User) GetPortalByChatIDIfExists(signalID string) *Portal {
- return user.bridge.GetPortalByChatIDIfExists(database.PortalKey{
- ChatID: signalID,
- Receiver: user.SignalID,
- })
-}
-
-func (user *User) disconnectNoLock() (*signalmeow.Client, error) {
- if user.Client == nil {
- return nil, ErrNotConnected
- }
-
- disconnectedDevice := user.Client
- err := user.Client.StopReceiveLoops()
- user.Client = nil
- return disconnectedDevice, err
-}
-
-func (user *User) Disconnect() error {
- user.Lock()
- defer user.Unlock()
- user.log.Info().Msg("Disconnecting session manually")
- _, err := user.disconnectNoLock()
- return err
-}
-
-func (user *User) Logout() error {
- user.Lock()
- defer user.Unlock()
- user.log.Info().Msg("Logging out of session")
- loggedOutDevice, err := user.disconnectNoLock()
- user.bridge.MeowStore.DeleteDevice(context.TODO(), &loggedOutDevice.Store.DeviceData)
- user.bridge.GetPuppetByCustomMXID(user.MXID).ClearCustomMXID()
- user.bridge.Metrics.TrackLoginState(user.SignalID, false)
- return err
-}
-
-func (user *User) UpdateDirectChats(ctx context.Context, chats map[id.UserID][]id.RoomID) {
- if !user.bridge.Config.Bridge.SyncDirectChatList {
- return
- }
-
- puppet := user.bridge.GetPuppetByMXID(user.MXID)
- if puppet == nil {
- return
- }
-
- intent := puppet.CustomIntent()
- if intent == nil {
- return
- }
-
- method := http.MethodPatch
- if chats == nil {
- chats = user.getDirectChats()
- method = http.MethodPut
- }
-
- user.log.Debug().Msg("Updating m.direct list on homeserver")
-
- var err error
- if user.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareAsmux {
- urlPath := intent.BuildURL(mautrix.ClientURLPath{"unstable", "com.beeper.asmux", "dms"})
- _, err = intent.MakeFullRequest(ctx, mautrix.FullRequest{
- Method: method,
- URL: urlPath,
- Headers: http.Header{"X-Asmux-Auth": {user.bridge.AS.Registration.AppToken}},
- RequestJSON: chats,
- })
- } else {
- existingChats := map[id.UserID][]id.RoomID{}
-
- err = intent.GetAccountData(ctx, event.AccountDataDirectChats.Type, &existingChats)
- if err != nil {
- user.log.Warn().Err(err).Msg("Failed to get m.direct event to update it")
- return
- }
-
- for userID, rooms := range existingChats {
- if _, ok := user.bridge.ParsePuppetMXID(userID); !ok {
- // This is not a ghost user, include it in the new list
- chats[userID] = rooms
- } else if _, ok := chats[userID]; !ok && method == http.MethodPatch {
- // This is a ghost user, but we're not replacing the whole list, so include it too
- chats[userID] = rooms
- }
- }
-
- err = intent.SetAccountData(ctx, event.AccountDataDirectChats.Type, &chats)
- }
-
- if err != nil {
- user.log.Warn().Err(err).Msg("Failed to update m.direct event")
- }
-}
-
-func (user *User) getDirectChats() map[id.UserID][]id.RoomID {
- chats := map[id.UserID][]id.RoomID{}
-
- privateChats, err := user.bridge.DB.Portal.FindPrivateChatsOf(context.TODO(), user.SignalID)
- if err != nil {
- user.log.Err(err).Msg("Failed to get private chats")
- return chats
- }
- for _, portal := range privateChats {
- portalUserID := portal.UserID()
- if portal.MXID != "" && portalUserID.Type == libsignalgo.ServiceIDTypeACI {
- puppetMXID := user.bridge.FormatPuppetMXID(portalUserID.UUID)
-
- chats[puppetMXID] = []id.RoomID{portal.MXID}
- }
- }
-
- return chats
-}