mirror of
https://github.com/mautrix/signal.git
synced 2025-03-14 14:15:36 +00:00
all: delete legacy bridge
This commit is contained in:
parent
c246473b52
commit
0c9f2c19d2
75 changed files with 562 additions and 10737 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,7 +6,5 @@
|
||||||
*.log*
|
*.log*
|
||||||
|
|
||||||
/mautrix-signal
|
/mautrix-signal
|
||||||
/mautrix-signalgo
|
|
||||||
/mautrix-signal-v2
|
|
||||||
/start
|
/start
|
||||||
/libsignal_ffi.a
|
/libsignal_ffi.a
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
include:
|
include:
|
||||||
- project: 'mautrix/ci'
|
|
||||||
file: '/go.yml'
|
|
||||||
- project: 'mautrix/ci'
|
- project: 'mautrix/ci'
|
||||||
file: '/gov2.yml'
|
file: '/gov2.yml'
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
BUILDER_IMAGE: dock.mau.dev/tulir/gomuks-build-docker/signal
|
BUILDER_IMAGE: dock.mau.dev/tulir/gomuks-build-docker/signal
|
||||||
|
BINARY_NAME_V2: mautrix-signal
|
||||||
|
|
||||||
# 32-bit arm builds aren't supported
|
# 32-bit arm builds aren't supported
|
||||||
build arm:
|
|
||||||
rules:
|
|
||||||
- when: never
|
|
||||||
|
|
||||||
build arm v2:
|
build arm v2:
|
||||||
rules:
|
rules:
|
||||||
- when: never
|
- when: never
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude_types: [markdown]
|
exclude_types: [markdown]
|
||||||
|
@ -18,11 +18,11 @@ repos:
|
||||||
- "go.mau.fi/mautrix-signal"
|
- "go.mau.fi/mautrix-signal"
|
||||||
- "-w"
|
- "-w"
|
||||||
- id: go-vet-mod
|
- id: go-vet-mod
|
||||||
#- id: go-staticcheck-repo-mod
|
# - id: go-staticcheck-repo-mod
|
||||||
# TODO: reenable this and fix all the problems
|
# TODO: reenable this and fix all the problems
|
||||||
|
|
||||||
- repo: https://github.com/beeper/pre-commit-go
|
- repo: https://github.com/beeper/pre-commit-go
|
||||||
rev: v0.3.0
|
rev: v0.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: zerolog-ban-msgf
|
- id: zerolog-ban-msgf
|
||||||
- id: zerolog-use-stringer
|
- id: zerolog-use-stringer
|
||||||
|
|
|
@ -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)
|
# v0.6.3 (2024-07-16)
|
||||||
|
|
||||||
* Updated to libsignal v0.52.0.
|
* Updated to libsignal v0.52.0.
|
||||||
|
|
|
@ -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"]
|
|
28
Makefile
28
Makefile
|
@ -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)
|
|
14
ROADMAP.md
14
ROADMAP.md
|
@ -1,17 +1,17 @@
|
||||||
# Features & roadmap
|
# Features & roadmap
|
||||||
|
|
||||||
* Matrix → Signal
|
* Matrix → Signal
|
||||||
* [ ] Message content
|
* [x] Message content
|
||||||
* [x] Text
|
* [x] Text
|
||||||
* [x] Formatting
|
* [x] Formatting
|
||||||
* [x] Mentions
|
* [x] Mentions
|
||||||
* [ ] Media
|
* [x] Media
|
||||||
* [x] Images
|
* [x] Images
|
||||||
* [x] Audio files
|
* [x] Audio files
|
||||||
* [x] Voice messages
|
* [x] Voice messages
|
||||||
* [x] Files
|
* [x] Files
|
||||||
* [x] Gifs
|
* [x] Gifs
|
||||||
* [ ] Locations
|
* [x] Locations
|
||||||
* [x] Stickers
|
* [x] Stickers
|
||||||
* [x] Message edits
|
* [x] Message edits
|
||||||
* [x] Message reactions
|
* [x] Message reactions
|
||||||
|
@ -22,9 +22,9 @@
|
||||||
* [x] Topic
|
* [x] Topic
|
||||||
* [ ] Membership actions
|
* [ ] Membership actions
|
||||||
* [ ] Join (accepting invites)
|
* [ ] Join (accepting invites)
|
||||||
* [x] Invite
|
* [ ] Invite
|
||||||
* [x] Leave
|
* [ ] Leave
|
||||||
* [x] Kick/Ban/Unban
|
* [ ] Kick/Ban/Unban
|
||||||
* [x] Group permissions
|
* [x] Group permissions
|
||||||
* [x] Typing notifications
|
* [x] Typing notifications
|
||||||
* [x] Read receipts
|
* [x] Read receipts
|
||||||
|
@ -70,5 +70,5 @@
|
||||||
* [x] When receiving message
|
* [x] When receiving message
|
||||||
* [x] Linking as secondary device
|
* [x] Linking as secondary device
|
||||||
* [ ] Registering as primary device
|
* [ ] Registering as primary device
|
||||||
* [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
|
* [x] Option to use own Matrix account for messages sent from other Signal clients
|
||||||
|
|
|
@ -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 "$@"
|
|
|
@ -1,9 +1,9 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
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
|
if [ "$DBG" = 1 ]; then
|
||||||
GO_GCFLAGS='all=-N -l'
|
GO_GCFLAGS='all=-N -l'
|
||||||
else
|
else
|
||||||
GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
|
GO_LDFLAGS="-s -w ${GO_LDFLAGS}"
|
||||||
fi
|
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 "$@"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#./build-rust.sh
|
./build-rust.sh
|
||||||
#cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
|
cp -f pkg/libsignalgo/libsignal/target/release/libsignal_ffi.a .
|
||||||
LIBRARY_PATH=.:$LIBRARY_PATH ./build-go-v2.sh
|
LIBRARY_PATH=.:$LIBRARY_PATH ./build-go.sh
|
||||||
|
|
4
build.sh
4
build.sh
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
git submodule init
|
|
||||||
git submodule update
|
|
||||||
make
|
|
|
@ -28,8 +28,8 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/legacyprovision"
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/connector"
|
"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)
|
user := m.Matrix.Provisioning.GetUser(r)
|
||||||
defLogin := user.GetDefaultLogin()
|
defLogin := user.GetDefaultLogin()
|
||||||
if defLogin != nil && defLogin.Client != nil && defLogin.Client.IsLoggedIn() {
|
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",
|
Error: "Already logged in",
|
||||||
ErrCode: "FI.MAU.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")
|
login, err := m.Connector.CreateLogin(r.Context(), user, "qr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to create login")
|
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",
|
Error: "Internal error starting login",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
|
@ -73,14 +73,14 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
|
||||||
firstStep, err := login.Start(r.Context())
|
firstStep, err := login.Start(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to start login")
|
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",
|
Error: "Internal error starting login",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if firstStep.StepID != connector.LoginStepQR || firstStep.Type != bridgev2.LoginStepTypeDisplayAndWait || firstStep.DisplayAndWaitParams.Type != bridgev2.LoginDisplayTypeQR {
|
} 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")
|
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",
|
Error: "Unexpected first login step",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
|
@ -93,7 +93,7 @@ func legacyProvLinkNew(w http.ResponseWriter, r *http.Request) {
|
||||||
User: user,
|
User: user,
|
||||||
}
|
}
|
||||||
loginSessionsLock.Unlock()
|
loginSessionsLock.Unlock()
|
||||||
legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
|
JSONResponse(w, http.StatusOK, Response{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: "provisioning_url_received",
|
Status: "provisioning_url_received",
|
||||||
SessionID: strconv.Itoa(int(handleID)),
|
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 {
|
func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess {
|
||||||
var body legacyprovision.LinkWaitForAccountRequest
|
var body LinkWaitForAccountRequest
|
||||||
err := json.NewDecoder(r.Body).Decode(&body)
|
err := json.NewDecoder(r.Body).Decode(&body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
|
JSONResponse(w, http.StatusBadRequest, Error{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "Error decoding JSON body",
|
Error: "Error decoding JSON body",
|
||||||
ErrCode: mautrix.MBadJSON.ErrCode,
|
ErrCode: mautrix.MBadJSON.ErrCode,
|
||||||
|
@ -114,7 +114,7 @@ func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess
|
||||||
}
|
}
|
||||||
sessionID, err := strconv.Atoi(body.SessionID)
|
sessionID, err := strconv.Atoi(body.SessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
legacyprovision.JSONResponse(w, http.StatusBadRequest, legacyprovision.Error{
|
JSONResponse(w, http.StatusBadRequest, Error{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "Error decoding session ID in JSON body",
|
Error: "Error decoding session ID in JSON body",
|
||||||
ErrCode: mautrix.MBadJSON.ErrCode,
|
ErrCode: mautrix.MBadJSON.ErrCode,
|
||||||
|
@ -124,7 +124,7 @@ func getLoginProcess(w http.ResponseWriter, r *http.Request) *legacyLoginProcess
|
||||||
process, ok := loginSessions[uint32(sessionID)]
|
process, ok := loginSessions[uint32(sessionID)]
|
||||||
user := m.Matrix.Provisioning.GetUser(r)
|
user := m.Matrix.Provisioning.GetUser(r)
|
||||||
if !ok || process.User != user {
|
if !ok || process.User != user {
|
||||||
legacyprovision.JSONResponse(w, http.StatusNotFound, legacyprovision.Error{
|
JSONResponse(w, http.StatusNotFound, Error{
|
||||||
Success: false,
|
Success: false,
|
||||||
Error: "No session found",
|
Error: "No session found",
|
||||||
ErrCode: mautrix.MNotFound.ErrCode,
|
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())
|
res, err := login.Login.(bridgev2.LoginProcessDisplayAndWait).Wait(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to log in")
|
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",
|
Error: "Failed to log in",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
|
@ -150,14 +150,14 @@ func legacyProvLinkWaitScan(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
} else if res.StepID != connector.LoginStepProcess {
|
} else if res.StepID != connector.LoginStepProcess {
|
||||||
zerolog.Ctx(r.Context()).Error().Any("first_step", res).Msg("Unexpected login step")
|
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",
|
Error: "Unexpected login step",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
login.Delete()
|
login.Delete()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
|
JSONResponse(w, http.StatusOK, Response{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: "provisioning_data_received",
|
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())
|
res, err := login.Login.(bridgev2.LoginProcessDisplayAndWait).Wait(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to log in")
|
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",
|
Error: "Failed to log in",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
} else if res.StepID != connector.LoginStepComplete || res.Type != bridgev2.LoginStepTypeComplete {
|
} else if res.StepID != connector.LoginStepComplete || res.Type != bridgev2.LoginStepTypeComplete {
|
||||||
zerolog.Ctx(r.Context()).Error().Any("first_step", res).Msg("Unexpected login step")
|
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",
|
Error: "Unexpected login step",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
legacyprovision.JSONResponse(w, http.StatusOK, legacyprovision.Response{
|
JSONResponse(w, http.StatusOK, Response{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: "prekeys_registered",
|
Status: "prekeys_registered",
|
||||||
UUID: string(res.CompleteParams.UserLogin.ID),
|
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) {
|
func legacyProvLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
// No-op for backwards compatibility
|
// 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) {
|
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)
|
resp, err := api.ResolveIdentifier(r.Context(), mux.Vars(r)["phonenum"], create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to resolve identifier")
|
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),
|
Error: fmt.Sprintf("Failed to resolve identifier: %v", err),
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
} else if resp == nil {
|
} else if resp == nil {
|
||||||
legacyprovision.JSONResponse(w, http.StatusNotFound, &legacyprovision.Error{
|
JSONResponse(w, http.StatusNotFound, &Error{
|
||||||
ErrCode: mautrix.MNotFound.ErrCode,
|
ErrCode: mautrix.MNotFound.ErrCode,
|
||||||
Error: "User not found on Signal",
|
Error: "User not found on Signal",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status := http.StatusOK
|
status := http.StatusOK
|
||||||
apiResp := &legacyprovision.ResolveIdentifierResponse{
|
apiResp := &ResolveIdentifierResponse{
|
||||||
ChatID: legacyprovision.ResolveIdentifierResponseChatID{
|
ChatID: ResolveIdentifierResponseChatID{
|
||||||
UUID: string(resp.UserID),
|
UUID: string(resp.UserID),
|
||||||
Number: "",
|
Number: "",
|
||||||
},
|
},
|
||||||
|
@ -229,7 +229,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
|
||||||
if resp.UserInfo != nil {
|
if resp.UserInfo != nil {
|
||||||
resp.Ghost.UpdateInfo(r.Context(), resp.UserInfo)
|
resp.Ghost.UpdateInfo(r.Context(), resp.UserInfo)
|
||||||
}
|
}
|
||||||
apiResp.OtherUser = &legacyprovision.ResolveIdentifierResponseOtherUser{
|
apiResp.OtherUser = &ResolveIdentifierResponseOtherUser{
|
||||||
MXID: resp.Ghost.Intent.GetMXID(),
|
MXID: resp.Ghost.Intent.GetMXID(),
|
||||||
DisplayName: resp.Ghost.Name,
|
DisplayName: resp.Ghost.Name,
|
||||||
AvatarURL: resp.Ghost.AvatarMXC.ParseOrIgnore(),
|
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)
|
resp.Chat.Portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to get portal")
|
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",
|
Err: "Failed to get portal",
|
||||||
ErrCode: "M_UNKNOWN",
|
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)
|
err = resp.Chat.Portal.CreateMatrixRoom(r.Context(), login, resp.Chat.PortalInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to create portal room")
|
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",
|
Err: "Failed to create portal room",
|
||||||
ErrCode: "M_UNKNOWN",
|
ErrCode: "M_UNKNOWN",
|
||||||
})
|
})
|
||||||
|
@ -262,7 +262,7 @@ func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request,
|
||||||
}
|
}
|
||||||
apiResp.RoomID = resp.Chat.Portal.MXID
|
apiResp.RoomID = resp.Chat.Portal.MXID
|
||||||
}
|
}
|
||||||
legacyprovision.JSONResponse(w, status, &legacyprovision.Response{
|
JSONResponse(w, status, &Response{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
ResolveIdentifierResponse: apiResp,
|
ResolveIdentifierResponse: apiResp,
|
||||||
|
@ -276,3 +276,71 @@ func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
|
||||||
func legacyProvPM(w http.ResponseWriter, r *http.Request) {
|
func legacyProvPM(w http.ResponseWriter, r *http.Request) {
|
||||||
legacyResolveIdentifierOrStartChat(w, r, true)
|
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
|
||||||
|
}
|
1161
commands.go
1161
commands.go
File diff suppressed because it is too large
Load diff
241
config/bridge.go
241
config/bridge.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BridgeConfig struct {
|
|
||||||
UsernameTemplate string `yaml:"username_template"`
|
|
||||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
|
||||||
PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"`
|
|
||||||
UseContactAvatars bool `yaml:"use_contact_avatars"`
|
|
||||||
UseOutdatedProfiles bool `yaml:"use_outdated_profiles"`
|
|
||||||
NumberInTopic bool `yaml:"number_in_topic"`
|
|
||||||
|
|
||||||
NoteToSelfAvatar id.ContentURIString `yaml:"note_to_self_avatar"`
|
|
||||||
|
|
||||||
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
|
||||||
|
|
||||||
PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"`
|
|
||||||
BridgeNotices bool `yaml:"bridge_notices"`
|
|
||||||
DeliveryReceipts bool `yaml:"delivery_receipts"`
|
|
||||||
MessageStatusEvents bool `yaml:"message_status_events"`
|
|
||||||
MessageErrorNotices bool `yaml:"message_error_notices"`
|
|
||||||
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
|
|
||||||
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
|
|
||||||
PublicPortals bool `yaml:"public_portals"`
|
|
||||||
CaptionInMessage bool `yaml:"caption_in_message"`
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
up "go.mau.fi/util/configupgrade"
|
|
||||||
"go.mau.fi/util/random"
|
|
||||||
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DoUpgrade(helper up.Helper) {
|
|
||||||
bridgeconfig.Upgrader.DoUpgrade(helper)
|
|
||||||
|
|
||||||
legacyDB, ok := helper.Get(up.Str, "appservice", "database")
|
|
||||||
if ok {
|
|
||||||
if strings.HasPrefix(legacyDB, "postgres") {
|
|
||||||
parsedDB, err := url.Parse(legacyDB)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
q := parsedDB.Query()
|
|
||||||
if parsedDB.Host == "" && !q.Has("host") {
|
|
||||||
q.Set("host", "/var/run/postgresql")
|
|
||||||
} else if !q.Has("sslmode") {
|
|
||||||
q.Set("sslmode", "disable")
|
|
||||||
}
|
|
||||||
parsedDB.RawQuery = q.Encode()
|
|
||||||
helper.Set(up.Str, parsedDB.String(), "appservice", "database", "uri")
|
|
||||||
helper.Set(up.Str, "postgres", "appservice", "database", "type")
|
|
||||||
} else {
|
|
||||||
dbPath := strings.TrimPrefix(strings.TrimPrefix(legacyDB, "sqlite:"), "///")
|
|
||||||
helper.Set(up.Str, dbPath, "appservice", "database", "uri")
|
|
||||||
helper.Set(up.Str, "sqlite3-fk-wal", "appservice", "database", "type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if legacyDBMinSize, ok := helper.Get(up.Int, "appservice", "database_opts", "min_size"); ok {
|
|
||||||
helper.Set(up.Int, legacyDBMinSize, "appservice", "database", "max_idle_conns")
|
|
||||||
}
|
|
||||||
if legacyDBMaxSize, ok := helper.Get(up.Int, "appservice", "database_opts", "max_size"); ok {
|
|
||||||
helper.Set(up.Int, legacyDBMaxSize, "appservice", "database", "max_open_conns")
|
|
||||||
}
|
|
||||||
if legacyBotUsername, ok := helper.Get(up.Str, "appservice", "bot_username"); ok {
|
|
||||||
helper.Set(up.Str, legacyBotUsername, "appservice", "bot", "username")
|
|
||||||
}
|
|
||||||
if legacyBotDisplayname, ok := helper.Get(up.Str, "appservice", "bot_displayname"); ok {
|
|
||||||
helper.Set(up.Str, legacyBotDisplayname, "appservice", "bot", "displayname")
|
|
||||||
}
|
|
||||||
if legacyBotAvatar, ok := helper.Get(up.Str, "appservice", "bot_avatar"); ok {
|
|
||||||
helper.Set(up.Str, legacyBotAvatar, "appservice", "bot", "avatar")
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.Copy(up.Bool, "metrics", "enabled")
|
|
||||||
helper.Copy(up.Str, "metrics", "listen")
|
|
||||||
|
|
||||||
helper.Copy(up.Str, "signal", "device_name")
|
|
||||||
|
|
||||||
if usernameTemplate, ok := helper.Get(up.Str, "bridge", "username_template"); ok && strings.Contains(usernameTemplate, "{userid}") {
|
|
||||||
helper.Set(up.Str, strings.ReplaceAll(usernameTemplate, "{userid}", "{{.}}"), "bridge", "username_template")
|
|
||||||
} else {
|
|
||||||
helper.Copy(up.Str, "bridge", "username_template")
|
|
||||||
}
|
|
||||||
if displaynameTemplate, ok := helper.Get(up.Str, "bridge", "displayname_template"); ok && strings.Contains(displaynameTemplate, "{displayname}") {
|
|
||||||
helper.Set(up.Str, strings.ReplaceAll(displaynameTemplate, "{displayname}", `{{or .ProfileName .PhoneNumber "Unknown user"}}`), "bridge", "displayname_template")
|
|
||||||
} else {
|
|
||||||
helper.Copy(up.Str, "bridge", "displayname_template")
|
|
||||||
}
|
|
||||||
helper.Copy(up.Str, "bridge", "private_chat_portal_meta")
|
|
||||||
helper.Copy(up.Bool, "bridge", "use_contact_avatars")
|
|
||||||
helper.Copy(up.Bool, "bridge", "use_outdated_profiles")
|
|
||||||
helper.Copy(up.Bool, "bridge", "number_in_topic")
|
|
||||||
helper.Copy(up.Str, "bridge", "note_to_self_avatar")
|
|
||||||
helper.Copy(up.Int, "bridge", "portal_message_buffer")
|
|
||||||
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
|
|
||||||
helper.Copy(up.Bool, "bridge", "bridge_notices")
|
|
||||||
helper.Copy(up.Bool, "bridge", "delivery_receipts")
|
|
||||||
helper.Copy(up.Bool, "bridge", "message_status_events")
|
|
||||||
helper.Copy(up.Bool, "bridge", "message_error_notices")
|
|
||||||
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
|
|
||||||
helper.Copy(up.Bool, "bridge", "resend_bridge_info")
|
|
||||||
helper.Copy(up.Bool, "bridge", "public_portals")
|
|
||||||
helper.Copy(up.Bool, "bridge", "caption_in_message")
|
|
||||||
helper.Copy(up.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"},
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
|
|
||||||
puppet.CustomMXID = mxid
|
|
||||||
puppet.AccessToken = accessToken
|
|
||||||
err := puppet.Update(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save access token: %w", err)
|
|
||||||
}
|
|
||||||
err = puppet.StartCustomMXID(false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// TODO leave rooms with default puppet
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (puppet *Puppet) ClearCustomMXID() {
|
|
||||||
save := puppet.CustomMXID != "" || puppet.AccessToken != ""
|
|
||||||
puppet.bridge.puppetsLock.Lock()
|
|
||||||
if puppet.CustomMXID != "" && puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] == puppet {
|
|
||||||
delete(puppet.bridge.puppetsByCustomMXID, puppet.CustomMXID)
|
|
||||||
}
|
|
||||||
puppet.bridge.puppetsLock.Unlock()
|
|
||||||
puppet.CustomMXID = ""
|
|
||||||
puppet.AccessToken = ""
|
|
||||||
puppet.customIntent = nil
|
|
||||||
puppet.customUser = nil
|
|
||||||
if save {
|
|
||||||
err := puppet.Update(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
puppet.log.Err(err).Msg("Failed to clear custom MXID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
|
|
||||||
newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(context.TODO(), puppet.CustomMXID, puppet.AccessToken, reloginOnFail)
|
|
||||||
if err != nil {
|
|
||||||
puppet.ClearCustomMXID()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
puppet.bridge.puppetsLock.Lock()
|
|
||||||
puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
|
|
||||||
puppet.bridge.puppetsLock.Unlock()
|
|
||||||
if puppet.AccessToken != newAccessToken {
|
|
||||||
puppet.AccessToken = newAccessToken
|
|
||||||
err = puppet.Update(context.TODO())
|
|
||||||
}
|
|
||||||
puppet.customIntent = newIntent
|
|
||||||
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) tryAutomaticDoublePuppeting() {
|
|
||||||
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.log.Debug().Msg("Checking if double puppeting needs to be enabled")
|
|
||||||
puppet := user.bridge.GetPuppetBySignalID(user.SignalID)
|
|
||||||
if puppet.CustomMXID == user.MXID {
|
|
||||||
user.log.Debug().Msg("User already has double-puppeting enabled")
|
|
||||||
// Custom puppet already enabled
|
|
||||||
return
|
|
||||||
}
|
|
||||||
puppet.CustomMXID = user.MXID
|
|
||||||
err := puppet.StartCustomMXID(true)
|
|
||||||
if err != nil {
|
|
||||||
user.log.Warn().Err(err).Msg("Failed to login with shared secret")
|
|
||||||
} else {
|
|
||||||
// TODO leave rooms with default puppet
|
|
||||||
user.log.Debug().Msg("Successfully automatically enabled custom puppet")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/database/upgrades"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Database struct {
|
|
||||||
*dbutil.Database
|
|
||||||
|
|
||||||
User *UserQuery
|
|
||||||
Portal *PortalQuery
|
|
||||||
LostPortal *LostPortalQuery
|
|
||||||
Puppet *PuppetQuery
|
|
||||||
Message *MessageQuery
|
|
||||||
Reaction *ReactionQuery
|
|
||||||
DisappearingMessage *DisappearingMessageQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(db *dbutil.Database) *Database {
|
|
||||||
db.UpgradeTable = upgrades.Table
|
|
||||||
return &Database{
|
|
||||||
Database: db,
|
|
||||||
User: &UserQuery{dbutil.MakeQueryHelper(db, newUser)},
|
|
||||||
Portal: &PortalQuery{dbutil.MakeQueryHelper(db, newPortal)},
|
|
||||||
LostPortal: &LostPortalQuery{dbutil.MakeQueryHelper(db, newLostPortal)},
|
|
||||||
Puppet: &PuppetQuery{dbutil.MakeQueryHelper(db, newPuppet)},
|
|
||||||
Message: &MessageQuery{dbutil.MakeQueryHelper(db, newMessage)},
|
|
||||||
Reaction: &ReactionQuery{dbutil.MakeQueryHelper(db, newReaction)},
|
|
||||||
DisappearingMessage: &DisappearingMessageQuery{dbutil.MakeQueryHelper(db, newDisappearingMessage)},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getUnscheduledDisappearingMessagesForRoomQuery = `
|
|
||||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
|
||||||
FROM disappearing_message WHERE expiration_ts IS NULL AND room_id = $1
|
|
||||||
`
|
|
||||||
getExpiredDisappearingMessagesQuery = `
|
|
||||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
|
||||||
FROM disappearing_message WHERE expiration_ts IS NOT NULL AND expiration_ts <= $1
|
|
||||||
`
|
|
||||||
getNextDisappearingMessageQuery = `
|
|
||||||
SELECT room_id, mxid, expiration_seconds, expiration_ts
|
|
||||||
FROM disappearing_message WHERE expiration_ts IS NOT NULL ORDER BY expiration_ts ASC LIMIT 1
|
|
||||||
`
|
|
||||||
insertDisappearingMessageQuery = `
|
|
||||||
INSERT INTO disappearing_message (room_id, mxid, expiration_seconds, expiration_ts) VALUES ($1, $2, $3, $4)
|
|
||||||
`
|
|
||||||
updateDisappearingMessageQuery = `
|
|
||||||
UPDATE disappearing_message SET expiration_ts=$2 WHERE mxid=$1
|
|
||||||
`
|
|
||||||
deleteDisappearingMessageQuery = `
|
|
||||||
DELETE FROM disappearing_message WHERE mxid=$1
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
type DisappearingMessageQuery struct {
|
|
||||||
*dbutil.QueryHelper[*DisappearingMessage]
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisappearingMessage struct {
|
|
||||||
qh *dbutil.QueryHelper[*DisappearingMessage]
|
|
||||||
|
|
||||||
RoomID id.RoomID
|
|
||||||
EventID id.EventID
|
|
||||||
ExpireIn time.Duration
|
|
||||||
ExpireAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDisappearingMessage(qh *dbutil.QueryHelper[*DisappearingMessage]) *DisappearingMessage {
|
|
||||||
return &DisappearingMessage{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmq *DisappearingMessageQuery) NewWithValues(roomID id.RoomID, eventID id.EventID, expireIn time.Duration, expireAt time.Time) *DisappearingMessage {
|
|
||||||
return &DisappearingMessage{
|
|
||||||
qh: dmq.QueryHelper,
|
|
||||||
RoomID: roomID,
|
|
||||||
EventID: eventID,
|
|
||||||
ExpireIn: expireIn,
|
|
||||||
ExpireAt: expireAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmq *DisappearingMessageQuery) GetUnscheduledForRoom(ctx context.Context, roomID id.RoomID) ([]*DisappearingMessage, error) {
|
|
||||||
return dmq.QueryMany(ctx, getUnscheduledDisappearingMessagesForRoomQuery, roomID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmq *DisappearingMessageQuery) GetExpiredMessages(ctx context.Context) ([]*DisappearingMessage, error) {
|
|
||||||
return dmq.QueryMany(ctx, getExpiredDisappearingMessagesQuery, time.Now().Unix()+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmq *DisappearingMessageQuery) GetNextScheduledMessage(ctx context.Context) (*DisappearingMessage, error) {
|
|
||||||
return dmq.QueryOne(ctx, getNextDisappearingMessageQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DisappearingMessage) Scan(row dbutil.Scannable) (*DisappearingMessage, error) {
|
|
||||||
var expireIn int64
|
|
||||||
var expireAt sql.NullInt64
|
|
||||||
err := row.Scan(&msg.RoomID, &msg.EventID, &expireIn, &expireAt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msg.ExpireIn = time.Duration(expireIn) * time.Second
|
|
||||||
if expireAt.Valid {
|
|
||||||
msg.ExpireAt = time.Unix(expireAt.Int64, 0)
|
|
||||||
}
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DisappearingMessage) sqlVariables() []any {
|
|
||||||
var expireAt sql.NullInt64
|
|
||||||
if !msg.ExpireAt.IsZero() {
|
|
||||||
expireAt.Valid = true
|
|
||||||
expireAt.Int64 = msg.ExpireAt.Unix()
|
|
||||||
}
|
|
||||||
return []any{msg.RoomID, msg.EventID, int64(msg.ExpireIn.Seconds()), expireAt}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DisappearingMessage) Insert(ctx context.Context) error {
|
|
||||||
return msg.qh.Exec(ctx, insertDisappearingMessageQuery, msg.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DisappearingMessage) StartExpirationTimer(ctx context.Context) error {
|
|
||||||
msg.ExpireAt = time.Now().Add(msg.ExpireIn)
|
|
||||||
return msg.qh.Exec(ctx, updateDisappearingMessageQuery, msg.EventID, msg.ExpireAt.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *DisappearingMessage) Delete(ctx context.Context) error {
|
|
||||||
return msg.qh.Exec(ctx, deleteDisappearingMessageQuery, msg.EventID)
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getLostPortalsQuery = `SELECT chat_id, receiver, mxid FROM lost_portals`
|
|
||||||
deleteLostPortalQuery = `DELETE FROM lost_portals WHERE mxid=$1`
|
|
||||||
)
|
|
||||||
|
|
||||||
type LostPortalQuery struct {
|
|
||||||
*dbutil.QueryHelper[*LostPortal]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lpq *LostPortalQuery) GetAll(ctx context.Context) ([]*LostPortal, error) {
|
|
||||||
return lpq.QueryMany(ctx, getLostPortalsQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LostPortal struct {
|
|
||||||
qh *dbutil.QueryHelper[*LostPortal]
|
|
||||||
|
|
||||||
ChatID string
|
|
||||||
Receiver string
|
|
||||||
MXID id.RoomID
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLostPortal(qh *dbutil.QueryHelper[*LostPortal]) *LostPortal {
|
|
||||||
return &LostPortal{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LostPortal) Scan(row dbutil.Scannable) (*LostPortal, error) {
|
|
||||||
err := row.Scan(&l.ChatID, &l.Receiver, &l.MXID)
|
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *LostPortal) Delete(ctx context.Context) error {
|
|
||||||
return l.qh.Exec(ctx, deleteLostPortalQuery, l.MXID)
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/lib/pq"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getMessageByMXIDQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE mxid=$1
|
|
||||||
`
|
|
||||||
getMessagePartBySignalIDQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=$1 AND timestamp=$2 AND part_index=$3 AND signal_receiver=$4
|
|
||||||
`
|
|
||||||
getLastMessagePartBySignalIDQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
|
||||||
ORDER BY part_index DESC LIMIT 1
|
|
||||||
`
|
|
||||||
getAllMessagePartsBySignalIDQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
|
||||||
`
|
|
||||||
getMessageLastPartBySignalIDWithUnknownReceiverQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=$1 AND timestamp=$2 AND (signal_receiver=$3 OR signal_receiver='00000000-0000-0000-0000-000000000000')
|
|
||||||
ORDER BY part_index DESC LIMIT 1
|
|
||||||
`
|
|
||||||
getManyMessagesBySignalIDQueryPostgres = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=$1 AND (signal_receiver=$2 OR signal_receiver=$3) AND timestamp=ANY($4)
|
|
||||||
ORDER BY timestamp DESC, part_index DESC
|
|
||||||
`
|
|
||||||
getManyMessagesBySignalIDQuerySQLite = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE sender=?1 AND (signal_receiver=?2 OR signal_receiver=?3) AND timestamp IN (?4)
|
|
||||||
ORDER BY timestamp DESC, part_index DESC
|
|
||||||
`
|
|
||||||
getFirstBeforeQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE mx_room=$1 AND timestamp <= $2
|
|
||||||
ORDER BY timestamp DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
getMessagesBetweenTimeQuery = `
|
|
||||||
SELECT sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room FROM message
|
|
||||||
WHERE signal_chat_id=$1 AND signal_receiver=$2 AND timestamp>$3 AND timestamp<=$4 AND part_index=0
|
|
||||||
ORDER BY timestamp ASC
|
|
||||||
`
|
|
||||||
insertMessageQuery = `
|
|
||||||
INSERT INTO message (sender, timestamp, part_index, signal_chat_id, signal_receiver, mxid, mx_room)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
`
|
|
||||||
deleteMessageQuery = `
|
|
||||||
DELETE FROM message
|
|
||||||
WHERE sender=$1 AND timestamp=$2 AND part_index=$3 AND signal_receiver=$4
|
|
||||||
`
|
|
||||||
updateMessageTimestampQuery = `
|
|
||||||
UPDATE message SET timestamp=$4 WHERE sender=$1 AND timestamp=$2 AND signal_receiver=$3
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
type MessageQuery struct {
|
|
||||||
*dbutil.QueryHelper[*Message]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
qh *dbutil.QueryHelper[*Message]
|
|
||||||
|
|
||||||
Sender uuid.UUID
|
|
||||||
Timestamp uint64
|
|
||||||
PartIndex int
|
|
||||||
|
|
||||||
SignalChatID string
|
|
||||||
SignalReceiver uuid.UUID
|
|
||||||
|
|
||||||
MXID id.EventID
|
|
||||||
RoomID id.RoomID
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMessage(qh *dbutil.QueryHelper[*Message]) *Message {
|
|
||||||
return &Message{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Message, error) {
|
|
||||||
return mq.QueryOne(ctx, getMessageByMXIDQuery, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, partIndex int, receiver uuid.UUID) (*Message, error) {
|
|
||||||
return mq.QueryOne(ctx, getMessagePartBySignalIDQuery, sender, timestamp, partIndex, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetLastPartBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) (*Message, error) {
|
|
||||||
return mq.QueryOne(ctx, getLastMessagePartBySignalIDQuery, sender, timestamp, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetAllPartsBySignalID(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) ([]*Message, error) {
|
|
||||||
return mq.QueryMany(ctx, getAllMessagePartsBySignalIDQuery, sender, timestamp, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetAllBetweenTimestamps(ctx context.Context, key PortalKey, min, max uint64) ([]*Message, error) {
|
|
||||||
return mq.QueryMany(ctx, getMessagesBetweenTimeQuery, key.ChatID, key.Receiver, int64(min), int64(max))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetLastPartBySignalIDWithUnknownReceiver(ctx context.Context, sender uuid.UUID, timestamp uint64, receiver uuid.UUID) (*Message, error) {
|
|
||||||
return mq.QueryOne(ctx, getMessageLastPartBySignalIDWithUnknownReceiverQuery, sender, timestamp, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mq *MessageQuery) GetManyBySignalID(ctx context.Context, sender uuid.UUID, timestamps []uint64, receiver uuid.UUID, strictReceiver bool) ([]*Message, error) {
|
|
||||||
receiver2 := uuid.Nil
|
|
||||||
if strictReceiver {
|
|
||||||
receiver2 = receiver
|
|
||||||
}
|
|
||||||
if mq.GetDB().Dialect == dbutil.Postgres {
|
|
||||||
int64Array := make([]int64, len(timestamps))
|
|
||||||
for i, timestamp := range timestamps {
|
|
||||||
int64Array[i] = int64(timestamp)
|
|
||||||
}
|
|
||||||
return mq.QueryMany(ctx, getManyMessagesBySignalIDQueryPostgres, sender, receiver, receiver2, pq.Array(int64Array))
|
|
||||||
} else {
|
|
||||||
const varargIndex = 3
|
|
||||||
arguments := make([]any, len(timestamps)+varargIndex)
|
|
||||||
placeholders := make([]string, len(timestamps))
|
|
||||||
arguments[0] = sender
|
|
||||||
arguments[1] = receiver
|
|
||||||
arguments[2] = receiver2
|
|
||||||
for i, timestamp := range timestamps {
|
|
||||||
arguments[i+varargIndex] = timestamp
|
|
||||||
placeholders[i] = fmt.Sprintf("?%d", i+varargIndex+1)
|
|
||||||
}
|
|
||||||
return mq.QueryMany(ctx, strings.Replace(getManyMessagesBySignalIDQuerySQLite, fmt.Sprintf("?%d", varargIndex+1), strings.Join(placeholders, ", "), 1), arguments...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *Message) Scan(row dbutil.Scannable) (*Message, error) {
|
|
||||||
return dbutil.ValueOrErr(msg, row.Scan(
|
|
||||||
&msg.Sender, &msg.Timestamp, &msg.PartIndex, &msg.SignalChatID, &msg.SignalReceiver, &msg.MXID, &msg.RoomID,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *Message) sqlVariables() []any {
|
|
||||||
return []any{msg.Sender, msg.Timestamp, msg.PartIndex, msg.SignalChatID, msg.SignalReceiver, msg.MXID, msg.RoomID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *Message) Insert(ctx context.Context) error {
|
|
||||||
return msg.qh.Exec(ctx, insertMessageQuery, msg.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *Message) Delete(ctx context.Context) error {
|
|
||||||
return msg.qh.Exec(ctx, deleteMessageQuery, msg.Sender, msg.Timestamp, msg.PartIndex, msg.SignalReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg *Message) SetTimestamp(ctx context.Context, editTime uint64) error {
|
|
||||||
return msg.qh.Exec(ctx, updateMessageTimestampQuery, msg.Sender, msg.Timestamp, msg.SignalReceiver, editTime)
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
portalBaseSelect = `
|
|
||||||
SELECT chat_id, receiver, mxid, name, topic, avatar_path, avatar_hash, avatar_url,
|
|
||||||
name_set, avatar_set, topic_set, revision, encrypted, relay_user_id, expiration_time
|
|
||||||
FROM portal
|
|
||||||
`
|
|
||||||
getPortalByMXIDQuery = portalBaseSelect + `WHERE mxid=$1`
|
|
||||||
getPortalByChatIDQuery = portalBaseSelect + `WHERE chat_id=$1 AND receiver=$2`
|
|
||||||
getPortalsByReceiver = portalBaseSelect + `WHERE receiver=$1`
|
|
||||||
getPortalsByUser = portalBaseSelect + `WHERE chat_id=$1`
|
|
||||||
getAllPortalsWithMXIDQuery = portalBaseSelect + `WHERE mxid IS NOT NULL`
|
|
||||||
getChatsNotInSpaceQuery = `
|
|
||||||
SELECT chat_id FROM portal
|
|
||||||
LEFT JOIN user_portal ON portal.chat_id=user_portal.portal_chat_id AND portal.receiver=user_portal.portal_receiver
|
|
||||||
WHERE mxid<>'' AND receiver=$1 AND (user_portal.in_space=false OR user_portal.in_space IS NULL)
|
|
||||||
`
|
|
||||||
insertPortalQuery = `
|
|
||||||
INSERT INTO portal (
|
|
||||||
chat_id, receiver, mxid, name, topic, avatar_path, avatar_hash, avatar_url,
|
|
||||||
name_set, avatar_set, topic_set, revision, encrypted, relay_user_id, expiration_time
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
||||||
`
|
|
||||||
updatePortalQuery = `
|
|
||||||
UPDATE portal SET
|
|
||||||
mxid=$3, name=$4, topic=$5, avatar_path=$6, avatar_hash=$7, avatar_url=$8,
|
|
||||||
name_set=$9, avatar_set=$10, topic_set=$11, revision=$12, encrypted=$13, relay_user_id=$14, expiration_time=$15
|
|
||||||
WHERE chat_id=$1 AND receiver=$2
|
|
||||||
`
|
|
||||||
deletePortalQuery = `DELETE FROM portal WHERE chat_id=$1 AND receiver=$2`
|
|
||||||
reIDPortalQuery = `UPDATE portal SET chat_id=$2 WHERE chat_id=$1 AND receiver=$3`
|
|
||||||
)
|
|
||||||
|
|
||||||
type PortalQuery struct {
|
|
||||||
*dbutil.QueryHelper[*Portal]
|
|
||||||
}
|
|
||||||
|
|
||||||
type PortalKey struct {
|
|
||||||
ChatID string
|
|
||||||
Receiver uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pk *PortalKey) UserID() libsignalgo.ServiceID {
|
|
||||||
parsed, _ := libsignalgo.ServiceIDFromString(pk.ChatID)
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pk *PortalKey) GroupID() types.GroupIdentifier {
|
|
||||||
if len(pk.ChatID) == 44 {
|
|
||||||
return types.GroupIdentifier(pk.ChatID)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPortalKey(chatID string, receiver uuid.UUID) PortalKey {
|
|
||||||
return PortalKey{
|
|
||||||
ChatID: chatID,
|
|
||||||
Receiver: receiver,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Portal struct {
|
|
||||||
qh *dbutil.QueryHelper[*Portal]
|
|
||||||
|
|
||||||
PortalKey
|
|
||||||
MXID id.RoomID
|
|
||||||
Name string
|
|
||||||
Topic string
|
|
||||||
AvatarPath string
|
|
||||||
AvatarHash string
|
|
||||||
AvatarURL id.ContentURI
|
|
||||||
NameSet bool
|
|
||||||
AvatarSet bool
|
|
||||||
TopicSet bool
|
|
||||||
Revision uint32
|
|
||||||
Encrypted bool
|
|
||||||
RelayUserID id.UserID
|
|
||||||
ExpirationTime uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPortal(qh *dbutil.QueryHelper[*Portal]) *Portal {
|
|
||||||
return &Portal{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) GetByMXID(ctx context.Context, mxid id.RoomID) (*Portal, error) {
|
|
||||||
return pq.QueryOne(ctx, getPortalByMXIDQuery, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) GetByChatID(ctx context.Context, pk PortalKey) (*Portal, error) {
|
|
||||||
return pq.QueryOne(ctx, getPortalByChatIDQuery, pk.ChatID, pk.Receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) FindPrivateChatsWith(ctx context.Context, userID uuid.UUID) ([]*Portal, error) {
|
|
||||||
return pq.QueryMany(ctx, getPortalsByUser, userID.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) FindPrivateChatsOf(ctx context.Context, receiver uuid.UUID) ([]*Portal, error) {
|
|
||||||
return pq.QueryMany(ctx, getPortalsByReceiver, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) GetAllWithMXID(ctx context.Context) ([]*Portal, error) {
|
|
||||||
return pq.QueryMany(ctx, getAllPortalsWithMXIDQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PortalQuery) FindPrivateChatsNotInSpace(ctx context.Context, receiver uuid.UUID) ([]PortalKey, error) {
|
|
||||||
rows, err := pq.GetDB().Query(ctx, getChatsNotInSpaceQuery, receiver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dbutil.NewRowIter(rows, func(rows dbutil.Scannable) (key PortalKey, err error) {
|
|
||||||
err = rows.Scan(&key.ChatID)
|
|
||||||
key.Receiver = receiver
|
|
||||||
return
|
|
||||||
}).AsList()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
|
|
||||||
var mxid sql.NullString
|
|
||||||
err := row.Scan(
|
|
||||||
&p.ChatID,
|
|
||||||
&p.Receiver,
|
|
||||||
&mxid,
|
|
||||||
&p.Name,
|
|
||||||
&p.Topic,
|
|
||||||
&p.AvatarPath,
|
|
||||||
&p.AvatarHash,
|
|
||||||
&p.AvatarURL,
|
|
||||||
&p.NameSet,
|
|
||||||
&p.AvatarSet,
|
|
||||||
&p.TopicSet,
|
|
||||||
&p.Revision,
|
|
||||||
&p.Encrypted,
|
|
||||||
&p.RelayUserID,
|
|
||||||
&p.ExpirationTime,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.MXID = id.RoomID(mxid.String)
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) sqlVariables() []any {
|
|
||||||
return []any{
|
|
||||||
p.ChatID,
|
|
||||||
p.Receiver,
|
|
||||||
dbutil.StrPtr(p.MXID),
|
|
||||||
p.Name,
|
|
||||||
p.Topic,
|
|
||||||
p.AvatarPath,
|
|
||||||
p.AvatarHash,
|
|
||||||
&p.AvatarURL,
|
|
||||||
p.NameSet,
|
|
||||||
p.AvatarSet,
|
|
||||||
p.TopicSet,
|
|
||||||
p.Revision,
|
|
||||||
p.Encrypted,
|
|
||||||
p.RelayUserID,
|
|
||||||
p.ExpirationTime,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) Insert(ctx context.Context) error {
|
|
||||||
return p.qh.Exec(ctx, insertPortalQuery, p.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) Update(ctx context.Context) error {
|
|
||||||
return p.qh.Exec(ctx, updatePortalQuery, p.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) Delete(ctx context.Context) error {
|
|
||||||
return p.qh.Exec(ctx, deletePortalQuery, p.ChatID, p.Receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Portal) ReID(ctx context.Context, newID string) error {
|
|
||||||
return p.qh.Exec(ctx, reIDPortalQuery, p.ChatID, newID, p.Receiver)
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
puppetBaseSelect = `
|
|
||||||
SELECT uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url, name_set, avatar_set,
|
|
||||||
contact_info_set, is_registered, profile_fetched_at, custom_mxid, access_token
|
|
||||||
FROM puppet
|
|
||||||
`
|
|
||||||
getPuppetBySignalIDQuery = puppetBaseSelect + `WHERE uuid=$1`
|
|
||||||
getPuppetByNumberQuery = puppetBaseSelect + `WHERE number=$1`
|
|
||||||
getPuppetByCustomMXIDQuery = puppetBaseSelect + `WHERE custom_mxid=$1`
|
|
||||||
getPuppetsWithCustomMXID = puppetBaseSelect + `WHERE custom_mxid<>''`
|
|
||||||
updatePuppetQuery = `
|
|
||||||
UPDATE puppet SET
|
|
||||||
number=$2, name=$3, name_quality=$4, avatar_path=$5, avatar_hash=$6, avatar_url=$7,
|
|
||||||
name_set=$8, avatar_set=$9, contact_info_set=$10, is_registered=$11, profile_fetched_at=$12,
|
|
||||||
custom_mxid=$13, access_token=$14
|
|
||||||
WHERE uuid=$1
|
|
||||||
`
|
|
||||||
insertPuppetQuery = `
|
|
||||||
INSERT INTO puppet (
|
|
||||||
uuid, number, name, name_quality, avatar_path, avatar_hash, avatar_url,
|
|
||||||
name_set, avatar_set, contact_info_set, is_registered, profile_fetched_at,
|
|
||||||
custom_mxid, access_token
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
type PuppetQuery struct {
|
|
||||||
*dbutil.QueryHelper[*Puppet]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Puppet struct {
|
|
||||||
qh *dbutil.QueryHelper[*Puppet]
|
|
||||||
|
|
||||||
SignalID uuid.UUID
|
|
||||||
Number string
|
|
||||||
Name string
|
|
||||||
NameQuality int
|
|
||||||
AvatarPath string
|
|
||||||
AvatarHash string
|
|
||||||
AvatarURL id.ContentURI
|
|
||||||
NameSet bool
|
|
||||||
AvatarSet bool
|
|
||||||
|
|
||||||
IsRegistered bool
|
|
||||||
ContactInfoSet bool
|
|
||||||
ProfileFetchedAt time.Time
|
|
||||||
|
|
||||||
CustomMXID id.UserID
|
|
||||||
AccessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPuppet(qh *dbutil.QueryHelper[*Puppet]) *Puppet {
|
|
||||||
return &Puppet{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PuppetQuery) GetBySignalID(ctx context.Context, signalID uuid.UUID) (*Puppet, error) {
|
|
||||||
return pq.QueryOne(ctx, getPuppetBySignalIDQuery, signalID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PuppetQuery) GetByNumber(ctx context.Context, number string) (*Puppet, error) {
|
|
||||||
return pq.QueryOne(ctx, getPuppetByNumberQuery, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PuppetQuery) GetByCustomMXID(ctx context.Context, mxid id.UserID) (*Puppet, error) {
|
|
||||||
return pq.QueryOne(ctx, getPuppetByCustomMXIDQuery, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pq *PuppetQuery) GetAllWithCustomMXID(ctx context.Context) ([]*Puppet, error) {
|
|
||||||
return pq.QueryMany(ctx, getPuppetsWithCustomMXID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
|
|
||||||
var number, customMXID sql.NullString
|
|
||||||
var profileFetchedAt sql.NullInt64
|
|
||||||
err := row.Scan(
|
|
||||||
&p.SignalID,
|
|
||||||
&number,
|
|
||||||
&p.Name,
|
|
||||||
&p.NameQuality,
|
|
||||||
&p.AvatarPath,
|
|
||||||
&p.AvatarHash,
|
|
||||||
&p.AvatarURL,
|
|
||||||
&p.NameSet,
|
|
||||||
&p.AvatarSet,
|
|
||||||
&p.ContactInfoSet,
|
|
||||||
&p.IsRegistered,
|
|
||||||
&profileFetchedAt,
|
|
||||||
&customMXID,
|
|
||||||
&p.AccessToken,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.Number = number.String
|
|
||||||
p.CustomMXID = id.UserID(customMXID.String)
|
|
||||||
if profileFetchedAt.Valid {
|
|
||||||
p.ProfileFetchedAt = time.UnixMilli(profileFetchedAt.Int64)
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) sqlVariables() []any {
|
|
||||||
return []any{
|
|
||||||
p.SignalID,
|
|
||||||
dbutil.StrPtr(p.Number),
|
|
||||||
p.Name,
|
|
||||||
p.NameQuality,
|
|
||||||
p.AvatarPath,
|
|
||||||
p.AvatarHash,
|
|
||||||
&p.AvatarURL,
|
|
||||||
p.NameSet,
|
|
||||||
p.AvatarSet,
|
|
||||||
p.ContactInfoSet,
|
|
||||||
p.IsRegistered,
|
|
||||||
dbutil.UnixMilliPtr(p.ProfileFetchedAt),
|
|
||||||
dbutil.StrPtr(p.CustomMXID),
|
|
||||||
p.AccessToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) Insert(ctx context.Context) error {
|
|
||||||
return p.qh.Exec(ctx, insertPuppetQuery, p.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Puppet) Update(ctx context.Context) error {
|
|
||||||
return p.qh.Exec(ctx, updatePuppetQuery, p.sqlVariables()...)
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getReactionByMXIDQuery = `SELECT msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room FROM reaction WHERE mxid=$1`
|
|
||||||
getReactionBySignalIDQuery = `SELECT msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room FROM reaction WHERE msg_author=$1 AND msg_timestamp=$2 AND author=$3 AND signal_receiver=$4`
|
|
||||||
insertReactionQuery = `
|
|
||||||
INSERT INTO reaction (msg_author, msg_timestamp, author, emoji, signal_chat_id, signal_receiver, mxid, mx_room)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
||||||
`
|
|
||||||
updateReactionQuery = `
|
|
||||||
UPDATE reaction
|
|
||||||
SET mxid=$1, emoji=$2
|
|
||||||
WHERE msg_author=$3 AND msg_timestamp=$4 AND author=$5 AND signal_receiver=$6
|
|
||||||
`
|
|
||||||
deleteReactionQuery = `
|
|
||||||
DELETE FROM reaction WHERE msg_author=$1 AND msg_timestamp=$2 AND author=$3 AND signal_receiver=$4
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReactionQuery struct {
|
|
||||||
*dbutil.QueryHelper[*Reaction]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReaction(qh *dbutil.QueryHelper[*Reaction]) *Reaction {
|
|
||||||
return &Reaction{qh: qh}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Reaction struct {
|
|
||||||
qh *dbutil.QueryHelper[*Reaction]
|
|
||||||
|
|
||||||
MsgAuthor uuid.UUID
|
|
||||||
MsgTimestamp uint64
|
|
||||||
Author uuid.UUID
|
|
||||||
Emoji string
|
|
||||||
|
|
||||||
SignalChatID string
|
|
||||||
SignalReceiver uuid.UUID
|
|
||||||
|
|
||||||
MXID id.EventID
|
|
||||||
RoomID id.RoomID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rq *ReactionQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Reaction, error) {
|
|
||||||
return rq.QueryOne(ctx, getReactionByMXIDQuery, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rq *ReactionQuery) GetBySignalID(ctx context.Context, msgAuthor uuid.UUID, msgTimestamp uint64, author, signalReceiver uuid.UUID) (*Reaction, error) {
|
|
||||||
return rq.QueryOne(ctx, getReactionBySignalIDQuery, msgAuthor, msgTimestamp, author, signalReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reaction) Scan(row dbutil.Scannable) (*Reaction, error) {
|
|
||||||
return dbutil.ValueOrErr(r, row.Scan(
|
|
||||||
&r.MsgAuthor, &r.MsgTimestamp, &r.Author, &r.Emoji, &r.SignalChatID, &r.SignalReceiver, &r.MXID, &r.RoomID,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reaction) sqlVariables() []any {
|
|
||||||
return []any{
|
|
||||||
r.MsgAuthor, r.MsgTimestamp, r.Author, r.Emoji, r.SignalChatID, r.SignalReceiver, r.MXID, r.RoomID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reaction) Insert(ctx context.Context) error {
|
|
||||||
return r.qh.Exec(ctx, insertReactionQuery, r.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reaction) Update(ctx context.Context) error {
|
|
||||||
return r.qh.Exec(ctx, updateReactionQuery, r.MXID, r.Emoji, r.MsgAuthor, r.MsgTimestamp, r.Author, r.SignalReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reaction) Delete(ctx context.Context) error {
|
|
||||||
return r.qh.Exec(ctx, deleteReactionQuery, r.MsgAuthor, r.MsgTimestamp, r.Author, r.SignalReceiver)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
);
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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
|
|
||||||
);
|
|
|
@ -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 '';
|
|
|
@ -1,2 +0,0 @@
|
||||||
-- v20 (compatible with v17+): Add profile fetch timestamp for puppets
|
|
||||||
ALTER TABLE puppet ADD profile_fetched_at BIGINT;
|
|
115
database/user.go
115
database/user.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getUserByMXIDQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE mxid=$1`
|
|
||||||
getUserByPhoneQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE phone=$1`
|
|
||||||
getUserByUUIDQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE uuid=$1`
|
|
||||||
getAllLoggedInUsersQuery = `SELECT mxid, phone, uuid, management_room, space_room FROM "user" WHERE phone IS NOT NULL`
|
|
||||||
insertUserQuery = `INSERT INTO "user" (mxid, phone, uuid, management_room, space_room) VALUES ($1, $2, $3, $4, $5)`
|
|
||||||
updateUserQuery = `UPDATE "user" SET phone=$2, uuid=$3, management_room=$4, space_room=$5 WHERE mxid=$1`
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserQuery struct {
|
|
||||||
*dbutil.QueryHelper[*User]
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
qh *dbutil.QueryHelper[*User]
|
|
||||||
|
|
||||||
MXID id.UserID
|
|
||||||
SignalUsername string
|
|
||||||
SignalID uuid.UUID
|
|
||||||
ManagementRoom id.RoomID
|
|
||||||
SpaceRoom id.RoomID
|
|
||||||
|
|
||||||
lastReadCache map[PortalKey]uint64
|
|
||||||
lastReadCacheLock sync.Mutex
|
|
||||||
inSpaceCache map[PortalKey]bool
|
|
||||||
inSpaceCacheLock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUser(qh *dbutil.QueryHelper[*User]) *User {
|
|
||||||
return &User{
|
|
||||||
qh: qh,
|
|
||||||
|
|
||||||
lastReadCache: make(map[PortalKey]uint64),
|
|
||||||
inSpaceCache: make(map[PortalKey]bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uq *UserQuery) GetByMXID(ctx context.Context, mxid id.UserID) (*User, error) {
|
|
||||||
return uq.QueryOne(ctx, getUserByMXIDQuery, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uq *UserQuery) GetByPhone(ctx context.Context, phone string) (*User, error) {
|
|
||||||
return uq.QueryOne(ctx, getUserByPhoneQuery, phone)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uq *UserQuery) GetBySignalID(ctx context.Context, uuid uuid.UUID) (*User, error) {
|
|
||||||
return uq.QueryOne(ctx, getUserByUUIDQuery, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uq *UserQuery) GetAllLoggedIn(ctx context.Context) ([]*User, error) {
|
|
||||||
return uq.QueryMany(ctx, getAllLoggedInUsersQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) sqlVariables() []any {
|
|
||||||
var nu uuid.NullUUID
|
|
||||||
nu.UUID = u.SignalID
|
|
||||||
nu.Valid = u.SignalID != uuid.Nil
|
|
||||||
return []any{u.MXID, dbutil.StrPtr(u.SignalUsername), nu, dbutil.StrPtr(u.ManagementRoom), dbutil.StrPtr(u.SpaceRoom)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Insert(ctx context.Context) error {
|
|
||||||
return u.qh.Exec(ctx, insertUserQuery, u.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Update(ctx context.Context) error {
|
|
||||||
return u.qh.Exec(ctx, updateUserQuery, u.sqlVariables()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Scan(row dbutil.Scannable) (*User, error) {
|
|
||||||
var phone, managementRoom, spaceRoom sql.NullString
|
|
||||||
var signalID uuid.NullUUID
|
|
||||||
err := row.Scan(
|
|
||||||
&u.MXID,
|
|
||||||
&phone,
|
|
||||||
&signalID,
|
|
||||||
&managementRoom,
|
|
||||||
&spaceRoom,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u.SignalUsername = phone.String
|
|
||||||
u.SignalID = signalID.UUID
|
|
||||||
u.ManagementRoom = id.RoomID(managementRoom.String)
|
|
||||||
u.SpaceRoom = id.RoomID(spaceRoom.String)
|
|
||||||
return u, nil
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getLastReadTSQuery = `SELECT last_read_ts FROM user_portal WHERE user_mxid=$1 AND portal_chat_id=$2 AND portal_receiver=$3`
|
|
||||||
setLastReadTSQuery = `
|
|
||||||
INSERT INTO user_portal (user_mxid, portal_chat_id, portal_receiver, last_read_ts) VALUES ($1, $2, $3, $4)
|
|
||||||
ON CONFLICT (user_mxid, portal_chat_id, portal_receiver) DO UPDATE
|
|
||||||
SET last_read_ts=excluded.last_read_ts WHERE user_portal.last_read_ts<excluded.last_read_ts
|
|
||||||
`
|
|
||||||
getIsInSpaceQuery = `SELECT in_space FROM user_portal WHERE user_mxid=$1 AND portal_chat_id=$2 AND portal_receiver=$3`
|
|
||||||
setIsInSpaceQuery = `
|
|
||||||
INSERT INTO user_portal (user_mxid, portal_chat_id, portal_receiver, in_space) VALUES ($1, $2, $3, true)
|
|
||||||
ON CONFLICT (user_mxid, portal_chat_id, portal_receiver) DO UPDATE SET in_space=true
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *User) GetLastReadTS(ctx context.Context, portal PortalKey) uint64 {
|
|
||||||
u.lastReadCacheLock.Lock()
|
|
||||||
defer u.lastReadCacheLock.Unlock()
|
|
||||||
if cached, ok := u.lastReadCache[portal]; ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
var ts int64
|
|
||||||
err := u.qh.GetDB().QueryRow(ctx, getLastReadTSQuery, u.MXID, portal.ChatID, portal.Receiver).Scan(&ts)
|
|
||||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
||||||
zerolog.Ctx(ctx).Err(err).
|
|
||||||
Stringer("user_id", u.MXID).
|
|
||||||
Any("portal_key", portal).
|
|
||||||
Msg("Failed to query last read timestamp")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
u.lastReadCache[portal] = uint64(ts)
|
|
||||||
return uint64(ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) SetLastReadTS(ctx context.Context, portal PortalKey, ts uint64) {
|
|
||||||
u.lastReadCacheLock.Lock()
|
|
||||||
defer u.lastReadCacheLock.Unlock()
|
|
||||||
err := u.qh.Exec(ctx, setLastReadTSQuery, u.MXID, portal.ChatID, portal.Receiver, int64(ts))
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).
|
|
||||||
Stringer("user_id", u.MXID).
|
|
||||||
Any("portal_key", portal).
|
|
||||||
Msg("Failed to update last read timestamp")
|
|
||||||
} else {
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Stringer("user_id", u.MXID).
|
|
||||||
Any("portal_key", portal).
|
|
||||||
Uint64("last_read_ts", ts).
|
|
||||||
Msg("Updated last read timestamp of portal")
|
|
||||||
u.lastReadCache[portal] = ts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) IsInSpace(ctx context.Context, portal PortalKey) bool {
|
|
||||||
u.inSpaceCacheLock.Lock()
|
|
||||||
defer u.inSpaceCacheLock.Unlock()
|
|
||||||
if cached, ok := u.inSpaceCache[portal]; ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
var inSpace bool
|
|
||||||
err := u.qh.GetDB().QueryRow(ctx, getIsInSpaceQuery, u.MXID, portal.ChatID, portal.Receiver).Scan(&inSpace)
|
|
||||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
||||||
zerolog.Ctx(ctx).Err(err).
|
|
||||||
Stringer("user_id", u.MXID).
|
|
||||||
Any("portal_key", portal).
|
|
||||||
Msg("Failed to query in space status")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
u.inSpaceCache[portal] = inSpace
|
|
||||||
return inSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) MarkInSpace(ctx context.Context, portal PortalKey) {
|
|
||||||
u.inSpaceCacheLock.Lock()
|
|
||||||
defer u.inSpaceCacheLock.Unlock()
|
|
||||||
err := u.qh.Exec(ctx, setIsInSpaceQuery, u.MXID, portal.ChatID, portal.Receiver)
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).
|
|
||||||
Stringer("user_id", u.MXID).
|
|
||||||
Any("portal_key", portal).
|
|
||||||
Msg("Failed to update in space status")
|
|
||||||
} else {
|
|
||||||
u.inSpaceCache[portal] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) RemoveInSpaceCache(key PortalKey) {
|
|
||||||
u.inSpaceCacheLock.Lock()
|
|
||||||
defer u.inSpaceCacheLock.Unlock()
|
|
||||||
delete(u.inSpaceCache, key)
|
|
||||||
}
|
|
156
disappearing.go
156
disappearing.go
|
@ -1,156 +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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DisappearingMessagesManager struct {
|
|
||||||
DB *database.Database
|
|
||||||
Log zerolog.Logger
|
|
||||||
Bridge *SignalBridge
|
|
||||||
checkMessagesChan chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmm *DisappearingMessagesManager) ScheduleDisappearingForRoom(ctx context.Context, roomID id.RoomID) {
|
|
||||||
log := dmm.Log.With().Stringer("room_id", roomID).Logger()
|
|
||||||
disappearingMessages, err := dmm.DB.DisappearingMessage.GetUnscheduledForRoom(ctx, roomID)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to get unscheduled disappearing messages")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, disappearingMessage := range disappearingMessages {
|
|
||||||
err = disappearingMessage.StartExpirationTimer(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to schedule disappearing message")
|
|
||||||
} else {
|
|
||||||
log.Debug().
|
|
||||||
Stringer("event_id", disappearingMessage.EventID).
|
|
||||||
Time("expire_at", disappearingMessage.ExpireAt).
|
|
||||||
Msg("Scheduling disappearing message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the disappearing messages loop to check again
|
|
||||||
dmm.checkMessagesChan <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmm *DisappearingMessagesManager) StartDisappearingLoop(ctx context.Context) {
|
|
||||||
dmm.checkMessagesChan = make(chan struct{}, 1)
|
|
||||||
go func() {
|
|
||||||
log := dmm.Log.With().Str("action", "loop").Logger()
|
|
||||||
ctx = log.WithContext(ctx)
|
|
||||||
for {
|
|
||||||
dmm.redactExpiredMessages(ctx)
|
|
||||||
|
|
||||||
duration := 10 * time.Minute // Check again in 10 minutes just in case
|
|
||||||
nextMsg, err := dmm.DB.DisappearingMessage.GetNextScheduledMessage(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Err(err).Msg("Failed to get next disappearing message")
|
|
||||||
continue
|
|
||||||
} else if nextMsg != nil {
|
|
||||||
duration = time.Until(nextMsg.ExpireAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(duration):
|
|
||||||
case <-dmm.checkMessagesChan:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmm *DisappearingMessagesManager) redactExpiredMessages(ctx context.Context) {
|
|
||||||
log := zerolog.Ctx(ctx)
|
|
||||||
expiredMessages, err := dmm.DB.DisappearingMessage.GetExpiredMessages(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to get expired disappearing messages")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, msg := range expiredMessages {
|
|
||||||
portal := dmm.Bridge.GetPortalByMXID(msg.RoomID)
|
|
||||||
if portal == nil {
|
|
||||||
log.Warn().Stringer("event_id", msg.EventID).Stringer("room_id", msg.RoomID).Msg("Failed to redact message: portal not found")
|
|
||||||
err = msg.Delete(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).
|
|
||||||
Stringer("event_id", msg.EventID).
|
|
||||||
Msg("Failed to delete disappearing message row in database")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, err = portal.MainIntent().RedactEvent(ctx, msg.RoomID, msg.EventID, mautrix.ReqRedact{
|
|
||||||
Reason: "Message expired",
|
|
||||||
TxnID: fmt.Sprintf("mxsg_disappear_%s", msg.EventID),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).
|
|
||||||
Stringer("event_id", msg.EventID).
|
|
||||||
Stringer("room_id", msg.RoomID).
|
|
||||||
Msg("Failed to redact message")
|
|
||||||
} else {
|
|
||||||
log.Err(err).
|
|
||||||
Stringer("event_id", msg.EventID).
|
|
||||||
Stringer("room_id", msg.RoomID).
|
|
||||||
Msg("Redacted message")
|
|
||||||
}
|
|
||||||
err = msg.Delete(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).
|
|
||||||
Stringer("event_id", msg.EventID).
|
|
||||||
Msg("Failed to delete disappearing message row in database")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dmm *DisappearingMessagesManager) AddDisappearingMessage(ctx context.Context, eventID id.EventID, roomID id.RoomID, expireIn time.Duration, startTimerNow bool) {
|
|
||||||
if expireIn == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var expireAt time.Time
|
|
||||||
if startTimerNow {
|
|
||||||
expireAt = time.Now().Add(expireIn)
|
|
||||||
}
|
|
||||||
disappearingMessage := dmm.DB.DisappearingMessage.NewWithValues(roomID, eventID, expireIn, expireAt)
|
|
||||||
err := disappearingMessage.Insert(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("event_id", eventID).
|
|
||||||
Msg("Failed to add disappearing message to database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
zerolog.Ctx(ctx).Debug().Stringer("event_id", eventID).
|
|
||||||
Msg("Added disappearing message row to database")
|
|
||||||
if startTimerNow {
|
|
||||||
// Tell the disappearing messages loop to check again
|
|
||||||
dmm.checkMessagesChan <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,11 +17,7 @@ function fixperms {
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ ! -f /data/config.yaml ]]; then
|
if [[ ! -f /data/config.yaml ]]; then
|
||||||
if [[ "$BRIDGEV2" == "1" ]]; then
|
$BINARY_NAME -c /data/config.yaml -e
|
||||||
$BINARY_NAME -c /data/config.yaml -e
|
|
||||||
else
|
|
||||||
cp /opt/mautrix-signal/example-config.yaml /data/config.yaml
|
|
||||||
fi
|
|
||||||
echo "Didn't find a config file."
|
echo "Didn't find a config file."
|
||||||
echo "Copied default config file to /data/config.yaml"
|
echo "Copied default config file to /data/config.yaml"
|
||||||
echo "Modify that config file to your liking."
|
echo "Modify that config file to your liking."
|
||||||
|
|
|
@ -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:<path>?_txlock=immediate` is recommended.
|
|
||||||
# https://github.com/mattn/go-sqlite3#connection-string
|
|
||||||
# Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
|
|
||||||
# To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
|
|
||||||
uri: postgres://user:password@host/database?sslmode=disable
|
|
||||||
# Maximum number of connections. Mostly relevant for Postgres.
|
|
||||||
max_open_conns: 20
|
|
||||||
max_idle_conns: 2
|
|
||||||
# Maximum connection idle time and lifetime before they're closed. Disabled if null.
|
|
||||||
# Parsed with https://pkg.go.dev/time#ParseDuration
|
|
||||||
max_conn_idle_time: null
|
|
||||||
max_conn_lifetime: null
|
|
||||||
|
|
||||||
# The unique ID of this appservice.
|
|
||||||
id: signal
|
|
||||||
# Appservice bot details.
|
|
||||||
bot:
|
|
||||||
# Username of the appservice bot.
|
|
||||||
username: signalbot
|
|
||||||
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
|
||||||
# to leave display name/avatar as-is.
|
|
||||||
displayname: Signal bridge bot
|
|
||||||
avatar: mxc://maunium.net/wPJgTQbZOtpBFmDNkiNEMDUp
|
|
||||||
|
|
||||||
# Whether or not to receive ephemeral events via appservice transactions.
|
|
||||||
# Requires MSC2409 support (i.e. Synapse 1.22+).
|
|
||||||
ephemeral_events: true
|
|
||||||
|
|
||||||
# Should incoming events be handled asynchronously?
|
|
||||||
# This may be necessary for large public instances with lots of messages going through.
|
|
||||||
# However, messages will not be guaranteed to be bridged in the same order they were sent in.
|
|
||||||
async_transactions: false
|
|
||||||
|
|
||||||
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
|
|
||||||
as_token: "This value is generated when generating the registration"
|
|
||||||
hs_token: "This value is generated when generating the registration"
|
|
||||||
|
|
||||||
# Prometheus config.
|
|
||||||
metrics:
|
|
||||||
# Enable prometheus metrics?
|
|
||||||
enabled: false
|
|
||||||
# IP and port where the metrics listener should be. The path is always /metrics
|
|
||||||
listen: 127.0.0.1:8000
|
|
||||||
|
|
||||||
signal:
|
|
||||||
# Default device name that shows up in the Signal app.
|
|
||||||
device_name: mautrix-signal
|
|
||||||
|
|
||||||
# Bridge config
|
|
||||||
bridge:
|
|
||||||
# Localpart template of MXIDs for Signal users.
|
|
||||||
# {{.}} is replaced with the internal ID of the Signal user.
|
|
||||||
username_template: signal_{{.}}
|
|
||||||
# Displayname template for Signal users. This is also used as the room name in DMs if private_chat_portal_meta is enabled.
|
|
||||||
# {{.ProfileName}} - The Signal profile name set by the user.
|
|
||||||
# {{.ContactName}} - The name for the user from your phone's contact list. This is not safe on multi-user instances.
|
|
||||||
# {{.PhoneNumber}} - The phone number of the user.
|
|
||||||
# {{.UUID}} - The UUID of the Signal user.
|
|
||||||
# {{.AboutEmoji}} - The emoji set by the user in their profile.
|
|
||||||
displayname_template: '{{or .ProfileName .PhoneNumber "Unknown user"}}'
|
|
||||||
# Whether to explicitly set the avatar and room name for private chat portal rooms.
|
|
||||||
# If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.
|
|
||||||
# If set to `always`, all DM rooms will have explicit names and avatars set.
|
|
||||||
# If set to `never`, DM rooms will never have names and avatars set.
|
|
||||||
private_chat_portal_meta: default
|
|
||||||
# Should avatars from the user's contact list be used? This is not safe on multi-user instances.
|
|
||||||
use_contact_avatars: false
|
|
||||||
# Should the bridge sync ghost user info even if profile fetching fails? This is not safe on multi-user instances.
|
|
||||||
use_outdated_profiles: false
|
|
||||||
# Should the Signal user's phone number be included in the room topic in private chat portal rooms?
|
|
||||||
number_in_topic: true
|
|
||||||
# Avatar image for the Note to Self room.
|
|
||||||
note_to_self_avatar: mxc://maunium.net/REBIVrqjZwmaWpssCZpBlmlL
|
|
||||||
|
|
||||||
portal_message_buffer: 128
|
|
||||||
|
|
||||||
# Should the bridge create a space for each logged-in user and add bridged rooms to it?
|
|
||||||
# Users who logged in before turning this on should run `!signal sync-space` to create and fill the space for the first time.
|
|
||||||
personal_filtering_spaces: false
|
|
||||||
# Should Matrix m.notice-type messages be bridged?
|
|
||||||
bridge_notices: true
|
|
||||||
# Should the bridge send a read receipt from the bridge bot when a message has been sent to Signal?
|
|
||||||
delivery_receipts: false
|
|
||||||
# Whether the bridge should send the message status as a custom com.beeper.message_send_status event.
|
|
||||||
message_status_events: false
|
|
||||||
# Whether the bridge should send error notices via m.notice events when a message fails to bridge.
|
|
||||||
message_error_notices: true
|
|
||||||
# Should the bridge update the m.direct account data event when double puppeting is enabled.
|
|
||||||
# Note that updating the m.direct event is not atomic (except with mautrix-asmux)
|
|
||||||
# and is therefore prone to race conditions.
|
|
||||||
sync_direct_chat_list: false
|
|
||||||
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
|
|
||||||
# This field will automatically be changed back to false after it, except if the config file is not writable.
|
|
||||||
resend_bridge_info: false
|
|
||||||
# Whether or not to make portals of groups that don't need approval of an admin to join by invite
|
|
||||||
# link publicly joinable on Matrix.
|
|
||||||
public_portals: false
|
|
||||||
# Send captions in the same message as images. This will send data compatible with both MSC2530.
|
|
||||||
# This is currently not supported in most clients.
|
|
||||||
caption_in_message: false
|
|
||||||
# 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: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
|
|
||||||
m.notice: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
|
|
||||||
m.emote: "* <b>{{ .Sender.Displayname }}</b> {{ .Message }}"
|
|
||||||
m.file: "<b>{{ .Sender.Displayname }}</b> sent a file"
|
|
||||||
m.image: "<b>{{ .Sender.Displayname }}</b> sent an image"
|
|
||||||
m.audio: "<b>{{ .Sender.Displayname }}</b> sent an audio file"
|
|
||||||
m.video: "<b>{{ .Sender.Displayname }}</b> sent a video"
|
|
||||||
m.location: "<b>{{ .Sender.Displayname }}</b> sent a location"
|
|
||||||
|
|
||||||
# Logging config. See https://github.com/tulir/zeroconfig for details.
|
|
||||||
logging:
|
|
||||||
min_level: debug
|
|
||||||
writers:
|
|
||||||
- type: stdout
|
|
||||||
format: pretty-colored
|
|
||||||
- type: file
|
|
||||||
format: json
|
|
||||||
filename: ./logs/mautrix-signal.log
|
|
||||||
max_size: 100
|
|
||||||
max_backups: 10
|
|
||||||
compress: true
|
|
3
go.mod
3
go.mod
|
@ -48,3 +48,6 @@ require (
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
maunium.net/go/mauflag v1.0.0 // indirect
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//replace maunium.net/go/mautrix => ../mautrix-go
|
||||||
|
//replace go.mau.fi/util => ../../Go/go-util
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
352
main.go
352
main.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"go.mau.fi/util/configupgrade"
|
|
||||||
"go.mau.fi/util/dbutil"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/bridge"
|
|
||||||
"maunium.net/go/mautrix/bridge/commands"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/config"
|
|
||||||
"go.mau.fi/mautrix-signal/database"
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed example-config.yaml
|
|
||||||
var ExampleConfig string
|
|
||||||
|
|
||||||
// Information to find out exactly which commit the bridge was built from.
|
|
||||||
// These are filled at build time with the -X linker flag.
|
|
||||||
var (
|
|
||||||
Tag = "unknown"
|
|
||||||
Commit = "unknown"
|
|
||||||
BuildTime = "unknown"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SignalBridge struct {
|
|
||||||
bridge.Bridge
|
|
||||||
|
|
||||||
Config *config.Config
|
|
||||||
DB *database.Database
|
|
||||||
Metrics *MetricsHandler
|
|
||||||
MeowStore *store.Container
|
|
||||||
|
|
||||||
provisioning *ProvisioningAPI
|
|
||||||
|
|
||||||
usersByMXID map[id.UserID]*User
|
|
||||||
usersBySignalID map[uuid.UUID]*User
|
|
||||||
usersLock sync.Mutex
|
|
||||||
|
|
||||||
managementRooms map[id.RoomID]*User
|
|
||||||
managementRoomsLock sync.Mutex
|
|
||||||
|
|
||||||
portalsByMXID map[id.RoomID]*Portal
|
|
||||||
portalsByID map[database.PortalKey]*Portal
|
|
||||||
portalsLock sync.Mutex
|
|
||||||
|
|
||||||
puppets map[uuid.UUID]*Puppet
|
|
||||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
|
||||||
puppetsLock sync.Mutex
|
|
||||||
|
|
||||||
disappearingMessagesManager *DisappearingMessagesManager
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ bridge.ChildOverride = (*SignalBridge)(nil)
|
|
||||||
|
|
||||||
func (br *SignalBridge) GetExampleConfig() string {
|
|
||||||
return ExampleConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) GetConfigPtr() interface{} {
|
|
||||||
br.Config = &config.Config{
|
|
||||||
BaseConfig: &br.Bridge.Config,
|
|
||||||
}
|
|
||||||
br.Config.BaseConfig.Bridge = &br.Config.Bridge
|
|
||||||
return br.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) Init() {
|
|
||||||
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
|
||||||
br.RegisterCommands()
|
|
||||||
|
|
||||||
signalmeow.SetLogger(br.ZLog.With().Str("component", "signalmeow").Logger())
|
|
||||||
|
|
||||||
br.DB = database.New(br.Bridge.DB)
|
|
||||||
br.MeowStore = store.NewStore(br.Bridge.DB, dbutil.ZeroLogger(br.ZLog.With().Str("db_section", "signalmeow").Logger()))
|
|
||||||
|
|
||||||
ss := br.Config.Bridge.Provisioning.SharedSecret
|
|
||||||
if len(ss) > 0 && ss != "disable" {
|
|
||||||
br.provisioning = &ProvisioningAPI{bridge: br, log: br.ZLog.With().Str("component", "provisioning").Logger()}
|
|
||||||
}
|
|
||||||
br.disappearingMessagesManager = &DisappearingMessagesManager{
|
|
||||||
DB: br.DB,
|
|
||||||
Log: br.ZLog.With().Str("component", "disappearing messages").Logger(),
|
|
||||||
Bridge: br,
|
|
||||||
}
|
|
||||||
|
|
||||||
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.ZLog.With().Str("component", "metrics").Logger(), br.DB)
|
|
||||||
br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent
|
|
||||||
|
|
||||||
signalFormatParams = &signalfmt.FormatParams{
|
|
||||||
GetUserInfo: func(ctx context.Context, u uuid.UUID) signalfmt.UserInfo {
|
|
||||||
puppet := br.GetPuppetBySignalID(u)
|
|
||||||
if puppet == nil {
|
|
||||||
return signalfmt.UserInfo{}
|
|
||||||
}
|
|
||||||
user := br.GetUserBySignalID(u)
|
|
||||||
if user != nil {
|
|
||||||
return signalfmt.UserInfo{
|
|
||||||
MXID: user.MXID,
|
|
||||||
Name: puppet.Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return signalfmt.UserInfo{
|
|
||||||
MXID: puppet.MXID,
|
|
||||||
Name: puppet.Name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
matrixFormatParams = &matrixfmt.HTMLParser{
|
|
||||||
GetUUIDFromMXID: func(ctx context.Context, userID id.UserID) uuid.UUID {
|
|
||||||
parsed, ok := br.ParsePuppetMXID(userID)
|
|
||||||
if ok {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
user := br.GetUserByMXIDIfExists(userID)
|
|
||||||
if user != nil && user.SignalID != uuid.Nil {
|
|
||||||
return user.SignalID
|
|
||||||
}
|
|
||||||
return uuid.Nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) logLostPortals(ctx context.Context) {
|
|
||||||
exists, err := br.DB.TableExists(ctx, "lost_portals")
|
|
||||||
if err != nil {
|
|
||||||
br.ZLog.Err(err).Msg("Failed to check if lost_portals table exists")
|
|
||||||
} else if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lostPortals, err := br.DB.LostPortal.GetAll(ctx)
|
|
||||||
if err != nil {
|
|
||||||
br.ZLog.Err(err).Msg("Failed to get lost portals")
|
|
||||||
return
|
|
||||||
} else if len(lostPortals) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
lostCountByReceiver := make(map[string]int)
|
|
||||||
for _, lost := range lostPortals {
|
|
||||||
lostCountByReceiver[lost.Receiver]++
|
|
||||||
}
|
|
||||||
br.ZLog.Warn().
|
|
||||||
Any("count_by_receiver", lostCountByReceiver).
|
|
||||||
Msg("Some portals were discarded due to the receiver not being logged into the bridge anymore. " +
|
|
||||||
"Use `!signal cleanup-lost-portals` to remove them from the database. " +
|
|
||||||
"Alternatively, you can re-insert the data into the portal table with the appropriate receiver column to restore the portals.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) Start() {
|
|
||||||
go br.logLostPortals(context.TODO())
|
|
||||||
err := br.MeowStore.Upgrade(context.TODO())
|
|
||||||
if err != nil {
|
|
||||||
br.ZLog.Fatal().Err(err).Msg("Failed to upgrade signalmeow database")
|
|
||||||
os.Exit(15)
|
|
||||||
}
|
|
||||||
if br.provisioning != nil {
|
|
||||||
br.ZLog.Debug().Msg("Initializing provisioning API")
|
|
||||||
br.provisioning.Init()
|
|
||||||
}
|
|
||||||
go br.StartUsers()
|
|
||||||
if br.Config.Metrics.Enabled {
|
|
||||||
go br.Metrics.Start()
|
|
||||||
}
|
|
||||||
go br.disappearingMessagesManager.StartDisappearingLoop(context.TODO())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) Stop() {
|
|
||||||
br.Metrics.Stop()
|
|
||||||
for _, user := range br.usersByMXID {
|
|
||||||
br.ZLog.Debug().Stringer("user_id", user.MXID).Msg("Disconnecting user")
|
|
||||||
user.Disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) GetIPortal(mxid id.RoomID) bridge.Portal {
|
|
||||||
p := br.GetPortalByMXID(mxid)
|
|
||||||
if p == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) GetIUser(mxid id.UserID, create bool) bridge.User {
|
|
||||||
p := br.GetUserByMXID(mxid)
|
|
||||||
if p == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) IsGhost(mxid id.UserID) bool {
|
|
||||||
_, isGhost := br.ParsePuppetMXID(mxid)
|
|
||||||
return isGhost
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) GetIGhost(mxid id.UserID) bridge.Ghost {
|
|
||||||
p := br.GetPuppetByMXID(mxid)
|
|
||||||
if p == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) CreatePrivatePortal(roomID id.RoomID, brInviter bridge.User, brGhost bridge.Ghost) {
|
|
||||||
inviter := brInviter.(*User)
|
|
||||||
puppet := brGhost.(*Puppet)
|
|
||||||
|
|
||||||
log := br.ZLog.With().
|
|
||||||
Str("action", "create private portal").
|
|
||||||
Stringer("target_room_id", roomID).
|
|
||||||
Stringer("inviter_mxid", brInviter.GetMXID()).
|
|
||||||
Stringer("invitee_uuid", puppet.SignalID).
|
|
||||||
Logger()
|
|
||||||
log.Debug().Msg("Creating private chat portal")
|
|
||||||
|
|
||||||
key := database.NewPortalKey(puppet.SignalID.String(), inviter.SignalID)
|
|
||||||
portal := br.GetPortalByChatID(key)
|
|
||||||
ctx := log.WithContext(context.TODO())
|
|
||||||
|
|
||||||
if len(portal.MXID) == 0 {
|
|
||||||
br.createPrivatePortalFromInvite(ctx, roomID, inviter, puppet, portal)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug().
|
|
||||||
Stringer("existing_room_id", portal.MXID).
|
|
||||||
Msg("Existing private chat portal found, trying to invite user")
|
|
||||||
|
|
||||||
ok := portal.ensureUserInvited(ctx, inviter)
|
|
||||||
if !ok {
|
|
||||||
log.Warn().Msg("Failed to invite user to existing private chat portal. Redirecting portal to new room")
|
|
||||||
br.createPrivatePortalFromInvite(ctx, roomID, inviter, puppet, portal)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
intent := puppet.DefaultIntent()
|
|
||||||
errorMessage := fmt.Sprintf("You already have a private chat portal with me at [%[1]s](https://matrix.to/#/%[1]s)", portal.MXID)
|
|
||||||
errorContent := format.RenderMarkdown(errorMessage, true, false)
|
|
||||||
_, _ = intent.SendMessageEvent(ctx, roomID, event.EventMessage, errorContent)
|
|
||||||
log.Debug().Msg("Leaving ghost from private chat room after accepting invite because we already have a chat with the user")
|
|
||||||
_, _ = intent.LeaveRoom(ctx, roomID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br *SignalBridge) createPrivatePortalFromInvite(ctx context.Context, roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) {
|
|
||||||
log := zerolog.Ctx(ctx)
|
|
||||||
log.Debug().Msg("Creating private portal from invite")
|
|
||||||
|
|
||||||
// Check if room is already encrypted
|
|
||||||
var existingEncryption event.EncryptionEventContent
|
|
||||||
var encryptionEnabled bool
|
|
||||||
err := portal.MainIntent().StateEvent(ctx, roomID, event.StateEncryption, "", &existingEncryption)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to check if encryption is enabled in private chat room")
|
|
||||||
} else {
|
|
||||||
encryptionEnabled = existingEncryption.Algorithm == id.AlgorithmMegolmV1
|
|
||||||
}
|
|
||||||
portal.MXID = roomID
|
|
||||||
br.portalsLock.Lock()
|
|
||||||
br.portalsByMXID[portal.MXID] = portal
|
|
||||||
br.portalsLock.Unlock()
|
|
||||||
intent := puppet.DefaultIntent()
|
|
||||||
|
|
||||||
if br.Config.Bridge.Encryption.Default || encryptionEnabled {
|
|
||||||
log.Debug().Msg("Adding bridge bot to new private chat portal as encryption is enabled")
|
|
||||||
_, err = intent.InviteUser(ctx, roomID, &mautrix.ReqInviteUser{UserID: br.Bot.UserID})
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to invite bridge bot to enable e2be")
|
|
||||||
}
|
|
||||||
err = br.Bot.EnsureJoined(ctx, roomID)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to join as bridge bot to enable e2be")
|
|
||||||
}
|
|
||||||
if !encryptionEnabled {
|
|
||||||
_, err = intent.SendStateEvent(ctx, roomID, event.StateEncryption, "", portal.getEncryptionEventContent())
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msg("Failed to enable e2be")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
br.AS.StateStore.SetMembership(ctx, roomID, inviter.MXID, event.MembershipJoin)
|
|
||||||
br.AS.StateStore.SetMembership(ctx, roomID, puppet.MXID, event.MembershipJoin)
|
|
||||||
br.AS.StateStore.SetMembership(ctx, roomID, br.Bot.UserID, event.MembershipJoin)
|
|
||||||
portal.Encrypted = true
|
|
||||||
}
|
|
||||||
portal.UpdateDMInfo(ctx, true)
|
|
||||||
_, _ = intent.SendNotice(ctx, roomID, "Private chat portal created")
|
|
||||||
log.Info().Msg("Created private chat portal after invite")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
br := &SignalBridge{
|
|
||||||
usersByMXID: make(map[id.UserID]*User),
|
|
||||||
usersBySignalID: make(map[uuid.UUID]*User),
|
|
||||||
|
|
||||||
managementRooms: make(map[id.RoomID]*User),
|
|
||||||
|
|
||||||
portalsByMXID: make(map[id.RoomID]*Portal),
|
|
||||||
portalsByID: make(map[database.PortalKey]*Portal),
|
|
||||||
|
|
||||||
puppets: make(map[uuid.UUID]*Puppet),
|
|
||||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
|
||||||
}
|
|
||||||
br.Bridge = bridge.Bridge{
|
|
||||||
Name: "mautrix-signal",
|
|
||||||
URL: "https://github.com/mautrix/signal",
|
|
||||||
Description: "A Matrix-Signal puppeting bridge.",
|
|
||||||
Version: "0.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()
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/bridge/status"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errUserNotConnected = errors.New("you are not connected to Signal")
|
|
||||||
errDifferentUser = errors.New("user is not the recipient of this private chat portal")
|
|
||||||
errUserNotLoggedIn = errors.New("user is not logged in and chat has no relay bot")
|
|
||||||
errRelaybotNotLoggedIn = errors.New("neither user nor relay bot of chat are logged in")
|
|
||||||
errCantRelayReactions = errors.New("user is not logged in and reactions can't be relayed")
|
|
||||||
errMNoticeDisabled = errors.New("bridging m.notice messages is disabled")
|
|
||||||
errUnexpectedParsedContentType = errors.New("unexpected parsed content type")
|
|
||||||
|
|
||||||
errRedactionTargetNotFound = errors.New("redaction target message was not found")
|
|
||||||
errRedactionTargetSentBySomeoneElse = errors.New("redaction target message was sent by someone else")
|
|
||||||
errUnreactTargetSentBySomeoneElse = errors.New("redaction target reaction was sent by someone else")
|
|
||||||
errReactionTargetNotFound = errors.New("reaction target message not found")
|
|
||||||
errEditUnknownTarget = errors.New("unknown edit target message")
|
|
||||||
errFailedToGetEditTarget = errors.New("failed to get edit target message")
|
|
||||||
errEditDifferentSender = errors.New("can't edit message sent by another user")
|
|
||||||
errEditTooOld = errors.New("message is too old to be edited")
|
|
||||||
|
|
||||||
errMessageTakingLong = errors.New("bridging the message is taking longer than usual")
|
|
||||||
errTimeoutBeforeHandling = errors.New("message timed out before handling was started")
|
|
||||||
)
|
|
||||||
|
|
||||||
func errorToStatusReason(err error) (reason event.MessageStatusReason, status event.MessageStatus, isCertain, sendNotice bool, humanMessage string) {
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, errUnexpectedParsedContentType),
|
|
||||||
errors.Is(err, msgconv.ErrUnsupportedMsgType),
|
|
||||||
errors.Is(err, msgconv.ErrInvalidGeoURI):
|
|
||||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, ""
|
|
||||||
case errors.Is(err, errMNoticeDisabled):
|
|
||||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, false, ""
|
|
||||||
case errors.Is(err, errEditDifferentSender),
|
|
||||||
errors.Is(err, errEditTooOld),
|
|
||||||
errors.Is(err, errEditUnknownTarget):
|
|
||||||
return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, err.Error()
|
|
||||||
case errors.Is(err, errTimeoutBeforeHandling):
|
|
||||||
return event.MessageStatusTooOld, event.MessageStatusRetriable, true, true, "the message was too old when it reached the bridge, so it was not handled"
|
|
||||||
case errors.Is(err, context.DeadlineExceeded):
|
|
||||||
return event.MessageStatusTooOld, event.MessageStatusRetriable, false, true, "handling the message took too long and was cancelled"
|
|
||||||
case errors.Is(err, errMessageTakingLong):
|
|
||||||
return event.MessageStatusTooOld, event.MessageStatusPending, false, true, err.Error()
|
|
||||||
case errors.Is(err, errRedactionTargetNotFound),
|
|
||||||
errors.Is(err, errReactionTargetNotFound),
|
|
||||||
errors.Is(err, errRedactionTargetSentBySomeoneElse),
|
|
||||||
errors.Is(err, errUnreactTargetSentBySomeoneElse):
|
|
||||||
return event.MessageStatusGenericError, event.MessageStatusFail, true, false, ""
|
|
||||||
case errors.Is(err, errUserNotConnected):
|
|
||||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, true, ""
|
|
||||||
case errors.Is(err, errUserNotLoggedIn),
|
|
||||||
errors.Is(err, errDifferentUser),
|
|
||||||
errors.Is(err, errRelaybotNotLoggedIn):
|
|
||||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, false, ""
|
|
||||||
default:
|
|
||||||
return event.MessageStatusGenericError, event.MessageStatusRetriable, false, true, ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) sendErrorMessage(ctx context.Context, evt *event.Event, err error, confirmed bool, editID id.EventID) id.EventID {
|
|
||||||
if !portal.bridge.Config.Bridge.MessageErrorNotices {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
certainty := "may not have been"
|
|
||||||
if confirmed {
|
|
||||||
certainty = "was not"
|
|
||||||
}
|
|
||||||
var msgType string
|
|
||||||
switch evt.Type {
|
|
||||||
case event.EventMessage:
|
|
||||||
msgType = "message"
|
|
||||||
case event.EventReaction:
|
|
||||||
msgType = "reaction"
|
|
||||||
case event.EventRedaction:
|
|
||||||
msgType = "redaction"
|
|
||||||
//case TypeMSC3381PollResponse, TypeMSC3381V2PollResponse:
|
|
||||||
// msgType = "poll response"
|
|
||||||
//case TypeMSC3381PollStart:
|
|
||||||
// msgType = "poll start"
|
|
||||||
default:
|
|
||||||
msgType = "unknown event"
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("\u26a0 Your %s %s bridged: %v", msgType, certainty, err)
|
|
||||||
if errors.Is(err, errMessageTakingLong) {
|
|
||||||
msg = fmt.Sprintf("\u26a0 Bridging your %s is taking longer than usual", msgType)
|
|
||||||
}
|
|
||||||
content := &event.MessageEventContent{
|
|
||||||
MsgType: event.MsgNotice,
|
|
||||||
Body: msg,
|
|
||||||
}
|
|
||||||
if editID != "" {
|
|
||||||
content.SetEdit(editID)
|
|
||||||
} else {
|
|
||||||
content.SetReply(evt)
|
|
||||||
}
|
|
||||||
resp, err := portal.sendMainIntentMessage(ctx, content)
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Err(err).Msg("Failed to send bridging error message")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return resp.EventID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) sendStatusEvent(ctx context.Context, evtID, lastRetry id.EventID, err error, deliveredTo *[]id.UserID) {
|
|
||||||
if !portal.bridge.Config.Bridge.MessageStatusEvents {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if lastRetry == evtID {
|
|
||||||
lastRetry = ""
|
|
||||||
}
|
|
||||||
intent := portal.bridge.Bot
|
|
||||||
if !portal.Encrypted {
|
|
||||||
// Bridge bot isn't present in unencrypted DMs
|
|
||||||
intent = portal.MainIntent()
|
|
||||||
}
|
|
||||||
content := event.BeeperMessageStatusEventContent{
|
|
||||||
Network: portal.getBridgeInfoStateKey(),
|
|
||||||
RelatesTo: event.RelatesTo{
|
|
||||||
Type: event.RelReference,
|
|
||||||
EventID: evtID,
|
|
||||||
},
|
|
||||||
DeliveredToUsers: deliveredTo,
|
|
||||||
LastRetry: lastRetry,
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
content.Status = event.MessageStatusSuccess
|
|
||||||
} else {
|
|
||||||
content.Reason, content.Status, _, _, content.Message = errorToStatusReason(err)
|
|
||||||
content.Error = err.Error()
|
|
||||||
}
|
|
||||||
_, err = intent.SendMessageEvent(ctx, portal.MXID, event.BeeperMessageStatus, &content)
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Err(err).Msg("Failed to send message status event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) sendDeliveryReceipt(ctx context.Context, eventID id.EventID) {
|
|
||||||
if portal.bridge.Config.Bridge.DeliveryReceipts {
|
|
||||||
err := portal.bridge.Bot.SendReceipt(ctx, portal.MXID, eventID, event.ReceiptTypeRead, nil)
|
|
||||||
if err != nil {
|
|
||||||
portal.log.Debug().Err(err).Stringer("event_id", eventID).Msg("Failed to send delivery receipt")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (portal *Portal) sendMessageMetrics(ctx context.Context, evt *event.Event, err error, part string, ms *metricSender) {
|
|
||||||
log := portal.log.With().
|
|
||||||
Str("handling_step", part).
|
|
||||||
Str("event_type", evt.Type.String()).
|
|
||||||
Stringer("event_id", evt.ID).
|
|
||||||
Stringer("sender", evt.Sender).
|
|
||||||
Logger()
|
|
||||||
if evt.Type == event.EventRedaction {
|
|
||||||
log = log.With().Stringer("redacts", evt.Redacts).Logger()
|
|
||||||
}
|
|
||||||
ctx = log.WithContext(ctx)
|
|
||||||
|
|
||||||
origEvtID := evt.ID
|
|
||||||
if retryMeta := evt.Content.AsMessage().MessageSendRetry; retryMeta != nil {
|
|
||||||
origEvtID = retryMeta.OriginalEventID
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logEvt := log.Error()
|
|
||||||
if part == "Ignoring" {
|
|
||||||
logEvt = log.Debug()
|
|
||||||
}
|
|
||||||
logEvt.Err(err).Msg("Sending message metrics for event")
|
|
||||||
reason, statusCode, isCertain, sendNotice, _ := errorToStatusReason(err)
|
|
||||||
checkpointStatus := status.ReasonToCheckpointStatus(reason, statusCode)
|
|
||||||
portal.bridge.SendMessageCheckpoint(evt, status.MsgStepRemote, err, checkpointStatus, ms.getRetryNum())
|
|
||||||
if sendNotice {
|
|
||||||
ms.setNoticeID(portal.sendErrorMessage(ctx, evt, err, isCertain, ms.getNoticeID()))
|
|
||||||
}
|
|
||||||
portal.sendStatusEvent(ctx, origEvtID, evt.ID, err, nil)
|
|
||||||
} else {
|
|
||||||
log.Debug().Msg("Sending metrics for successfully handled Matrix event")
|
|
||||||
portal.sendDeliveryReceipt(ctx, evt.ID)
|
|
||||||
portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, ms.getRetryNum())
|
|
||||||
var deliveredTo *[]id.UserID
|
|
||||||
if portal.IsPrivateChat() {
|
|
||||||
deliveredTo = &[]id.UserID{}
|
|
||||||
}
|
|
||||||
portal.sendStatusEvent(ctx, origEvtID, evt.ID, nil, deliveredTo)
|
|
||||||
if prevNotice := ms.popNoticeID(); prevNotice != "" {
|
|
||||||
_, _ = portal.MainIntent().RedactEvent(ctx, portal.MXID, prevNotice, mautrix.ReqRedact{
|
|
||||||
Reason: "error resolved",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ms != nil {
|
|
||||||
log.Debug().Object("timings", ms.timings).Msg("Timings for event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type messageTimings struct {
|
|
||||||
initReceive time.Duration
|
|
||||||
decrypt time.Duration
|
|
||||||
implicitRR time.Duration
|
|
||||||
portalQueue time.Duration
|
|
||||||
totalReceive time.Duration
|
|
||||||
|
|
||||||
preproc time.Duration
|
|
||||||
convert time.Duration
|
|
||||||
totalSend time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func niceRound(dur time.Duration) time.Duration {
|
|
||||||
switch {
|
|
||||||
case dur < time.Millisecond:
|
|
||||||
return dur
|
|
||||||
case dur < time.Second:
|
|
||||||
return dur.Round(100 * time.Microsecond)
|
|
||||||
default:
|
|
||||||
return dur.Round(time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *messageTimings) MarshalZerologObject(evt *zerolog.Event) {
|
|
||||||
evt.
|
|
||||||
Dict("bridge", zerolog.Dict().
|
|
||||||
Stringer("init_receive", niceRound(mt.initReceive)).
|
|
||||||
Stringer("decrypt", niceRound(mt.decrypt)).
|
|
||||||
Stringer("queue", niceRound(mt.portalQueue)).
|
|
||||||
Stringer("total_hs_to_portal", niceRound(mt.totalReceive))).
|
|
||||||
Dict("portal", zerolog.Dict().
|
|
||||||
Stringer("implicit_rr", niceRound(mt.implicitRR)).
|
|
||||||
Stringer("preproc", niceRound(mt.preproc)).
|
|
||||||
Stringer("convert", niceRound(mt.convert)).
|
|
||||||
Stringer("total_send", niceRound(mt.totalSend)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type metricSender struct {
|
|
||||||
portal *Portal
|
|
||||||
previousNotice id.EventID
|
|
||||||
lock sync.Mutex
|
|
||||||
completed bool
|
|
||||||
retryNum int
|
|
||||||
timings *messageTimings
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metricSender) getRetryNum() int {
|
|
||||||
if ms != nil {
|
|
||||||
return ms.retryNum
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metricSender) getNoticeID() id.EventID {
|
|
||||||
if ms == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ms.previousNotice
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metricSender) popNoticeID() id.EventID {
|
|
||||||
if ms == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
evtID := ms.previousNotice
|
|
||||||
ms.previousNotice = ""
|
|
||||||
return evtID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metricSender) setNoticeID(evtID id.EventID) {
|
|
||||||
if ms != nil && ms.previousNotice == "" {
|
|
||||||
ms.previousNotice = evtID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ms *metricSender) sendMessageMetrics(evt *event.Event, err error, part string, completed bool) {
|
|
||||||
ms.lock.Lock()
|
|
||||||
defer ms.lock.Unlock()
|
|
||||||
if !completed && ms.completed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ms.portal.sendMessageMetrics(ms.ctx, evt, err, part, ms)
|
|
||||||
ms.retryNum++
|
|
||||||
ms.completed = completed
|
|
||||||
}
|
|
281
metrics.go
281
metrics.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"runtime/debug"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MetricsHandler struct {
|
|
||||||
db *database.Database
|
|
||||||
server *http.Server
|
|
||||||
log zerolog.Logger
|
|
||||||
|
|
||||||
running bool
|
|
||||||
ctx context.Context
|
|
||||||
stopRecorder func()
|
|
||||||
|
|
||||||
matrixEventHandling *prometheus.HistogramVec
|
|
||||||
signalMessageAge prometheus.Histogram
|
|
||||||
signalMessageHandling *prometheus.HistogramVec
|
|
||||||
countCollection prometheus.Histogram
|
|
||||||
puppetCount prometheus.Gauge
|
|
||||||
userCount prometheus.Gauge
|
|
||||||
messageCount prometheus.Gauge
|
|
||||||
portalCount *prometheus.GaugeVec
|
|
||||||
encryptedGroupCount prometheus.Gauge
|
|
||||||
encryptedPrivateCount prometheus.Gauge
|
|
||||||
unencryptedGroupCount prometheus.Gauge
|
|
||||||
unencryptedPrivateCount prometheus.Gauge
|
|
||||||
|
|
||||||
connected prometheus.Gauge
|
|
||||||
connectedState map[uuid.UUID]bool
|
|
||||||
connectedStateLock sync.Mutex
|
|
||||||
loggedIn prometheus.Gauge
|
|
||||||
loggedInState map[uuid.UUID]bool
|
|
||||||
loggedInStateLock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMetricsHandler(address string, log zerolog.Logger, db *database.Database) *MetricsHandler {
|
|
||||||
portalCount := promauto.NewGaugeVec(prometheus.GaugeOpts{
|
|
||||||
Name: "signal_portals_total",
|
|
||||||
Help: "Number of portal rooms on Matrix",
|
|
||||||
}, []string{"type", "encrypted"})
|
|
||||||
return &MetricsHandler{
|
|
||||||
db: db,
|
|
||||||
server: &http.Server{Addr: address, Handler: promhttp.Handler()},
|
|
||||||
log: log,
|
|
||||||
running: false,
|
|
||||||
|
|
||||||
matrixEventHandling: promauto.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Name: "matrix_event",
|
|
||||||
Help: "Time spent processing Matrix events",
|
|
||||||
}, []string{"event_type"}),
|
|
||||||
signalMessageAge: promauto.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Name: "remote_event_age",
|
|
||||||
Help: "Age of messages received from Signal",
|
|
||||||
Buckets: []float64{1, 2, 3, 5, 7.5, 10, 20, 30, 60},
|
|
||||||
}),
|
|
||||||
signalMessageHandling: promauto.NewHistogramVec(prometheus.HistogramOpts{
|
|
||||||
Name: "remote_event",
|
|
||||||
Help: "Time spent processing Signal messages",
|
|
||||||
}, []string{"message_type"}),
|
|
||||||
countCollection: promauto.NewHistogram(prometheus.HistogramOpts{
|
|
||||||
Name: "signal_count_collection",
|
|
||||||
Help: "Time spent collecting the bridge_*_total metrics",
|
|
||||||
}),
|
|
||||||
puppetCount: promauto.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "signal_puppets_total",
|
|
||||||
Help: "Number of Signal users bridged into Matrix",
|
|
||||||
}),
|
|
||||||
userCount: promauto.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "signal_users_total",
|
|
||||||
Help: "Number of Matrix users using the bridge",
|
|
||||||
}),
|
|
||||||
messageCount: promauto.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "signal_messages_total",
|
|
||||||
Help: "Number of messages bridged",
|
|
||||||
}),
|
|
||||||
portalCount: portalCount,
|
|
||||||
encryptedGroupCount: portalCount.With(prometheus.Labels{"type": "group", "encrypted": "true"}),
|
|
||||||
encryptedPrivateCount: portalCount.With(prometheus.Labels{"type": "private", "encrypted": "true"}),
|
|
||||||
unencryptedGroupCount: portalCount.With(prometheus.Labels{"type": "group", "encrypted": "false"}),
|
|
||||||
unencryptedPrivateCount: portalCount.With(prometheus.Labels{"type": "private", "encrypted": "false"}),
|
|
||||||
|
|
||||||
loggedIn: promauto.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "bridge_logged_in",
|
|
||||||
Help: "Bridge users logged into Signal",
|
|
||||||
}),
|
|
||||||
loggedInState: make(map[uuid.UUID]bool),
|
|
||||||
connected: promauto.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "bridge_connected",
|
|
||||||
Help: "Bridge users connected to Signal",
|
|
||||||
}),
|
|
||||||
connectedState: make(map[uuid.UUID]bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func noop() {}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
|
|
||||||
if !mh.running {
|
|
||||||
return noop
|
|
||||||
}
|
|
||||||
start := time.Now()
|
|
||||||
return func() {
|
|
||||||
duration := time.Since(start)
|
|
||||||
mh.matrixEventHandling.
|
|
||||||
With(prometheus.Labels{"event_type": eventType.Type}).
|
|
||||||
Observe(duration.Seconds())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) TrackSignalMessage(timestamp time.Time, messageType string) func() {
|
|
||||||
if !mh.running {
|
|
||||||
return noop
|
|
||||||
}
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
return func() {
|
|
||||||
duration := time.Since(start)
|
|
||||||
mh.signalMessageHandling.
|
|
||||||
With(prometheus.Labels{"message_type": messageType}).
|
|
||||||
Observe(duration.Seconds())
|
|
||||||
mh.signalMessageAge.Observe(time.Since(timestamp).Seconds())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) TrackLoginState(signalID uuid.UUID, loggedIn bool) {
|
|
||||||
if !mh.running {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mh.loggedInStateLock.Lock()
|
|
||||||
defer mh.loggedInStateLock.Unlock()
|
|
||||||
currentVal, ok := mh.loggedInState[signalID]
|
|
||||||
if !ok || currentVal != loggedIn {
|
|
||||||
mh.loggedInState[signalID] = loggedIn
|
|
||||||
if loggedIn {
|
|
||||||
mh.loggedIn.Inc()
|
|
||||||
} else if ok {
|
|
||||||
mh.loggedIn.Dec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) TrackConnectionState(signalID uuid.UUID, connected bool) {
|
|
||||||
if !mh.running {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mh.connectedStateLock.Lock()
|
|
||||||
defer mh.connectedStateLock.Unlock()
|
|
||||||
currentVal, ok := mh.connectedState[signalID]
|
|
||||||
if !ok || currentVal != connected {
|
|
||||||
mh.connectedState[signalID] = connected
|
|
||||||
if connected {
|
|
||||||
mh.connected.Inc()
|
|
||||||
} else if ok {
|
|
||||||
mh.connected.Dec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) updateStats() {
|
|
||||||
start := time.Now()
|
|
||||||
var puppetCount int
|
|
||||||
err := mh.db.QueryRow(mh.ctx, "SELECT COUNT(*) FROM puppet").Scan(&puppetCount)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Warn().Err(err).Msg("Failed to scan number of puppets")
|
|
||||||
} else {
|
|
||||||
mh.puppetCount.Set(float64(puppetCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
var userCount int
|
|
||||||
err = mh.db.QueryRow(mh.ctx, `SELECT COUNT(*) FROM "user"`).Scan(&userCount)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Warn().Err(err).Msg("Failed to scan number of users:")
|
|
||||||
} else {
|
|
||||||
mh.userCount.Set(float64(userCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageCount int
|
|
||||||
err = mh.db.QueryRow(mh.ctx, "SELECT COUNT(*) FROM message").Scan(&messageCount)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Warn().Err(err).Msg("Failed to scan number of messages")
|
|
||||||
} else {
|
|
||||||
mh.messageCount.Set(float64(messageCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
var encryptedGroupCount, encryptedPrivateCount, unencryptedGroupCount, unencryptedPrivateCount int
|
|
||||||
// TODO Use a more precise way to check if a chat_id is a UUID.
|
|
||||||
// It should also be compatible with both SQLite & Postgres.
|
|
||||||
err = mh.db.QueryRow(mh.ctx, `
|
|
||||||
SELECT
|
|
||||||
COUNT(CASE WHEN chat_id NOT LIKE '%-%-%-%-%' AND encrypted THEN 1 END) AS encrypted_group_portals,
|
|
||||||
COUNT(CASE WHEN chat_id LIKE '%-%-%-%-%' AND encrypted THEN 1 END) AS encrypted_private_portals,
|
|
||||||
COUNT(CASE WHEN chat_id NOT LIKE '%-%-%-%-%' AND NOT encrypted THEN 1 END) AS unencrypted_group_portals,
|
|
||||||
COUNT(CASE WHEN chat_id LIKE '%-%-%-%-%' AND NOT encrypted THEN 1 END) AS unencrypted_private_portals
|
|
||||||
FROM portal WHERE mxid<>''
|
|
||||||
`).Scan(&encryptedGroupCount, &encryptedPrivateCount, &unencryptedGroupCount, &unencryptedPrivateCount)
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Warn().Err(err).Msg("Failed to scan number of portals")
|
|
||||||
} else {
|
|
||||||
mh.encryptedGroupCount.Set(float64(encryptedGroupCount))
|
|
||||||
mh.encryptedPrivateCount.Set(float64(encryptedPrivateCount))
|
|
||||||
mh.unencryptedGroupCount.Set(float64(unencryptedGroupCount))
|
|
||||||
mh.unencryptedPrivateCount.Set(float64(encryptedPrivateCount))
|
|
||||||
}
|
|
||||||
mh.countCollection.Observe(time.Since(start).Seconds())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) startUpdatingStats() {
|
|
||||||
defer func() {
|
|
||||||
r := recover()
|
|
||||||
if r != nil {
|
|
||||||
evt := mh.log.Fatal().Str("stack", string(debug.Stack()))
|
|
||||||
if err, ok := r.(error); ok {
|
|
||||||
evt = evt.Err(err)
|
|
||||||
} else {
|
|
||||||
evt = evt.Any("error", r)
|
|
||||||
}
|
|
||||||
evt.Msg("Panic in metric updater")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ticker := time.Tick(10 * time.Second)
|
|
||||||
for {
|
|
||||||
mh.updateStats()
|
|
||||||
select {
|
|
||||||
case <-mh.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) Start() {
|
|
||||||
mh.running = true
|
|
||||||
mh.ctx, mh.stopRecorder = context.WithCancel(context.Background())
|
|
||||||
go mh.startUpdatingStats()
|
|
||||||
err := mh.server.ListenAndServe()
|
|
||||||
mh.running = false
|
|
||||||
if err != nil && err != http.ErrServerClosed {
|
|
||||||
mh.log.Fatal().Err(err).Msg("Error in metrics listener")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mh *MetricsHandler) Stop() {
|
|
||||||
if !mh.running {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mh.stopRecorder()
|
|
||||||
err := mh.server.Close()
|
|
||||||
if err != nil {
|
|
||||||
mh.log.Err(err).Msg("Error closing metrics listener")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ const PrivateChatTopic = "Signal private chat"
|
||||||
const NoteToSelfName = "Signal Note to Self"
|
const NoteToSelfName = "Signal Note to Self"
|
||||||
|
|
||||||
func (s *SignalClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// createChat is a no-op: chats don't need to be created, and we always return chat info
|
||||||
if aci != uuid.Nil {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ghost: %w", err)
|
return nil, fmt.Errorf("failed to get ghost: %w", err)
|
||||||
}
|
}
|
||||||
return &bridgev2.ResolveIdentifierResponse{
|
return &bridgev2.ResolveIdentifierResponse{
|
||||||
UserID: makeUserID(aci),
|
UserID: signalid.MakeUserID(aci),
|
||||||
UserInfo: s.contactToUserInfo(recipient),
|
UserInfo: s.contactToUserInfo(recipient),
|
||||||
Ghost: ghost,
|
Ghost: ghost,
|
||||||
Chat: s.makeCreateDMResponse(recipient),
|
Chat: s.makeCreateDMResponse(recipient),
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
} else {
|
||||||
return &bridgev2.ResolveIdentifierResponse{
|
return &bridgev2.ResolveIdentifierResponse{
|
||||||
UserID: makeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
|
UserID: signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(pni)),
|
||||||
UserInfo: s.contactToUserInfo(recipient),
|
UserInfo: s.contactToUserInfo(recipient),
|
||||||
Chat: s.makeCreateDMResponse(recipient),
|
Chat: s.makeCreateDMResponse(recipient),
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -179,14 +180,14 @@ func (s *SignalClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveI
|
||||||
Chat: s.makeCreateDMResponse(recipient),
|
Chat: s.makeCreateDMResponse(recipient),
|
||||||
}
|
}
|
||||||
if recipient.ACI != uuid.Nil {
|
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)
|
ghost, err := s.Main.Bridge.GetGhostByID(ctx, recipientResp.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ghost for %s: %w", recipient.ACI, err)
|
return nil, fmt.Errorf("failed to get ghost for %s: %w", recipient.ACI, err)
|
||||||
}
|
}
|
||||||
recipientResp.Ghost = ghost
|
recipientResp.Ghost = ghost
|
||||||
} else {
|
} else {
|
||||||
recipientResp.UserID = makeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
|
recipientResp.UserID = signalid.MakeUserIDFromServiceID(libsignalgo.NewPNIServiceID(recipient.PNI))
|
||||||
}
|
}
|
||||||
resp[i] = recipientResp
|
resp[i] = recipientResp
|
||||||
}
|
}
|
||||||
|
@ -215,7 +216,7 @@ func (s *SignalClient) makeCreateDMResponse(recipient *types.Recipient) *bridgev
|
||||||
name = s.Main.Config.FormatDisplayname(recipient)
|
name = s.Main.Config.FormatDisplayname(recipient)
|
||||||
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
|
serviceID = libsignalgo.NewPNIServiceID(recipient.PNI)
|
||||||
} else {
|
} else {
|
||||||
members.OtherUserID = makeUserID(recipient.ACI)
|
members.OtherUserID = signalid.MakeUserID(recipient.ACI)
|
||||||
if recipient.ACI == s.Client.Store.ACI {
|
if recipient.ACI == s.Client.Store.ACI {
|
||||||
name = NoteToSelfName
|
name = NoteToSelfName
|
||||||
avatar = &bridgev2.Avatar{
|
avatar = &bridgev2.Avatar{
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
"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 {
|
if s.Client == nil {
|
||||||
return false
|
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) {
|
func (s *SignalClient) bridgeStateLoop(statusChan <-chan signalmeow.SignalConnectionStatus) {
|
||||||
|
|
|
@ -20,19 +20,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"go.mau.fi/util/dbutil"
|
"go.mau.fi/util/dbutil"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv"
|
"go.mau.fi/mautrix-signal/pkg/msgconv"
|
||||||
"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"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
"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.Store = store.NewStore(bridge.DB.Database, dbutil.ZeroLogger(bridge.Log.With().Str("db_section", "signalmeow").Logger()))
|
||||||
s.Bridge = bridge
|
s.Bridge = bridge
|
||||||
s.MsgConv = &msgconv.MessageConverter{
|
s.MsgConv = msgconv.NewMessageConverter(bridge, s.Config.LocationFormat)
|
||||||
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")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
|
func (s *SignalConnector) SetMaxFileSize(maxSize int64) {
|
||||||
|
|
|
@ -18,26 +18,20 @@ package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
"maunium.net/go/mautrix/bridgev2/database"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *SignalConnector) GetDBMetaTypes() database.MetaTypes {
|
func (s *SignalConnector) GetDBMetaTypes() database.MetaTypes {
|
||||||
return database.MetaTypes{
|
return database.MetaTypes{
|
||||||
Portal: func() any {
|
Portal: func() any {
|
||||||
return &PortalMetadata{}
|
return &signalid.PortalMetadata{}
|
||||||
},
|
},
|
||||||
Ghost: nil,
|
Ghost: nil,
|
||||||
Message: func() any {
|
Message: func() any {
|
||||||
return &MessageMetadata{}
|
return &signalid.MessageMetadata{}
|
||||||
},
|
},
|
||||||
Reaction: nil,
|
Reaction: nil,
|
||||||
UserLogin: nil,
|
UserLogin: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortalMetadata struct {
|
|
||||||
Revision uint32 `json:"revision"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageMetadata struct {
|
|
||||||
ContainsAttachments bool `json:"contains_attachments,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
|
"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 {
|
func makeRevisionUpdater(rev uint32) func(ctx context.Context, portal *bridgev2.Portal) bool {
|
||||||
return 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 {
|
if meta.Revision < rev {
|
||||||
meta.Revision = rev
|
meta.Revision = rev
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -32,12 +32,13 @@ import (
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
|
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
|
||||||
userID, groupID, err := parsePortalID(portalID)
|
userID, groupID, err := signalid.ParsePortalID(portalID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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) {
|
func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
|
||||||
mcCtx := &msgconvContext{
|
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -94,10 +87,10 @@ func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Ma
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dbMsg := &database.Message{
|
dbMsg := &database.Message{
|
||||||
ID: makeMessageID(s.Client.Store.ACI, converted.GetTimestamp()),
|
ID: signalid.MakeMessageID(s.Client.Store.ACI, converted.GetTimestamp()),
|
||||||
SenderID: makeUserID(s.Client.Store.ACI),
|
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||||
Timestamp: time.UnixMilli(int64(converted.GetTimestamp())),
|
Timestamp: time.UnixMilli(int64(converted.GetTimestamp())),
|
||||||
Metadata: &MessageMetadata{
|
Metadata: &signalid.MessageMetadata{
|
||||||
ContainsAttachments: len(converted.Attachments) > 0,
|
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 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
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")
|
return fmt.Errorf("cannot edit other people's messages")
|
||||||
}
|
}
|
||||||
mcCtx := &msgconvContext{
|
var replyTo *database.Message
|
||||||
Connector: s.Main,
|
|
||||||
Intent: nil,
|
|
||||||
Client: s,
|
|
||||||
Portal: msg.Portal,
|
|
||||||
}
|
|
||||||
if msg.EditTarget.ReplyTo.MessageID != "" {
|
if msg.EditTarget.ReplyTo.MessageID != "" {
|
||||||
var err error
|
replyTo, err = s.Main.Bridge.DB.Message.GetFirstOrSpecificPartByID(ctx, msg.Portal.Receiver, msg.EditTarget.ReplyTo)
|
||||||
mcCtx.ReplyTo, err = s.Main.Bridge.DB.Message.GetFirstOrSpecificPartByID(ctx, msg.Portal.Receiver, msg.EditTarget.ReplyTo)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get message reply target: %w", err)
|
return fmt.Errorf("failed to get message reply target: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, msgconvContextKey, mcCtx)
|
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
|
||||||
converted, err := s.Main.MsgConv.ToSignal(ctx, msg.Event, msg.Content, msg.OrigSender != nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -138,22 +124,22 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
msg.EditTarget.ID = makeMessageID(s.Client.Store.ACI, converted.GetTimestamp())
|
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, converted.GetTimestamp())
|
||||||
msg.EditTarget.Metadata = &MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
|
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
|
||||||
msg.EditTarget.EditCount++
|
msg.EditTarget.EditCount++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||||
return bridgev2.MatrixReactionPreResponse{
|
return bridgev2.MatrixReactionPreResponse{
|
||||||
SenderID: makeUserID(s.Client.Store.ACI),
|
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
|
||||||
EmojiID: "",
|
EmojiID: "",
|
||||||
Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key),
|
Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
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 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
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 {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
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")
|
return fmt.Errorf("cannot delete other people's messages")
|
||||||
}
|
}
|
||||||
wrappedContent := &signalpb.Content{
|
wrappedContent := &signalpb.Content{
|
||||||
|
@ -237,7 +223,7 @@ func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bri
|
||||||
}
|
}
|
||||||
messagesToRead := map[uuid.UUID][]uint64{}
|
messagesToRead := map[uuid.UUID][]uint64{}
|
||||||
for _, msg := range dbMessages {
|
for _, msg := range dbMessages {
|
||||||
userID, timestamp, err := parseMessageID(msg.ID)
|
userID, timestamp, err := signalid.ParseMessageID(msg.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse message ID %q: %w", msg.ID, err)
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
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) {
|
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 == "" {
|
if err != nil || groupID == "" {
|
||||||
return false, err
|
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)
|
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -316,7 +302,7 @@ func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev
|
||||||
if postUpdatePortal != nil {
|
if postUpdatePortal != nil {
|
||||||
postUpdatePortal()
|
postUpdatePortal()
|
||||||
}
|
}
|
||||||
portal.Metadata.(*PortalMetadata).Revision = revision
|
portal.Metadata.(*signalid.PortalMetadata).Revision = revision
|
||||||
return true, nil
|
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) {
|
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 == "" {
|
if err != nil || groupID == "" {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-signal/pkg/signalid"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
|
"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
|
||||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||||
)
|
)
|
||||||
|
@ -63,7 +64,7 @@ func (s *SignalClient) wrapCallEvent(evt *events.Call) bridgev2.RemoteMessage {
|
||||||
PortalKey: s.makePortalKey(evt.Info.ChatID),
|
PortalKey: s.makePortalKey(evt.Info.ChatID),
|
||||||
Data: evt,
|
Data: evt,
|
||||||
CreatePortal: true,
|
CreatePortal: true,
|
||||||
ID: makeMessageID(evt.Info.Sender, evt.Timestamp),
|
ID: signalid.MakeMessageID(evt.Info.Sender, evt.Timestamp),
|
||||||
Sender: s.makeEventSender(evt.Info.Sender),
|
Sender: s.makeEventSender(evt.Info.Sender),
|
||||||
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
Timestamp: time.UnixMilli(int64(evt.Timestamp)),
|
||||||
ConvertMessageFunc: convertCallEvent,
|
ConvertMessageFunc: convertCallEvent,
|
||||||
|
@ -76,7 +77,7 @@ func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridg
|
||||||
}
|
}
|
||||||
if data.IsRinging {
|
if data.IsRinging {
|
||||||
content.Body = "Incoming call"
|
content.Body = "Incoming call"
|
||||||
if userID, _, _ := parsePortalID(portal.ID); !userID.IsEmpty() {
|
if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
|
||||||
content.MsgType = event.MsgText
|
content.MsgType = event.MsgText
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -152,7 +153,7 @@ func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal)
|
||||||
if !ok || dataMsg.GroupV2 == nil {
|
if !ok || dataMsg.GroupV2 == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
portalRev := portal.Metadata.(*PortalMetadata).Revision
|
portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision
|
||||||
if evt.Info.GroupRevision > portalRev {
|
if evt.Info.GroupRevision > portalRev {
|
||||||
toRevision := evt.Info.GroupRevision
|
toRevision := evt.Info.GroupRevision
|
||||||
if dataMsg.GetGroupV2().GetGroupChange() != nil {
|
if dataMsg.GetGroupV2().GetGroupChange() != nil {
|
||||||
|
@ -206,7 +207,7 @@ func (evt *Bv2ChatEvent) GetID() networkid.MessageID {
|
||||||
if ts == 0 {
|
if ts == 0 {
|
||||||
panic(fmt.Errorf("GetID() called for non-DataMessage event"))
|
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 {
|
func (evt *Bv2ChatEvent) getDataMsgTimestamp() uint64 {
|
||||||
|
@ -251,7 +252,7 @@ func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
|
||||||
if targetAuthorACI != "" {
|
if targetAuthorACI != "" {
|
||||||
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
|
targetAuthorUUID, _ = uuid.Parse(targetAuthorACI)
|
||||||
}
|
}
|
||||||
return makeMessageID(targetAuthorUUID, targetSentTS)
|
return signalid.MakeMessageID(targetAuthorUUID, targetSentTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
|
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) {
|
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)
|
dataMsg, ok := evt.Event.(*signalpb.DataMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
|
return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
|
||||||
}
|
}
|
||||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, dataMsg)
|
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, dataMsg)
|
||||||
converted.MergeCaption()
|
if converted.Disappear.Type != "" {
|
||||||
var replyTo *networkid.MessageOptionalPartID
|
evtTS := evt.GetTimestamp()
|
||||||
if dataMsg.GetQuote() != nil {
|
portal.UpdateDisappearingSetting(ctx, converted.Disappear, nil, evtTS, true, true)
|
||||||
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()))
|
|
||||||
if evt.Info.Sender == evt.s.Client.Store.ACI {
|
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{
|
return converted, nil
|
||||||
ReplyTo: replyTo,
|
|
||||||
Parts: convertedParts,
|
|
||||||
Disappear: disappear,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
|
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)
|
editMsg, ok := evt.Event.(*signalpb.EditMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
|
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
|
||||||
}
|
}
|
||||||
// TODO tell converter about existing parts to avoid reupload?
|
// TODO tell converter about existing parts to avoid reupload?
|
||||||
converted := evt.s.Main.MsgConv.ToMatrix(ctx, editMsg.GetDataMessage())
|
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, intent, editMsg.GetDataMessage())
|
||||||
converted.MergeCaption()
|
|
||||||
convertedEdit := &bridgev2.ConvertedEdit{}
|
|
||||||
// TODO can anything other than the text be edited?
|
// TODO can anything other than the text be edited?
|
||||||
lastPart := converted.Parts[len(converted.Parts)-1]
|
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
|
||||||
convertedEdit.ModifiedParts = append(convertedEdit.ModifiedParts, &bridgev2.ConvertedEditPart{
|
editPart.Part.EditCount++
|
||||||
Part: existing[len(existing)-1],
|
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
|
||||||
Type: lastPart.Type,
|
return &bridgev2.ConvertedEdit{
|
||||||
Content: lastPart.Content,
|
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
|
||||||
Extra: lastPart.Extra,
|
}, nil
|
||||||
})
|
|
||||||
convertedEdit.ModifiedParts[0].Part.EditCount++
|
|
||||||
convertedEdit.ModifiedParts[0].Part.ID = makeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
|
|
||||||
return convertedEdit, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Bv2Receipt struct {
|
type Bv2Receipt struct {
|
||||||
|
@ -438,7 +392,7 @@ func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) {
|
||||||
Logger()
|
Logger()
|
||||||
ctx := log.WithContext(context.TODO())
|
ctx := log.WithContext(context.TODO())
|
||||||
receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
|
receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
|
||||||
return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, 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)
|
s.dispatchReceipts(evt.Sender, evt.Content.GetType(), receipts)
|
||||||
}
|
}
|
||||||
|
@ -453,7 +407,7 @@ func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
s.dispatchReceipts(s.Client.Store.ACI, signalpb.ReceiptMessage_READ, receipts)
|
||||||
}
|
}
|
||||||
|
@ -495,7 +449,7 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fullContact.ContactAvatar = contact.ContactAvatar
|
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 {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to get ghost to update contact info")
|
log.Err(err).Msg("Failed to get ghost to update contact info")
|
||||||
continue
|
continue
|
||||||
|
@ -510,7 +464,7 @@ func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
|
||||||
func (s *SignalClient) updateRemoteProfile(ctx context.Context, resendState bool) {
|
func (s *SignalClient) updateRemoteProfile(ctx context.Context, resendState bool) {
|
||||||
var err error
|
var err error
|
||||||
if s.Ghost == nil {
|
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 {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost for remote profile update")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost for remote profile update")
|
||||||
return
|
return
|
||||||
|
|
|
@ -17,71 +17,14 @@
|
||||||
package connector
|
package connector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
|
"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 {
|
func (s *SignalClient) makePortalKey(chatID string) networkid.PortalKey {
|
||||||
key := networkid.PortalKey{ID: networkid.PortalID(chatID)}
|
key := networkid.PortalKey{ID: networkid.PortalID(chatID)}
|
||||||
// For non-group chats, add receiver
|
// 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 {
|
func (s *SignalClient) makeDMPortalKey(serviceID libsignalgo.ServiceID) networkid.PortalKey {
|
||||||
return networkid.PortalKey{
|
return networkid.PortalKey{
|
||||||
ID: makeDMPortalID(serviceID),
|
ID: signalid.MakeDMPortalID(serviceID),
|
||||||
Receiver: s.UserLogin.ID,
|
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 {
|
func (s *SignalClient) makeEventSender(sender uuid.UUID) bridgev2.EventSender {
|
||||||
return bridgev2.EventSender{
|
return bridgev2.EventSender{
|
||||||
IsFromMe: sender == s.Client.Store.ACI,
|
IsFromMe: sender == s.Client.Store.ACI,
|
||||||
SenderLogin: makeUserLoginID(sender),
|
SenderLogin: signalid.MakeUserLoginID(sender),
|
||||||
Sender: makeUserID(sender),
|
Sender: signalid.MakeUserID(sender),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMessagePartID(index int) networkid.PartID {
|
|
||||||
if index == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return networkid.PartID(strconv.Itoa(index))
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"maunium.net/go/mautrix/bridgev2"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/bridgev2/database"
|
"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"
|
||||||
"go.mau.fi/mautrix-signal/pkg/signalmeow/store"
|
"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) {
|
func (qr *QRLogin) processingWait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||||
defer qr.cancelChan()
|
defer qr.cancelChan()
|
||||||
newLoginID := makeUserLoginID(qr.ProvData.ACI)
|
newLoginID := signalid.MakeUserLoginID(qr.ProvData.ACI)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case resp := <-qr.ProvChan:
|
case resp := <-qr.ProvChan:
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,35 +18,37 @@ package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"go.mau.fi/util/exerrors"
|
|
||||||
"go.mau.fi/util/exmime"
|
"go.mau.fi/util/exmime"
|
||||||
"go.mau.fi/util/ffmpeg"
|
"go.mau.fi/util/ffmpeg"
|
||||||
"go.mau.fi/util/variationselector"
|
"go.mau.fi/util/variationselector"
|
||||||
"golang.org/x/exp/constraints"
|
"golang.org/x/exp/constraints"
|
||||||
"google.golang.org/protobuf/proto"
|
"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/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"
|
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (mc *MessageConverter) ToSignal(
|
||||||
ErrUnsupportedMsgType = errors.New("unsupported msgtype")
|
ctx context.Context,
|
||||||
ErrMediaDownloadFailed = errors.New("failed to download media")
|
client *signalmeow.Client,
|
||||||
ErrMediaDecryptFailed = errors.New("failed to decrypt media")
|
portal *bridgev2.Portal,
|
||||||
ErrMediaConvertFailed = errors.New("failed to convert")
|
evt *event.Event,
|
||||||
ErrMediaUploadFailed = errors.New("failed to upload media")
|
content *event.MessageEventContent,
|
||||||
ErrInvalidGeoURI = errors.New("invalid `geo:` URI in message")
|
relaybotFormatted bool,
|
||||||
)
|
replyTo *database.Message,
|
||||||
|
) (*signalpb.DataMessage, error) {
|
||||||
func (mc *MessageConverter) ToSignal(ctx context.Context, evt *event.Event, content *event.MessageEventContent, relaybotFormatted bool) (*signalpb.DataMessage, error) {
|
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||||
|
ctx = context.WithValue(ctx, contextKeyPortal, portal)
|
||||||
if evt.Type == event.EventSticker {
|
if evt.Type == event.EventSticker {
|
||||||
content.MsgType = event.MessageType(event.EventSticker.Type)
|
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{
|
dm := &signalpb.DataMessage{
|
||||||
Timestamp: &ts,
|
Timestamp: &ts,
|
||||||
Quote: mc.GetSignalReply(ctx, content),
|
Preview: mc.convertURLPreviewToSignal(ctx, content),
|
||||||
Preview: mc.convertURLPreviewToSignal(ctx, evt),
|
|
||||||
}
|
}
|
||||||
if expirationTime := mc.GetData(ctx).ExpirationTime; expirationTime != 0 {
|
if replyTo != nil {
|
||||||
dm.ExpireTimer = proto.Uint32(uint32(expirationTime))
|
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 {
|
if content.MsgType == event.MsgEmote && !relaybotFormatted {
|
||||||
content.Body = "/me " + content.Body
|
content.Body = "/me " + content.Body
|
||||||
|
@ -113,13 +127,13 @@ func (mc *MessageConverter) ToSignal(ctx context.Context, evt *event.Event, cont
|
||||||
case event.MsgLocation:
|
case event.MsgLocation:
|
||||||
lat, lon, err := parseGeoURI(content.GeoURI)
|
lat, lon, err := parseGeoURI(content.GeoURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Invalid geo URI")
|
zerolog.Ctx(ctx).Err(err).Msg("Invalid geo URI")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
locationString := fmt.Sprintf(mc.LocationFormat, lat, lon)
|
locationString := fmt.Sprintf(mc.LocationFormat, lat, lon)
|
||||||
dm.Body = &locationString
|
dm.Body = &locationString
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w %s", ErrUnsupportedMsgType, content.MsgType)
|
return nil, fmt.Errorf("%w %s", bridgev2.ErrUnsupportedMessageType, content.MsgType)
|
||||||
}
|
}
|
||||||
return dm, nil
|
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) {
|
func (mc *MessageConverter) convertFileToSignal(ctx context.Context, evt *event.Event, content *event.MessageEventContent) (*signalpb.AttachmentPointer, error) {
|
||||||
log := zerolog.Ctx(ctx)
|
log := zerolog.Ctx(ctx)
|
||||||
mxc := content.URL
|
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||||
if content.File != nil {
|
|
||||||
mxc = content.File.URL
|
|
||||||
}
|
|
||||||
data, err := mc.DownloadMatrixMedia(ctx, mxc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, exerrors.NewDualError(ErrMediaDownloadFailed, err)
|
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||||
}
|
|
||||||
if content.File != nil {
|
|
||||||
err = content.File.DecryptInPlace(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, exerrors.NewDualError(ErrMediaDecryptFailed, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fileName := content.Body
|
fileName := content.Body
|
||||||
if content.FileName != "" {
|
if content.FileName != "" {
|
||||||
fileName = content.FileName
|
fileName = content.FileName
|
||||||
}
|
}
|
||||||
_, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"]
|
|
||||||
mime := content.GetInfo().MimeType
|
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)
|
data, err = ffmpeg.ConvertBytes(ctx, data, ".m4a", []string{}, []string{"-c:a", "aac"}, mime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mime = "audio/aac"
|
mime = "audio/aac"
|
||||||
fileName += ".m4a"
|
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 {
|
switch mime {
|
||||||
case "image/webp", "image/png", "image/apng":
|
case "image/webp", "image/png", "image/apng":
|
||||||
// allowed
|
// allowed
|
||||||
case "image/gif":
|
case "image/gif":
|
||||||
if !mc.ConvertGIFToAPNG {
|
if !ffmpeg.Supported() {
|
||||||
return nil, fmt.Errorf("converting gif stickers is not supported")
|
return nil, fmt.Errorf("converting gif stickers is not supported")
|
||||||
}
|
}
|
||||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".apng", []string{}, []string{}, mime)
|
data, err = ffmpeg.ConvertBytes(ctx, data, ".apng", []string{}, []string{}, mime)
|
||||||
if err != nil {
|
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"
|
fileName += ".apng"
|
||||||
mime = "image/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)
|
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 {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to upload file")
|
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.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE))
|
||||||
}
|
}
|
||||||
att.ContentType = proto.String(mime)
|
att.ContentType = proto.String(mime)
|
|
@ -26,49 +26,22 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-vcard"
|
"github.com/emersion/go-vcard"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.mau.fi/util/exfmt"
|
"go.mau.fi/util/exfmt"
|
||||||
"go.mau.fi/util/exmime"
|
"go.mau.fi/util/exmime"
|
||||||
"go.mau.fi/util/ffmpeg"
|
"go.mau.fi/util/ffmpeg"
|
||||||
"golang.org/x/exp/slices"
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
"maunium.net/go/mautrix/crypto/attachment"
|
"maunium.net/go/mautrix/bridgev2/database"
|
||||||
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||||
"maunium.net/go/mautrix/event"
|
"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"
|
"go.mau.fi/mautrix-signal/pkg/signalmeow"
|
||||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
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 {
|
func calculateLength(dm *signalpb.DataMessage) int {
|
||||||
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
||||||
return 1
|
return 1
|
||||||
|
@ -96,19 +69,30 @@ func CanConvertSignal(dm *signalpb.DataMessage) bool {
|
||||||
return calculateLength(dm) > 0
|
return calculateLength(dm) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) ToMatrix(ctx context.Context, dm *signalpb.DataMessage) *ConvertedMessage {
|
func (mc *MessageConverter) ToMatrix(
|
||||||
cm := &ConvertedMessage{
|
ctx context.Context,
|
||||||
Timestamp: dm.GetTimestamp(),
|
client *signalmeow.Client,
|
||||||
DisappearIn: dm.GetExpireTimer(),
|
portal *bridgev2.Portal,
|
||||||
Parts: make([]*ConvertedMessagePart, 0, calculateLength(dm)),
|
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 {
|
if dm.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0 {
|
||||||
cm.Parts = append(cm.Parts, mc.ConvertDisappearingTimerChangeToMatrix(ctx, dm.GetExpireTimer(), true))
|
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
|
// Don't allow any other parts in a disappearing timer change message
|
||||||
return cm
|
return cm
|
||||||
}
|
}
|
||||||
|
if dm.GetExpireTimer() > 0 {
|
||||||
|
cm.Disappear.Type = database.DisappearingTypeAfterRead
|
||||||
|
cm.Disappear.Timer = time.Duration(dm.GetExpireTimer()) * time.Second
|
||||||
|
}
|
||||||
if dm.Sticker != nil {
|
if dm.Sticker != nil {
|
||||||
cm.Parts = append(cm.Parts, mc.convertStickerToMatrix(ctx, dm.Sticker))
|
cm.Parts = append(cm.Parts, mc.convertStickerToMatrix(ctx, dm.Sticker))
|
||||||
// Don't allow any other parts in a sticker message
|
// 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))
|
cm.Parts = append(cm.Parts, mc.convertTextToMatrix(ctx, dm))
|
||||||
}
|
}
|
||||||
if len(cm.Parts) == 0 && dm.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT) {
|
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,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -147,23 +131,28 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, dm *signalpb.DataMessa
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
replyTo, sender := mc.GetMatrixReply(ctx, dm.Quote)
|
cm.MergeCaption()
|
||||||
for _, part := range cm.Parts {
|
for i, part := range cm.Parts {
|
||||||
if part.Content.Mentions == nil {
|
part.ID = signalid.MakeMessagePartID(i)
|
||||||
part.Content.Mentions = &event.Mentions{}
|
part.DBMetadata = &signalid.MessageMetadata{
|
||||||
|
ContainsAttachments: len(dm.GetAttachments()) > 0,
|
||||||
}
|
}
|
||||||
if replyTo != "" {
|
}
|
||||||
part.Content.RelatesTo = (&event.RelatesTo{}).SetReplyTo(replyTo)
|
if dm.Quote != nil {
|
||||||
if !slices.Contains(part.Content.Mentions.UserIDs, sender) {
|
authorACI, err := uuid.Parse(dm.Quote.GetAuthorAci())
|
||||||
part.Content.Mentions.UserIDs = append(part.Content.Mentions.UserIDs, sender)
|
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
|
return cm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.Context, timer uint32, updatePortal bool) *ConvertedMessagePart {
|
func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.Context, timer uint32, updatePortal bool) *bridgev2.ConvertedMessagePart {
|
||||||
part := &ConvertedMessagePart{
|
part := &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -174,35 +163,36 @@ func (mc *MessageConverter) ConvertDisappearingTimerChangeToMatrix(ctx context.C
|
||||||
part.Content.Body = "Disappearing messages disabled"
|
part.Content.Body = "Disappearing messages disabled"
|
||||||
}
|
}
|
||||||
if updatePortal {
|
if updatePortal {
|
||||||
if mc.UpdateDisappearing != nil {
|
portal := getPortal(ctx)
|
||||||
mc.UpdateDisappearing(ctx, time.Duration(timer)*time.Second)
|
portal.Disappear.Timer = time.Duration(timer) * time.Second
|
||||||
|
if timer == 0 {
|
||||||
|
portal.Disappear.Type = ""
|
||||||
} else {
|
} else {
|
||||||
portal := mc.GetData(ctx)
|
portal.Disappear.Type = database.DisappearingTypeAfterRead
|
||||||
portal.ExpirationTime = timer
|
}
|
||||||
err := portal.Update(ctx)
|
err := portal.Save(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update portal disappearing timer in database")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to update portal disappearing timer in database")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return part
|
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)
|
content := signalfmt.Parse(ctx, dm.GetBody(), dm.GetBodyRanges(), mc.SignalFmtParams)
|
||||||
extra := map[string]any{}
|
extra := map[string]any{}
|
||||||
if len(dm.Preview) > 0 {
|
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,
|
Type: event.EventMessage,
|
||||||
Content: content,
|
Content: content,
|
||||||
Extra: extra,
|
Extra: extra,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *signalpb.DataMessage_Payment) *ConvertedMessagePart {
|
func (mc *MessageConverter) convertPaymentToMatrix(_ context.Context, payment *signalpb.DataMessage_Payment) *bridgev2.ConvertedMessagePart {
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
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 {
|
func (mc *MessageConverter) convertGiftBadgeToMatrix(_ context.Context, giftBadge *signalpb.DataMessage_GiftBadge) *bridgev2.ConvertedMessagePart {
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -303,7 +293,7 @@ func (mc *MessageConverter) convertContactToVCard(ctx context.Context, contact *
|
||||||
return card
|
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)
|
card := mc.convertContactToVCard(ctx, contact)
|
||||||
contact.Avatar = nil
|
contact.Avatar = nil
|
||||||
extraData := map[string]any{
|
extraData := map[string]any{
|
||||||
|
@ -313,7 +303,7 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
|
||||||
err := vcard.NewEncoder(&buf).Encode(card)
|
err := vcard.NewEncoder(&buf).Encode(card)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to encode vCard")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to encode vCard")
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -323,30 +313,6 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data := buf.Bytes()
|
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()
|
displayName := contact.GetName().GetDisplayName()
|
||||||
if displayName == "" {
|
if displayName == "" {
|
||||||
displayName = contact.GetName().GetGivenName()
|
displayName = contact.GetName().GetGivenName()
|
||||||
|
@ -368,24 +334,30 @@ func (mc *MessageConverter) convertContactToMatrix(ctx context.Context, contact
|
||||||
Size: len(data),
|
Size: len(data),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if file != nil {
|
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, content.Info.MimeType, content.Body)
|
||||||
file.URL = mxc
|
if err != nil {
|
||||||
content.File = file
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to upload vCard")
|
||||||
} else {
|
return &bridgev2.ConvertedMessagePart{
|
||||||
content.URL = mxc
|
Type: event.EventMessage,
|
||||||
|
Content: &event.MessageEventContent{
|
||||||
|
MsgType: event.MsgNotice,
|
||||||
|
Body: "Failed to upload vCard",
|
||||||
|
},
|
||||||
|
Extra: extraData,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: content,
|
Content: content,
|
||||||
Extra: extraData,
|
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)
|
part, err := mc.reuploadAttachment(ctx, att)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Int("attachment_index", index).Msg("Failed to handle attachment")
|
zerolog.Ctx(ctx).Err(err).Int("attachment_index", index).Msg("Failed to handle attachment")
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -396,11 +368,11 @@ func (mc *MessageConverter) convertAttachmentToMatrix(ctx context.Context, index
|
||||||
return part
|
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())
|
converted, err := mc.reuploadAttachment(ctx, sticker.GetData())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to handle sticker")
|
||||||
return &ConvertedMessagePart{
|
return &bridgev2.ConvertedMessagePart{
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: &event.MessageEventContent{
|
Content: &event.MessageEventContent{
|
||||||
MsgType: event.MsgNotice,
|
MsgType: event.MsgNotice,
|
||||||
|
@ -437,7 +409,7 @@ func (mc *MessageConverter) downloadSignalLongText(ctx context.Context, att *sig
|
||||||
return &longBody, nil
|
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)
|
data, err := signalmeow.DownloadAttachment(ctx, att)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to download attachment: %w", err)
|
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)
|
mimeType = http.DetectContentType(data)
|
||||||
}
|
}
|
||||||
fileName := att.GetFileName()
|
fileName := att.GetFileName()
|
||||||
extra := map[string]any{}
|
content := &event.MessageEventContent{
|
||||||
if mc.ConvertVoiceMessages && att.GetFlags()&uint32(signalpb.AttachmentPointer_VOICE_MESSAGE) != 0 {
|
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)
|
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
|
return nil, fmt.Errorf("failed to convert audio to ogg/opus: %w", err)
|
||||||
}
|
}
|
||||||
fileName += ".ogg"
|
fileName += ".ogg"
|
||||||
mimeType = "audio/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
|
// 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
|
content.URL, content.File, err = getIntent(ctx).UploadMedia(ctx, getPortal(ctx).MXID, data, fileName, mimeType)
|
||||||
uploadMime := mimeType
|
|
||||||
uploadFileName := fileName
|
|
||||||
if mc.GetData(ctx).Encrypted {
|
|
||||||
file = &event.EncryptedFileInfo{
|
|
||||||
EncryptedFile: *attachment.NewEncryptedFile(),
|
|
||||||
URL: "",
|
|
||||||
}
|
|
||||||
file.EncryptInPlace(data)
|
|
||||||
uploadMime = "application/octet-stream"
|
|
||||||
uploadFileName = ""
|
|
||||||
}
|
|
||||||
mxc, err := mc.UploadMatrixMedia(ctx, data, uploadFileName, uploadMime)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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() != "" {
|
if att.GetBlurHash() != "" {
|
||||||
content.Info.Blurhash = att.GetBlurHash()
|
content.Info.Blurhash = att.GetBlurHash()
|
||||||
content.Info.AnoaBlurhash = att.GetBlurHash()
|
content.Info.AnoaBlurhash = att.GetBlurHash()
|
||||||
|
@ -498,18 +455,13 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
|
||||||
default:
|
default:
|
||||||
content.MsgType = event.MsgFile
|
content.MsgType = event.MsgFile
|
||||||
}
|
}
|
||||||
|
content.Body = fileName
|
||||||
|
content.Info.MimeType = mimeType
|
||||||
if content.Body == "" {
|
if content.Body == "" {
|
||||||
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
|
content.Body = strings.TrimPrefix(string(content.MsgType), "m.") + exmime.ExtensionFromMimetype(mimeType)
|
||||||
}
|
}
|
||||||
if file != nil {
|
return &bridgev2.ConvertedMessagePart{
|
||||||
file.URL = mxc
|
|
||||||
content.File = file
|
|
||||||
} else {
|
|
||||||
content.URL = mxc
|
|
||||||
}
|
|
||||||
return &ConvertedMessagePart{
|
|
||||||
Type: event.EventMessage,
|
Type: event.EventMessage,
|
||||||
Content: content,
|
Content: content,
|
||||||
Extra: extra,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv/matrixfmt"
|
"go.mau.fi/mautrix-signal/pkg/msgconv/matrixfmt"
|
||||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var formatParams = &matrixfmt.HTMLParser{
|
var formatParams = &matrixfmt.HTMLParser{
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntityString struct {
|
type EntityString struct {
|
107
pkg/msgconv/msgconv.go
Normal file
107
pkg/msgconv/msgconv.go
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-signal/msgconv/signalfmt"
|
"go.mau.fi/mautrix-signal/pkg/msgconv/signalfmt"
|
||||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,37 +18,27 @@ package msgconv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
|
|
||||||
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BeeperLinkPreview struct {
|
func (mc *MessageConverter) convertURLPreviewsToBeeper(ctx context.Context, preview []*signalpb.Preview) []*event.BeeperLinkPreview {
|
||||||
mautrix.RespPreviewURL
|
output := make([]*event.BeeperLinkPreview, len(preview))
|
||||||
MatchedURL string `json:"matched_url"`
|
|
||||||
ImageEncryption *event.EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *MessageConverter) convertURLPreviewsToBeeper(ctx context.Context, preview []*signalpb.Preview) []*BeeperLinkPreview {
|
|
||||||
output := make([]*BeeperLinkPreview, len(preview))
|
|
||||||
for i, p := range preview {
|
for i, p := range preview {
|
||||||
output[i] = mc.convertURLPreviewToBeeper(ctx, p)
|
output[i] = mc.convertURLPreviewToBeeper(ctx, p)
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, preview *signalpb.Preview) *BeeperLinkPreview {
|
func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, preview *signalpb.Preview) *event.BeeperLinkPreview {
|
||||||
output := &BeeperLinkPreview{
|
output := &event.BeeperLinkPreview{
|
||||||
MatchedURL: preview.GetUrl(),
|
MatchedURL: preview.GetUrl(),
|
||||||
RespPreviewURL: mautrix.RespPreviewURL{
|
LinkPreview: event.LinkPreview{
|
||||||
CanonicalURL: preview.GetUrl(),
|
CanonicalURL: preview.GetUrl(),
|
||||||
Title: preview.GetTitle(),
|
Title: preview.GetTitle(),
|
||||||
Description: preview.GetDescription(),
|
Description: preview.GetDescription(),
|
||||||
|
@ -70,65 +60,34 @@ func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, previ
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
var URLRegex = regexp.MustCompile(`https?://[^\s/_*]+(?:/\S*)?`)
|
func (mc *MessageConverter) convertURLPreviewToSignal(ctx context.Context, content *event.MessageEventContent) []*signalpb.Preview {
|
||||||
|
if len(content.BeeperLinkPreviews) == 0 {
|
||||||
func (mc *MessageConverter) convertURLPreviewToSignal(ctx context.Context, evt *event.Event) []*signalpb.Preview {
|
|
||||||
var previews []*BeeperLinkPreview
|
|
||||||
|
|
||||||
log := zerolog.Ctx(ctx)
|
|
||||||
rawPreview := gjson.GetBytes(evt.Content.VeryRaw, `com\.beeper\.linkpreviews`)
|
|
||||||
if rawPreview.Exists() && rawPreview.IsArray() {
|
|
||||||
if err := json.Unmarshal([]byte(rawPreview.Raw), &previews); err != nil || len(previews) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} /* else if portal.bridge.Config.Bridge.URLPreviews {
|
|
||||||
if matchedURL := URLRegex.FindString(evt.Content.AsMessage().Body); len(matchedURL) == 0 {
|
|
||||||
return nil
|
|
||||||
} else if parsed, err := url.Parse(matchedURL); err != nil {
|
|
||||||
return nil
|
|
||||||
} else if parsed.Host, err = idna.ToASCII(parsed.Host); err != nil {
|
|
||||||
return nil
|
|
||||||
} else if mxPreview, err := portal.MainIntent().GetURLPreview(parsed.String()); err != nil {
|
|
||||||
log.Err(err).Str("matched_url", matchedURL).Msg("Failed to fetch preview for URL found in message")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
previews = []*BeeperLinkPreview{{
|
|
||||||
RespPreviewURL: *mxPreview,
|
|
||||||
MatchedURL: matchedURL,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
if len(previews) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
output := make([]*signalpb.Preview, len(previews))
|
output := make([]*signalpb.Preview, len(content.BeeperLinkPreviews))
|
||||||
for i, preview := range previews {
|
for i, preview := range content.BeeperLinkPreviews {
|
||||||
output[i] = &signalpb.Preview{
|
output[i] = &signalpb.Preview{
|
||||||
Url: proto.String(preview.MatchedURL),
|
Url: proto.String(preview.MatchedURL),
|
||||||
Title: proto.String(preview.Title),
|
Title: proto.String(preview.Title),
|
||||||
Description: proto.String(preview.Description),
|
Description: proto.String(preview.Description),
|
||||||
Date: proto.Uint64(uint64(time.Now().UnixMilli())),
|
Date: proto.Uint64(uint64(time.Now().UnixMilli())),
|
||||||
}
|
}
|
||||||
imageMXC := preview.ImageURL
|
if preview.ImageURL != "" || preview.ImageEncryption != nil {
|
||||||
if preview.ImageEncryption != nil {
|
data, err := mc.Bridge.Bot.DownloadMedia(ctx, preview.ImageURL, preview.ImageEncryption)
|
||||||
imageMXC = preview.ImageEncryption.URL
|
|
||||||
}
|
|
||||||
if imageMXC != "" {
|
|
||||||
data, err := mc.DownloadMatrixMedia(ctx, imageMXC)
|
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if preview.ImageEncryption != nil {
|
if preview.ImageEncryption != nil {
|
||||||
err = preview.ImageEncryption.DecryptInPlace(data)
|
err = preview.ImageEncryption.DecryptInPlace(data)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uploaded, err := mc.GetClient(ctx).UploadAttachment(ctx, data)
|
uploaded, err := getClient(ctx).UploadAttachment(ctx, data)
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
uploaded.ContentType = proto.String(preview.ImageType)
|
uploaded.ContentType = proto.String(preview.ImageType)
|
|
@ -1,5 +1,5 @@
|
||||||
// mautrix-signal - A Matrix-Signal puppeting bridge.
|
// 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
|
// 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
|
// 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
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package upgrades
|
package signalid
|
||||||
|
|
||||||
import (
|
type PortalMetadata struct {
|
||||||
"context"
|
Revision uint32 `json:"revision"`
|
||||||
"embed"
|
}
|
||||||
"errors"
|
|
||||||
|
type MessageMetadata struct {
|
||||||
"go.mau.fi/util/dbutil"
|
ContainsAttachments bool `json:"contains_attachments,omitempty"`
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
105
pkg/signalid/ids.go
Normal file
105
pkg/signalid/ids.go
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
640
provisioning.go
640
provisioning.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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",
|
|
||||||
})
|
|
||||||
}
|
|
411
puppet.go
411
puppet.go
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue