Add initial compiling version of bridge
This commit is contained in:
parent
28629b8229
commit
63284481d7
25 changed files with 4547 additions and 1 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,5 +8,6 @@
|
||||||
*.json
|
*.json
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
*.log.gz
|
||||||
|
|
||||||
/mautrix-gmessages
|
/mautrix-gmessages
|
||||||
|
|
|
@ -14,7 +14,6 @@ ENV UID=1337 \
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl
|
||||||
|
|
||||||
COPY --from=builder /usr/bin/mautrix-gmessages /usr/bin/mautrix-gmessages
|
COPY --from=builder /usr/bin/mautrix-gmessages /usr/bin/mautrix-gmessages
|
||||||
COPY --from=builder /build/example-config.yaml /opt/mautrix-whatsapp/example-config.yaml
|
|
||||||
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
|
|
||||||
|
|
94
bridgestate.go
Normal file
94
bridgestate.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 (
|
||||||
|
"maunium.net/go/mautrix/bridge/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WALoggedOut status.BridgeStateErrorCode = "wa-logged-out"
|
||||||
|
WAMainDeviceGone status.BridgeStateErrorCode = "wa-main-device-gone"
|
||||||
|
WAUnknownLogout status.BridgeStateErrorCode = "wa-unknown-logout"
|
||||||
|
WANotConnected status.BridgeStateErrorCode = "wa-not-connected"
|
||||||
|
WAConnecting status.BridgeStateErrorCode = "wa-connecting"
|
||||||
|
WAKeepaliveTimeout status.BridgeStateErrorCode = "wa-keepalive-timeout"
|
||||||
|
WAPhoneOffline status.BridgeStateErrorCode = "wa-phone-offline"
|
||||||
|
WAConnectionFailed status.BridgeStateErrorCode = "wa-connection-failed"
|
||||||
|
WADisconnected status.BridgeStateErrorCode = "wa-transient-disconnect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
|
||||||
|
WALoggedOut: "You were logged out from another device. Relogin to continue using the bridge.",
|
||||||
|
WAMainDeviceGone: "Your phone was logged out from WhatsApp. Relogin to continue using the bridge.",
|
||||||
|
WAUnknownLogout: "You were logged out for an unknown reason. Relogin to continue using the bridge.",
|
||||||
|
WANotConnected: "You're not connected to WhatsApp",
|
||||||
|
WAConnecting: "Reconnecting to WhatsApp...",
|
||||||
|
WAKeepaliveTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
|
||||||
|
WAPhoneOffline: "Your phone hasn't been seen in over 12 days. The bridge is currently connected, but will get disconnected if you don't open the app soon.",
|
||||||
|
WAConnectionFailed: "Connecting to the WhatsApp web servers failed.",
|
||||||
|
WADisconnected: "Disconnected from WhatsApp. Trying to reconnect.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetRemoteID() string {
|
||||||
|
if user == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return user.Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetRemoteName() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/*func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !prov.bridge.AS.CheckServerToken(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := r.URL.Query().Get("user_id")
|
||||||
|
user := prov.bridge.GetUserByMXID(id.UserID(userID))
|
||||||
|
var global status.BridgeState
|
||||||
|
global.StateEvent = status.StateRunning
|
||||||
|
var remote status.BridgeState
|
||||||
|
if user.IsConnected() {
|
||||||
|
if user.Client.IsLoggedIn() {
|
||||||
|
remote.StateEvent = status.StateConnected
|
||||||
|
} else if user.Session != nil {
|
||||||
|
remote.StateEvent = status.StateConnecting
|
||||||
|
remote.Error = WAConnecting
|
||||||
|
} // else: unconfigured
|
||||||
|
} else if user.Session != nil {
|
||||||
|
remote.StateEvent = status.StateBadCredentials
|
||||||
|
remote.Error = WANotConnected
|
||||||
|
} // else: unconfigured
|
||||||
|
global = global.Fill(nil)
|
||||||
|
resp := status.GlobalBridgeState{
|
||||||
|
BridgeState: global,
|
||||||
|
RemoteStates: map[string]status.BridgeState{},
|
||||||
|
}
|
||||||
|
if len(remote.StateEvent) > 0 {
|
||||||
|
remote = remote.Fill(user)
|
||||||
|
resp.RemoteStates[remote.RemoteID] = remote
|
||||||
|
}
|
||||||
|
user.log.Debugfln("Responding bridge state in bridge status endpoint: %+v", resp)
|
||||||
|
jsonResponse(w, http.StatusOK, &resp)
|
||||||
|
if len(resp.RemoteStates) > 0 {
|
||||||
|
user.BridgeState.SetPrev(remote)
|
||||||
|
}
|
||||||
|
}*/
|
321
commands.go
Normal file
321
commands.go
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
|
"maunium.net/go/mautrix/bridge/status"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WrappedCommandEvent struct {
|
||||||
|
*commands.Event
|
||||||
|
Bridge *GMBridge
|
||||||
|
User *User
|
||||||
|
Portal *Portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) RegisterCommands() {
|
||||||
|
proc := br.CommandProcessor.(*commands.Processor)
|
||||||
|
proc.AddHandlers(
|
||||||
|
cmdLogin,
|
||||||
|
cmdDeleteSession,
|
||||||
|
cmdReconnect,
|
||||||
|
cmdDisconnect,
|
||||||
|
cmdPing,
|
||||||
|
cmdDeletePortal,
|
||||||
|
cmdDeleteAllPortals,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) {
|
||||||
|
return func(ce *commands.Event) {
|
||||||
|
user := ce.User.(*User)
|
||||||
|
var portal *Portal
|
||||||
|
if ce.Portal != nil {
|
||||||
|
portal = ce.Portal.(*Portal)
|
||||||
|
}
|
||||||
|
br := ce.Bridge.Child.(*GMBridge)
|
||||||
|
handler(&WrappedCommandEvent{ce, br, user, portal})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
HelpSectionConnectionManagement = commands.HelpSection{Name: "Connection management", Order: 11}
|
||||||
|
HelpSectionPortalManagement = commands.HelpSection{Name: "Portal management", Order: 20}
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdLogin = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnLogin),
|
||||||
|
Name: "login",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Link the bridge to Google Messages on your Android phone as a web client.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnLogin(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Session != nil {
|
||||||
|
if ce.User.IsConnected() {
|
||||||
|
ce.Reply("You're already logged in")
|
||||||
|
} else {
|
||||||
|
ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ce.User.Login(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
ce.User.log.Errorf("Failed to log in:", err)
|
||||||
|
ce.Reply("Failed to log in: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) sendQR(ce *WrappedCommandEvent, code string, prevEvent id.EventID) id.EventID {
|
||||||
|
url, ok := user.uploadQR(ce, code)
|
||||||
|
if !ok {
|
||||||
|
return prevEvent
|
||||||
|
}
|
||||||
|
content := event.MessageEventContent{
|
||||||
|
MsgType: event.MsgImage,
|
||||||
|
Body: code,
|
||||||
|
URL: url.CUString(),
|
||||||
|
}
|
||||||
|
if len(prevEvent) != 0 {
|
||||||
|
content.SetEdit(prevEvent)
|
||||||
|
}
|
||||||
|
resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Errorln("Failed to send edited QR code to user:", err)
|
||||||
|
} else if len(prevEvent) == 0 {
|
||||||
|
prevEvent = resp.EventID
|
||||||
|
}
|
||||||
|
return prevEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) uploadQR(ce *WrappedCommandEvent, code string) (id.ContentURI, bool) {
|
||||||
|
qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Errorln("Failed to encode QR code:", err)
|
||||||
|
ce.Reply("Failed to encode QR code: %v", err)
|
||||||
|
return id.ContentURI{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
bot := user.bridge.AS.BotClient()
|
||||||
|
|
||||||
|
resp, err := bot.UploadBytes(qrCode, "image/png")
|
||||||
|
if err != nil {
|
||||||
|
user.log.Errorln("Failed to upload QR code:", err)
|
||||||
|
ce.Reply("Failed to upload QR code: %v", err)
|
||||||
|
return id.ContentURI{}, false
|
||||||
|
}
|
||||||
|
return resp.ContentURI, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDeleteSession = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnDeleteSession),
|
||||||
|
Name: "delete-session",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: commands.HelpSectionAuth,
|
||||||
|
Description: "Delete session information and disconnect from Google Messages without sending a logout request.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnDeleteSession(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Session == nil && ce.User.Client == nil {
|
||||||
|
ce.Reply("Nothing to purge: no session information stored and no active connection.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.User.removeFromPhoneMap(status.BridgeState{StateEvent: status.StateLoggedOut})
|
||||||
|
ce.User.DeleteConnection()
|
||||||
|
ce.User.DeleteSession()
|
||||||
|
ce.Reply("Session information purged")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdReconnect = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnReconnect),
|
||||||
|
Name: "reconnect",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionConnectionManagement,
|
||||||
|
Description: "Reconnect to Google Messages.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnReconnect(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Client == nil {
|
||||||
|
if ce.User.Session == nil {
|
||||||
|
ce.Reply("You're not logged into Google Messages. Please log in first.")
|
||||||
|
} else {
|
||||||
|
ce.User.Connect()
|
||||||
|
ce.Reply("Started connecting to Google Messages")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ce.User.DeleteConnection()
|
||||||
|
ce.User.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WANotConnected})
|
||||||
|
ce.User.Connect()
|
||||||
|
ce.Reply("Restarted connection to Google Messages")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDisconnect = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnDisconnect),
|
||||||
|
Name: "disconnect",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionConnectionManagement,
|
||||||
|
Description: "Disconnect from Google Messages (without logging out).",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnDisconnect(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Client == nil {
|
||||||
|
ce.Reply("You don't have a Google Messages connection.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ce.User.DeleteConnection()
|
||||||
|
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
|
||||||
|
ce.User.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Error: WANotConnected})
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdPing = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnPing),
|
||||||
|
Name: "ping",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionConnectionManagement,
|
||||||
|
Description: "Check your connection to Google Messages.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnPing(ce *WrappedCommandEvent) {
|
||||||
|
if ce.User.Session == nil {
|
||||||
|
if ce.User.Client != nil {
|
||||||
|
ce.Reply("Connected to Google Messages, but not logged in.")
|
||||||
|
} else {
|
||||||
|
ce.Reply("You're not logged into Google Messages.")
|
||||||
|
}
|
||||||
|
} else if ce.User.Client == nil || !ce.User.Client.IsConnected() {
|
||||||
|
ce.Reply("You're logged in as %s (device #%d), but you don't have a Google Messages connection.", ce.User.Phone)
|
||||||
|
} else {
|
||||||
|
ce.Reply("Logged in as %s, connection to Google Messages may be OK", ce.User.Phone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canDeletePortal(portal *Portal, userID id.UserID) bool {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := portal.MainIntent().JoinedMembers(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorfln("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for otherUser := range members.Joined {
|
||||||
|
_, isPuppet := portal.bridge.ParsePuppetMXID(otherUser)
|
||||||
|
if isPuppet || otherUser == portal.bridge.Bot.UserID || otherUser == userID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
user := portal.bridge.GetUserByMXID(otherUser)
|
||||||
|
if user != nil && user.Session != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDeletePortal = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnDeletePortal),
|
||||||
|
Name: "delete-portal",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Delete the current portal. If the portal is used by other people, this is limited to bridge admins.",
|
||||||
|
},
|
||||||
|
RequiresPortal: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnDeletePortal(ce *WrappedCommandEvent) {
|
||||||
|
if !ce.User.Admin && !canDeletePortal(ce.Portal, ce.User.MXID) {
|
||||||
|
ce.Reply("Only bridge admins can delete portals with other Matrix users")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.")
|
||||||
|
ce.Portal.Delete()
|
||||||
|
ce.Portal.Cleanup(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDeleteAllPortals = &commands.FullHandler{
|
||||||
|
Func: wrapCommand(fnDeleteAllPortals),
|
||||||
|
Name: "delete-all-portals",
|
||||||
|
Help: commands.HelpMeta{
|
||||||
|
Section: HelpSectionPortalManagement,
|
||||||
|
Description: "Delete all portals.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func fnDeleteAllPortals(ce *WrappedCommandEvent) {
|
||||||
|
portals := ce.Bridge.GetAllPortalsForUser(ce.User.RowID)
|
||||||
|
if len(portals) == 0 {
|
||||||
|
ce.Reply("Didn't find any portals to delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
leave := func(portal *Portal) {
|
||||||
|
if len(portal.MXID) > 0 {
|
||||||
|
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
|
||||||
|
Reason: "Deleting portal",
|
||||||
|
UserID: ce.User.MXID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intent := ce.User.DoublePuppetIntent
|
||||||
|
if intent != nil {
|
||||||
|
leave = func(portal *Portal) {
|
||||||
|
if len(portal.MXID) > 0 {
|
||||||
|
_, _ = intent.LeaveRoom(portal.MXID)
|
||||||
|
_, _ = intent.ForgetRoom(portal.MXID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomYeeting := ce.Bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting)
|
||||||
|
if roomYeeting {
|
||||||
|
leave = func(portal *Portal) {
|
||||||
|
portal.Cleanup(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ce.Reply("Found %d portals, deleting...", len(portals))
|
||||||
|
for _, portal := range portals {
|
||||||
|
portal.Delete()
|
||||||
|
leave(portal)
|
||||||
|
}
|
||||||
|
if !roomYeeting {
|
||||||
|
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
|
||||||
|
go func() {
|
||||||
|
for _, portal := range portals {
|
||||||
|
portal.Cleanup(false)
|
||||||
|
}
|
||||||
|
ce.Reply("Finished background cleanup of deleted portal rooms.")
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
ce.Reply("Finished deleting portals.")
|
||||||
|
}
|
||||||
|
}
|
159
config/bridge.go
Normal file
159
config/bridge.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BridgeConfig struct {
|
||||||
|
UsernameTemplate string `yaml:"username_template"`
|
||||||
|
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||||
|
|
||||||
|
PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"`
|
||||||
|
|
||||||
|
DeliveryReceipts bool `yaml:"delivery_receipts"`
|
||||||
|
MessageStatusEvents bool `yaml:"message_status_events"`
|
||||||
|
MessageErrorNotices bool `yaml:"message_error_notices"`
|
||||||
|
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
||||||
|
|
||||||
|
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
|
||||||
|
|
||||||
|
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
|
||||||
|
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||||
|
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
|
||||||
|
|
||||||
|
PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"`
|
||||||
|
BridgeNotices bool `yaml:"bridge_notices"`
|
||||||
|
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
|
||||||
|
MuteBridging bool `yaml:"mute_bridging"`
|
||||||
|
ArchiveTag string `yaml:"archive_tag"`
|
||||||
|
PinnedTag string `yaml:"pinned_tag"`
|
||||||
|
TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
|
||||||
|
FederateRooms bool `yaml:"federate_rooms"`
|
||||||
|
CaptionInMessage bool `yaml:"caption_in_message"`
|
||||||
|
|
||||||
|
DisableBridgeAlerts bool `yaml:"disable_bridge_alerts"`
|
||||||
|
|
||||||
|
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"`
|
||||||
|
} `yaml:"provisioning"`
|
||||||
|
|
||||||
|
Permissions bridgeconfig.PermissionConfig `yaml:"permissions"`
|
||||||
|
|
||||||
|
ParsedUsernameTemplate *template.Template `yaml:"-"`
|
||||||
|
displaynameTemplate *template.Template `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
|
||||||
|
return bc.Encryption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) EnableMessageStatusEvents() bool {
|
||||||
|
return bc.MessageStatusEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) EnableMessageErrorNotices() bool {
|
||||||
|
return bc.MessageErrorNotices
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) GetCommandPrefix() string {
|
||||||
|
return bc.CommandPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) GetManagementRoomTexts() bridgeconfig.ManagementRoomTexts {
|
||||||
|
return bc.ManagementRoomText
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) GetResendBridgeInfo() bool {
|
||||||
|
return bc.ResendBridgeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ParsedUsernameTemplate, err = template.New("username").Parse(bc.UsernameTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !strings.Contains(bc.FormatUsername("1.1234567890"), "1.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
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplaynameTemplateArgs struct {
|
||||||
|
PhoneNumber string
|
||||||
|
FullName string
|
||||||
|
FirstName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) FormatDisplayname(phone, fullName, firstName string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
_ = bc.displaynameTemplate.Execute(&buf, DisplaynameTemplateArgs{
|
||||||
|
PhoneNumber: phone,
|
||||||
|
FullName: fullName,
|
||||||
|
FirstName: firstName,
|
||||||
|
})
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc BridgeConfig) FormatUsername(username string) string {
|
||||||
|
var buf strings.Builder
|
||||||
|
_ = bc.ParsedUsernameTemplate.Execute(&buf, username)
|
||||||
|
return buf.String()
|
||||||
|
}
|
42
config/config.go
Normal file
42
config/config.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
*bridgeconfig.BaseConfig `yaml:",inline"`
|
||||||
|
|
||||||
|
SegmentKey string `yaml:"segment_key"`
|
||||||
|
SegmentUserID string `yaml:"segment_user_id"`
|
||||||
|
|
||||||
|
Metrics struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Listen string `yaml:"listen"`
|
||||||
|
} `yaml:"metrics"`
|
||||||
|
|
||||||
|
Bridge BridgeConfig `yaml:"bridge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
|
||||||
|
_, homeserver, _ := userID.Parse()
|
||||||
|
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
|
||||||
|
return hasSecret
|
||||||
|
}
|
108
config/upgrade.go
Normal file
108
config/upgrade.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
"maunium.net/go/mautrix/util"
|
||||||
|
up "maunium.net/go/mautrix/util/configupgrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoUpgrade(helper *up.Helper) {
|
||||||
|
bridgeconfig.Upgrader.DoUpgrade(helper)
|
||||||
|
|
||||||
|
helper.Copy(up.Str|up.Null, "segment_key")
|
||||||
|
helper.Copy(up.Str|up.Null, "segment_user_id")
|
||||||
|
|
||||||
|
helper.Copy(up.Bool, "metrics", "enabled")
|
||||||
|
helper.Copy(up.Str, "metrics", "listen")
|
||||||
|
|
||||||
|
helper.Copy(up.Str, "bridge", "username_template")
|
||||||
|
helper.Copy(up.Str, "bridge", "displayname_template")
|
||||||
|
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
|
||||||
|
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.Int, "bridge", "portal_message_buffer")
|
||||||
|
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
|
||||||
|
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", "private_chat_portal_meta")
|
||||||
|
helper.Copy(up.Bool, "bridge", "bridge_notices")
|
||||||
|
helper.Copy(up.Bool, "bridge", "resend_bridge_info")
|
||||||
|
helper.Copy(up.Bool, "bridge", "mute_bridging")
|
||||||
|
helper.Copy(up.Str|up.Null, "bridge", "archive_tag")
|
||||||
|
helper.Copy(up.Str|up.Null, "bridge", "pinned_tag")
|
||||||
|
helper.Copy(up.Bool, "bridge", "tag_only_on_create")
|
||||||
|
helper.Copy(up.Str, "bridge", "command_prefix")
|
||||||
|
helper.Copy(up.Bool, "bridge", "federate_rooms")
|
||||||
|
helper.Copy(up.Bool, "bridge", "disable_bridge_alerts")
|
||||||
|
helper.Copy(up.Bool, "bridge", "caption_in_message")
|
||||||
|
|
||||||
|
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.Str, "bridge", "provisioning", "prefix")
|
||||||
|
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
|
||||||
|
sharedSecret := util.RandomString(64)
|
||||||
|
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
|
||||||
|
} else {
|
||||||
|
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")
|
||||||
|
}
|
||||||
|
helper.Copy(up.Map, "bridge", "permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
var SpacedBlocks = [][]string{
|
||||||
|
{"homeserver", "software"},
|
||||||
|
{"appservice"},
|
||||||
|
{"appservice", "hostname"},
|
||||||
|
{"appservice", "database"},
|
||||||
|
{"appservice", "id"},
|
||||||
|
{"appservice", "as_token"},
|
||||||
|
{"segment_key"},
|
||||||
|
{"metrics"},
|
||||||
|
{"bridge"},
|
||||||
|
{"bridge", "command_prefix"},
|
||||||
|
{"bridge", "management_room_text"},
|
||||||
|
{"bridge", "encryption"},
|
||||||
|
{"bridge", "provisioning"},
|
||||||
|
{"bridge", "permissions"},
|
||||||
|
{"logging"},
|
||||||
|
}
|
168
custompuppet.go
Normal file
168
custompuppet.go
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ bridge.DoublePuppet = (*User)(nil)
|
||||||
|
|
||||||
|
func (user *User) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
|
||||||
|
if mxid != user.MXID {
|
||||||
|
return errors.New("mismatching mxid")
|
||||||
|
}
|
||||||
|
user.AccessToken = accessToken
|
||||||
|
return user.startCustomMXID(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) CustomIntent() *appservice.IntentAPI {
|
||||||
|
return user.DoublePuppetIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) loginWithSharedSecret() error {
|
||||||
|
_, homeserver, _ := user.MXID.Parse()
|
||||||
|
user.zlog.Debug().Msg("Logging into double puppet with shared secret")
|
||||||
|
loginSecret := user.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
|
||||||
|
client, err := user.bridge.newDoublePuppetClient(user.MXID, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req := mautrix.ReqLogin{
|
||||||
|
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(user.MXID)},
|
||||||
|
DeviceID: "Google Messages Bridge",
|
||||||
|
InitialDeviceDisplayName: "Google Messages Bridge",
|
||||||
|
}
|
||||||
|
if loginSecret == "appservice" {
|
||||||
|
client.AccessToken = user.bridge.AS.Registration.AppToken
|
||||||
|
req.Type = mautrix.AuthTypeAppservice
|
||||||
|
} else {
|
||||||
|
mac := hmac.New(sha512.New, []byte(loginSecret))
|
||||||
|
mac.Write([]byte(user.MXID))
|
||||||
|
req.Password = hex.EncodeToString(mac.Sum(nil))
|
||||||
|
req.Type = mautrix.AuthTypePassword
|
||||||
|
}
|
||||||
|
resp, err := client.Login(&req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to log in with shared secret: %w", err)
|
||||||
|
}
|
||||||
|
user.AccessToken = resp.AccessToken
|
||||||
|
err = user.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save access token: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
|
||||||
|
_, homeserver, err := mxid.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
|
||||||
|
if !found {
|
||||||
|
if homeserver == br.AS.HomeserverDomain {
|
||||||
|
homeserverURL = ""
|
||||||
|
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
|
||||||
|
resp, err := mautrix.DiscoverClientAPI(homeserver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
|
||||||
|
}
|
||||||
|
homeserverURL = resp.Homeserver.BaseURL
|
||||||
|
br.ZLog.Debug().
|
||||||
|
Str("server_name", homeserver).
|
||||||
|
Str("base_url", homeserverURL).
|
||||||
|
Str("user_id", mxid.String()).
|
||||||
|
Msg("Discovered homeserver URL to enable double puppeting for external user")
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) newDoublePuppetIntent() (*appservice.IntentAPI, error) {
|
||||||
|
client, err := user.bridge.newDoublePuppetClient(user.MXID, user.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ia := user.bridge.AS.NewIntentAPI("custom")
|
||||||
|
ia.Client = client
|
||||||
|
ia.Localpart, _, _ = user.MXID.Parse()
|
||||||
|
ia.UserID = user.MXID
|
||||||
|
ia.IsCustomPuppet = true
|
||||||
|
return ia, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) clearCustomMXID() {
|
||||||
|
user.AccessToken = ""
|
||||||
|
user.DoublePuppetIntent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) startCustomMXID(reloginOnFail bool) error {
|
||||||
|
if len(user.AccessToken) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
intent, err := user.newDoublePuppetIntent()
|
||||||
|
if err != nil {
|
||||||
|
user.clearCustomMXID()
|
||||||
|
return fmt.Errorf("failed to create double puppet intent: %w", err)
|
||||||
|
}
|
||||||
|
resp, err := intent.Whoami()
|
||||||
|
if err != nil {
|
||||||
|
if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !user.tryRelogin(err)) {
|
||||||
|
user.clearCustomMXID()
|
||||||
|
return fmt.Errorf("failed to ensure double puppet token is valid: %w", err)
|
||||||
|
}
|
||||||
|
intent.AccessToken = user.AccessToken
|
||||||
|
}
|
||||||
|
if resp.UserID != user.MXID {
|
||||||
|
user.clearCustomMXID()
|
||||||
|
return ErrMismatchingMXID
|
||||||
|
}
|
||||||
|
user.DoublePuppetIntent = intent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) tryRelogin(err error) bool {
|
||||||
|
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
user.zlog.Debug().Err(err).Msg("Trying to relogin after error in double puppet")
|
||||||
|
err = user.loginWithSharedSecret()
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to relogin after error in double puppet")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
user.zlog.Info().Msg("Successfully relogined after error in double puppet")
|
||||||
|
return true
|
||||||
|
}
|
77
database/database.go
Normal file
77
database/database.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-gmessages/database/upgrades"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
*dbutil.Database
|
||||||
|
|
||||||
|
User *UserQuery
|
||||||
|
Portal *PortalQuery
|
||||||
|
Puppet *PuppetQuery
|
||||||
|
Message *MessageQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(baseDB *dbutil.Database) *Database {
|
||||||
|
db := &Database{Database: baseDB}
|
||||||
|
db.UpgradeTable = upgrades.Table
|
||||||
|
db.User = &UserQuery{db: db}
|
||||||
|
db.Portal = &PortalQuery{db: db}
|
||||||
|
db.Puppet = &PuppetQuery{db: db}
|
||||||
|
db.Message = &MessageQuery{db: db}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
type dataStruct[T any] interface {
|
||||||
|
Scan(row dbutil.Scannable) (T, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type queryStruct[T dataStruct[T]] interface {
|
||||||
|
New() T
|
||||||
|
getDB() *Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func get[T dataStruct[T]](qs queryStruct[T], ctx context.Context, query string, args ...any) (T, error) {
|
||||||
|
return qs.New().Scan(qs.getDB().Conn(ctx).QueryRowContext(ctx, query, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAll[T dataStruct[T]](qs queryStruct[T], ctx context.Context, query string, args ...any) ([]T, error) {
|
||||||
|
rows, err := qs.getDB().Conn(ctx).QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]T, 0)
|
||||||
|
defer func() {
|
||||||
|
_ = rows.Close()
|
||||||
|
}()
|
||||||
|
for rows.Next() {
|
||||||
|
item, err := qs.New().Scan(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
104
database/message.go
Normal file
104
database/message.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageQuery struct {
|
||||||
|
db *Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mq *MessageQuery) New() *Message {
|
||||||
|
return &Message{
|
||||||
|
db: mq.db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mq *MessageQuery) getDB() *Database {
|
||||||
|
return mq.db
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
getMessageByIDQuery = `
|
||||||
|
SELECT conv_id, conv_receiver, id, mxid, sender, timestamp FROM message
|
||||||
|
WHERE conv_id=$1 AND conv_receiver=$2 AND id=$3
|
||||||
|
`
|
||||||
|
getMessageByMXIDQuery = `
|
||||||
|
SELECT conv_id, conv_receiver, id, mxid, sender, timestamp FROM message
|
||||||
|
WHERE mxid=$1
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func (mq *MessageQuery) GetByID(ctx context.Context, chat Key, messageID string) (*Message, error) {
|
||||||
|
return get[*Message](mq, ctx, getMessageByIDQuery, chat.ID, chat.Receiver, messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mq *MessageQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Message, error) {
|
||||||
|
return get[*Message](mq, ctx, getMessageByMXIDQuery, mxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
db *Database
|
||||||
|
log log.Logger
|
||||||
|
|
||||||
|
Chat Key
|
||||||
|
ID string
|
||||||
|
MXID id.EventID
|
||||||
|
Sender string
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) Scan(row dbutil.Scannable) (*Message, error) {
|
||||||
|
var ts int64
|
||||||
|
err := row.Scan(&msg.Chat.ID, &msg.Chat.Receiver, &msg.ID, &msg.MXID, &msg.Sender, &msg.Timestamp)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ts != 0 {
|
||||||
|
msg.Timestamp = time.UnixMilli(ts)
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) sqlVariables() []any {
|
||||||
|
return []any{msg.Chat.ID, msg.Chat.Receiver, msg.ID, msg.MXID, msg.Sender, msg.Timestamp.UnixMilli()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) Insert(ctx context.Context) error {
|
||||||
|
_, err := msg.db.Conn(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO message (conv_id, conv_receiver, id, mxid, sender, timestamp)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`, msg.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) Delete(ctx context.Context) error {
|
||||||
|
_, err := msg.db.Conn(ctx).ExecContext(ctx, "DELETE FROM message WHERE conv_id=$1 AND conv_receiver=$2 AND id=$3", msg.Chat.ID, msg.Chat.Receiver, msg.ID)
|
||||||
|
return err
|
||||||
|
}
|
138
database/portal.go
Normal file
138
database/portal.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortalQuery struct {
|
||||||
|
db *Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) New() *Portal {
|
||||||
|
return &Portal{
|
||||||
|
db: pq.db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) getDB() *Database {
|
||||||
|
return pq.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) GetAll(ctx context.Context) ([]*Portal, error) {
|
||||||
|
return getAll[*Portal](pq, ctx, "SELECT id, receiver, self_user, other_user, mxid, name, name_set, avatar_id, avatar_mxc, avatar_set, encrypted, in_space FROM portal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) GetAllForUser(ctx context.Context, receiver int) ([]*Portal, error) {
|
||||||
|
return getAll[*Portal](pq, ctx, "SELECT id, receiver, self_user, other_user, mxid, name, name_set, avatar_id, avatar_mxc, avatar_set, encrypted, in_space FROM portal WHERE receiver=$1", receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) GetByKey(ctx context.Context, key Key) (*Portal, error) {
|
||||||
|
return get[*Portal](pq, ctx, "SELECT id, receiver, self_user, other_user, mxid, name, name_set, avatar_id, avatar_mxc, avatar_set, encrypted, in_space FROM portal WHERE id=$1 AND receiver=$2", key.ID, key.Receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PortalQuery) GetByMXID(ctx context.Context, mxid id.RoomID) (*Portal, error) {
|
||||||
|
return get[*Portal](pq, ctx, "SELECT id, receiver, self_user, other_user, mxid, name, name_set, avatar_id, avatar_mxc, avatar_set, encrypted, in_space FROM portal WHERE mxid=$1", mxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key struct {
|
||||||
|
ID string
|
||||||
|
Receiver int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Key) String() string {
|
||||||
|
return fmt.Sprintf("%d.%s", p.Receiver, p.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Key) MarshalZerologObject(e *zerolog.Event) {
|
||||||
|
e.Str("id", p.ID).Int("receiver", p.Receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Portal struct {
|
||||||
|
db *Database
|
||||||
|
|
||||||
|
Key
|
||||||
|
SelfUserID string
|
||||||
|
OtherUserID string
|
||||||
|
MXID id.RoomID
|
||||||
|
|
||||||
|
Name string
|
||||||
|
NameSet bool
|
||||||
|
AvatarID string
|
||||||
|
AvatarMXC id.ContentURI
|
||||||
|
AvatarSet bool
|
||||||
|
Encrypted bool
|
||||||
|
InSpace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
|
||||||
|
var mxid, selfUserID, otherUserID sql.NullString
|
||||||
|
err := row.Scan(&portal.ID, &portal.Receiver, &selfUserID, &otherUserID, &mxid, &portal.Name, &portal.NameSet, &portal.AvatarID, &portal.AvatarMXC, &portal.AvatarSet, &portal.Encrypted, &portal.InSpace)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
portal.MXID = id.RoomID(mxid.String)
|
||||||
|
portal.SelfUserID = selfUserID.String
|
||||||
|
portal.OtherUserID = otherUserID.String
|
||||||
|
return portal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sqlVariables() []any {
|
||||||
|
var mxid, selfUserID, otherUserID *string
|
||||||
|
if portal.MXID != "" {
|
||||||
|
mxid = (*string)(&portal.MXID)
|
||||||
|
}
|
||||||
|
if portal.SelfUserID != "" {
|
||||||
|
selfUserID = &portal.SelfUserID
|
||||||
|
}
|
||||||
|
if portal.OtherUserID != "" {
|
||||||
|
otherUserID = &portal.OtherUserID
|
||||||
|
}
|
||||||
|
return []any{portal.ID, portal.Receiver, selfUserID, otherUserID, mxid, portal.Name, portal.NameSet, portal.AvatarID, portal.AvatarMXC, portal.AvatarSet, portal.Encrypted, portal.InSpace}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Insert(ctx context.Context) error {
|
||||||
|
_, err := portal.db.Conn(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO portal (id, receiver, self_user, other_user, mxid, name, name_set, avatar_id, avatar_mxc, avatar_set, encrypted, in_space)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
|
`, portal.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Update(ctx context.Context) error {
|
||||||
|
_, err := portal.db.Conn(ctx).ExecContext(ctx, `
|
||||||
|
UPDATE portal
|
||||||
|
SET self_user=$3, other_user=$4, mxid=$5, name=$6, name_set=$7, avatar_id=$8, avatar_mxc=$9, avatar_set=$10, encrypted=$11, in_space=$12
|
||||||
|
WHERE id=$1 AND receiver=$2
|
||||||
|
`, portal.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Delete(ctx context.Context) error {
|
||||||
|
_, err := portal.db.Conn(ctx).ExecContext(ctx, "DELETE FROM portal WHERE id=$1 AND receiver=$2", portal.ID, portal.Receiver)
|
||||||
|
return err
|
||||||
|
}
|
92
database/puppet.go
Normal file
92
database/puppet.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PuppetQuery struct {
|
||||||
|
db *Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PuppetQuery) New() *Puppet {
|
||||||
|
return &Puppet{
|
||||||
|
db: pq.db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PuppetQuery) getDB() *Database {
|
||||||
|
return pq.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PuppetQuery) GetAll(ctx context.Context) ([]*Puppet, error) {
|
||||||
|
return getAll[*Puppet](pq, ctx, "SELECT id, receiver, phone, name, name_set, avatar_id, avatar_mxc, avatar_set, contact_info_set FROM puppet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pq *PuppetQuery) Get(ctx context.Context, key Key) (*Puppet, error) {
|
||||||
|
return get[*Puppet](pq, ctx, "SELECT id, receiver, phone, name, name_set, avatar_id, avatar_mxc, avatar_set, contact_info_set FROM puppet WHERE phone=$1 AND receiver=$2", key.ID, key.Receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Puppet struct {
|
||||||
|
db *Database
|
||||||
|
|
||||||
|
Key
|
||||||
|
Phone string
|
||||||
|
Name string
|
||||||
|
NameSet bool
|
||||||
|
AvatarID string
|
||||||
|
AvatarMXC id.ContentURI
|
||||||
|
AvatarSet bool
|
||||||
|
ContactInfoSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) Scan(row dbutil.Scannable) (*Puppet, error) {
|
||||||
|
err := row.Scan(&puppet.ID, &puppet.Receiver, &puppet.Phone, &puppet.Name, &puppet.NameSet, &puppet.AvatarID, &puppet.AvatarMXC, &puppet.AvatarSet, &puppet.ContactInfoSet)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return puppet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) sqlVariables() []any {
|
||||||
|
return []any{puppet.ID, puppet.Receiver, puppet.Phone, puppet.Name, puppet.NameSet, puppet.AvatarID, puppet.AvatarMXC, puppet.AvatarSet, puppet.ContactInfoSet}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) Insert(ctx context.Context) error {
|
||||||
|
_, err := puppet.db.Conn(ctx).ExecContext(ctx, `
|
||||||
|
INSERT INTO puppet (id, receiver, phone, name, name_set, avatar_id, avatar_mxc, avatar_set, contact_info_set)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
`, puppet.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) Update(ctx context.Context) error {
|
||||||
|
_, err := puppet.db.Conn(ctx).ExecContext(ctx, `
|
||||||
|
UPDATE puppet
|
||||||
|
SET phone=$3, name=$4, name_set=$5, avatar_id=$6, avatar_mxc=$7, avatar_set=$8, contact_info_set=$9
|
||||||
|
WHERE id=$1 AND receiver=$2
|
||||||
|
`, puppet.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
64
database/upgrades/00-latest-revision.sql
Normal file
64
database/upgrades/00-latest-revision.sql
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
-- v0 -> v1: Latest revision
|
||||||
|
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
-- only: postgres
|
||||||
|
rowid BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
||||||
|
-- only: sqlite
|
||||||
|
rowid INTEGER PRIMARY KEY,
|
||||||
|
|
||||||
|
mxid TEXT NOT NULL UNIQUE,
|
||||||
|
phone TEXT UNIQUE,
|
||||||
|
session jsonb,
|
||||||
|
|
||||||
|
management_room TEXT,
|
||||||
|
space_room TEXT,
|
||||||
|
|
||||||
|
access_token TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE puppet (
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
receiver BIGINT NOT NULL,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
avatar_id TEXT NOT NULL,
|
||||||
|
avatar_mxc TEXT NOT NULL,
|
||||||
|
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
contact_info_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
FOREIGN KEY (receiver) REFERENCES "user"(rowid) ON DELETE CASCADE,
|
||||||
|
UNIQUE (phone, receiver),
|
||||||
|
PRIMARY KEY (id, receiver)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE portal (
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
receiver BIGINT NOT NULL,
|
||||||
|
self_user TEXT,
|
||||||
|
other_user TEXT,
|
||||||
|
mxid TEXT UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
avatar_id TEXT NOT NULL,
|
||||||
|
avatar_mxc TEXT NOT NULL,
|
||||||
|
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
encrypted BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
in_space BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
FOREIGN KEY (receiver) REFERENCES "user"(rowid) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (other_user, receiver) REFERENCES puppet(id, receiver) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (id, receiver)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE message (
|
||||||
|
conv_id TEXT NOT NULL,
|
||||||
|
conv_receiver BIGINT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
mxid TEXT NOT NULL UNIQUE,
|
||||||
|
sender TEXT NOT NULL,
|
||||||
|
timestamp BIGINT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (conv_id, conv_receiver, id),
|
||||||
|
FOREIGN KEY (conv_id, conv_receiver) REFERENCES portal(id, receiver) ON DELETE CASCADE
|
||||||
|
);
|
32
database/upgrades/upgrades.go
Normal file
32
database/upgrades/upgrades.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 upgrades
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Table dbutil.UpgradeTable
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var rawUpgrades embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Table.RegisterFS(rawUpgrades)
|
||||||
|
}
|
144
database/user.go
Normal file
144
database/user.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/dbutil"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserQuery struct {
|
||||||
|
db *Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) New() *User {
|
||||||
|
return &User{
|
||||||
|
db: uq.db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) getDB() *Database {
|
||||||
|
return uq.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetAllWithSession(ctx context.Context) ([]*User, error) {
|
||||||
|
return getAll[*User](uq, ctx, `SELECT rowid, mxid, phone, session, management_room, space_room, access_token FROM "user" WHERE phone<>'' AND session IS NOT NULL`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetAllWithDoublePuppet(ctx context.Context) ([]*User, error) {
|
||||||
|
return getAll[*User](uq, ctx, `SELECT rowid, mxid, phone, session, management_room, space_room, access_token FROM "user" WHERE access_token<>''`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetByRowID(ctx context.Context, rowID int) (*User, error) {
|
||||||
|
return get[*User](uq, ctx, `SELECT rowid, mxid, phone, session, management_room, space_room, access_token FROM "user" WHERE rowid=$1`, rowID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetByMXID(ctx context.Context, userID id.UserID) (*User, error) {
|
||||||
|
return get[*User](uq, ctx, `SELECT rowid, mxid, phone, session, management_room, space_room, access_token FROM "user" WHERE mxid=$1`, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uq *UserQuery) GetByPhone(ctx context.Context, phone string) (*User, error) {
|
||||||
|
return get[*User](uq, ctx, `SELECT rowid, mxid, phone, session, management_room, space_room, access_token FROM "user" WHERE phone=$1`, phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
WebAuthKey []byte `json:"web_auth_key"`
|
||||||
|
AESKey []byte `json:"aes_key"`
|
||||||
|
HMACKey []byte `json:"hmac_key"`
|
||||||
|
|
||||||
|
PhoneInfo *binary.Device `json:"phone_info"`
|
||||||
|
BrowserInfo *binary.Device `json:"browser_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
db *Database
|
||||||
|
|
||||||
|
RowID int
|
||||||
|
MXID id.UserID
|
||||||
|
Phone string
|
||||||
|
Session *Session
|
||||||
|
|
||||||
|
ManagementRoom id.RoomID
|
||||||
|
SpaceRoom id.RoomID
|
||||||
|
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Scan(row dbutil.Scannable) (*User, error) {
|
||||||
|
var phone, session, managementRoom, spaceRoom, accessToken sql.NullString
|
||||||
|
err := row.Scan(&user.RowID, &user.MXID, &phone, &session, &managementRoom, &spaceRoom, &accessToken)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if session.String != "" {
|
||||||
|
var sess Session
|
||||||
|
err = json.Unmarshal([]byte(session.String), &sess)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse session: %w", err)
|
||||||
|
}
|
||||||
|
user.Session = &sess
|
||||||
|
}
|
||||||
|
user.Phone = phone.String
|
||||||
|
user.AccessToken = accessToken.String
|
||||||
|
user.ManagementRoom = id.RoomID(managementRoom.String)
|
||||||
|
user.SpaceRoom = id.RoomID(spaceRoom.String)
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) sqlVariables() []any {
|
||||||
|
var phone, session, managementRoom, spaceRoom, accessToken *string
|
||||||
|
if user.Phone != "" {
|
||||||
|
phone = &user.Phone
|
||||||
|
}
|
||||||
|
if user.Session != nil {
|
||||||
|
data, _ := json.Marshal(user.Session)
|
||||||
|
strData := string(data)
|
||||||
|
session = &strData
|
||||||
|
}
|
||||||
|
if user.ManagementRoom != "" {
|
||||||
|
managementRoom = (*string)(&user.ManagementRoom)
|
||||||
|
}
|
||||||
|
if user.SpaceRoom != "" {
|
||||||
|
spaceRoom = (*string)(&user.SpaceRoom)
|
||||||
|
}
|
||||||
|
if user.AccessToken != "" {
|
||||||
|
accessToken = &user.AccessToken
|
||||||
|
}
|
||||||
|
return []any{user.MXID, phone, session, managementRoom, spaceRoom, accessToken}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Insert(ctx context.Context) error {
|
||||||
|
err := user.db.Conn(ctx).
|
||||||
|
QueryRowContext(ctx, `INSERT INTO "user" (mxid, phone, session, management_room, space_room, access_token) VALUES ($1, $2, $3, $4, $5, $6) RETURNING rowid`, user.sqlVariables()...).
|
||||||
|
Scan(&user.RowID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Update(ctx context.Context) error {
|
||||||
|
_, err := user.db.Conn(ctx).ExecContext(ctx, `UPDATE "user" SET phone=$2, session=$3, management_room=$4, space_room=$5, access_token=$6 WHERE mxid=$1`, user.sqlVariables()...)
|
||||||
|
return err
|
||||||
|
}
|
36
docker-run.sh
Executable file
36
docker-run.sh
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [[ -z "$GID" ]]; then
|
||||||
|
GID="$UID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Define functions.
|
||||||
|
function fixperms {
|
||||||
|
chown -R $UID:$GID /data
|
||||||
|
|
||||||
|
# /opt/mautrix-gmessages is read-only, so disable file logging if it's pointing there.
|
||||||
|
if [[ "$(yq e '.logging.writers[1].filename' /data/config.yaml)" == "./logs/mautrix-gmessages.log" ]]; then
|
||||||
|
yq -I4 e -i 'del(.logging.writers[1])' /data/config.yaml
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f /data/config.yaml ]]; then
|
||||||
|
cp /opt/mautrix-gmessages/example-config.yaml /data/config.yaml
|
||||||
|
echo "Didn't find a config file."
|
||||||
|
echo "Copied default config file to /data/config.yaml"
|
||||||
|
echo "Modify that config file to your liking."
|
||||||
|
echo "Start the container again after that to generate the registration file."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f /data/registration.yaml ]]; then
|
||||||
|
/usr/bin/mautrix-gmessages -g -c /data/config.yaml -r /data/registration.yaml || exit $?
|
||||||
|
echo "Didn't find a registration file."
|
||||||
|
echo "Generated one for you."
|
||||||
|
echo "See https://docs.mau.fi/bridges/general/registering-appservices.html on how to use it."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd /data
|
||||||
|
fixperms
|
||||||
|
exec su-exec $UID:$GID /usr/bin/mautrix-gmessages
|
282
example-config.yaml
Normal file
282
example-config.yaml
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
# 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 google messages 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:29336
|
||||||
|
|
||||||
|
# The hostname and port where this appservice should listen.
|
||||||
|
hostname: 0.0.0.0
|
||||||
|
port: 29336
|
||||||
|
|
||||||
|
# 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: gmessages
|
||||||
|
# Appservice bot details.
|
||||||
|
bot:
|
||||||
|
# Username of the appservice bot.
|
||||||
|
username: gmessagesbot
|
||||||
|
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
||||||
|
# to leave display name/avatar as-is.
|
||||||
|
displayname: Google Messages bridge bot
|
||||||
|
avatar: mxc://maunium.net/yGOdcrJcwqARZqdzbfuxfhzb
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# Segment API key to track some events, like provisioning API login and encryption errors.
|
||||||
|
segment_key: null
|
||||||
|
# Optional user_id to use when sending Segment events. If null, defaults to using mxID.
|
||||||
|
segment_user_id: null
|
||||||
|
|
||||||
|
# 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:8001
|
||||||
|
|
||||||
|
# Bridge config
|
||||||
|
bridge:
|
||||||
|
# Localpart template of MXIDs for SMS users.
|
||||||
|
# {{.}} is replaced with an identifier of the recipient.
|
||||||
|
username_template: gmessages_{{.}}
|
||||||
|
# Displayname template for SMS users.
|
||||||
|
# {{.FullName}} - Full name provided by the phone
|
||||||
|
# {{.FirstName}} - First name provided by the phone
|
||||||
|
# {{.PhoneNumber}} - Formatted phone number provided by the phone
|
||||||
|
displayname_template: "{{or .FullName .PhoneNumber}}"
|
||||||
|
# Should the bridge create a space for each logged-in user and add bridged rooms to it?
|
||||||
|
personal_filtering_spaces: true
|
||||||
|
# Should the bridge send a read receipt from the bridge bot when a message has been sent to the phone?
|
||||||
|
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
|
||||||
|
|
||||||
|
portal_message_buffer: 128
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 Matrix m.notice-type messages be bridged?
|
||||||
|
bridge_notices: true
|
||||||
|
# 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
|
||||||
|
# When using double puppeting, should muted chats be muted in Matrix?
|
||||||
|
mute_bridging: false
|
||||||
|
# When using double puppeting, should archived chats be moved to a specific tag in Matrix?
|
||||||
|
# This can be set to a tag (e.g. m.lowpriority), or null to disable.
|
||||||
|
archive_tag: null
|
||||||
|
# Same as above, but for pinned chats. The favorite tag is called m.favourite
|
||||||
|
pinned_tag: null
|
||||||
|
# Should mute status and tags only be bridged when the portal room is created?
|
||||||
|
tag_only_on_create: true
|
||||||
|
# Whether or not created rooms should have federation enabled.
|
||||||
|
# If false, created portal rooms will never be federated.
|
||||||
|
federate_rooms: true
|
||||||
|
# Should the bridge never send alerts to the bridge management room?
|
||||||
|
# These are mostly things like the user being logged out.
|
||||||
|
disable_bridge_alerts: false
|
||||||
|
# Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552.
|
||||||
|
# This is currently not supported in most clients.
|
||||||
|
caption_in_message: false
|
||||||
|
|
||||||
|
# The prefix for commands. Only required in non-management rooms.
|
||||||
|
command_prefix: "!gm"
|
||||||
|
|
||||||
|
# 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 Google Messages 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 SMS 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Permissions for using the bridge.
|
||||||
|
# Permitted values:
|
||||||
|
# user - Access to use the bridge to link their own Google Messages on android.
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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-gmessages.log
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 10
|
||||||
|
compress: true
|
46
go.mod
46
go.mod
|
@ -1,3 +1,49 @@
|
||||||
module go.mau.fi/mautrix-gmessages
|
module go.mau.fi/mautrix-gmessages
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
|
github.com/rs/zerolog v1.29.1
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
go.mau.fi/mautrix-gmessages/libgm v0.1.0
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1
|
||||||
|
maunium.net/go/mautrix v0.15.4-0.20230628151140-e99578a15474
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/yuin/goldmark v1.5.4 // indirect
|
||||||
|
go.mau.fi/zeroconfig v0.1.2 // indirect
|
||||||
|
golang.org/x/crypto v0.10.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||||
|
golang.org/x/net v0.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.9.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exclude some things that cause go.sum to explode
|
||||||
|
exclude (
|
||||||
|
cloud.google.com/go v0.65.0
|
||||||
|
github.com/prometheus/client_golang v1.12.1
|
||||||
|
google.golang.org/appengine v1.6.6
|
||||||
|
)
|
||||||
|
|
||||||
|
replace go.mau.fi/mautrix-gmessages/libgm => ./libgm
|
||||||
|
|
83
go.sum
Normal file
83
go.sum
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||||
|
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||||
|
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
|
||||||
|
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||||
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||||
|
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||||
|
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||||
|
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||||
|
maunium.net/go/mautrix v0.15.4-0.20230628151140-e99578a15474 h1:Pzopg6NL7qaNdG2iUf8dVnu9+tVEa82b44KJKmpb/NY=
|
||||||
|
maunium.net/go/mautrix v0.15.4-0.20230628151140-e99578a15474/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE=
|
175
main.go
Normal file
175
main.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 (
|
||||||
|
_ "embed"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/bridge/commands"
|
||||||
|
"maunium.net/go/mautrix/bridge/status"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/util/configupgrade"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-gmessages/config"
|
||||||
|
"go.mau.fi/mautrix-gmessages/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed example-config.yaml
|
||||||
|
var ExampleConfig string
|
||||||
|
|
||||||
|
type GMBridge struct {
|
||||||
|
bridge.Bridge
|
||||||
|
Config *config.Config
|
||||||
|
DB *database.Database
|
||||||
|
//Provisioning *ProvisioningAPI
|
||||||
|
|
||||||
|
usersByMXID map[id.UserID]*User
|
||||||
|
usersByPhone map[string]*User
|
||||||
|
usersLock sync.Mutex
|
||||||
|
spaceRooms map[id.RoomID]*User
|
||||||
|
spaceRoomsLock sync.Mutex
|
||||||
|
managementRooms map[id.RoomID]*User
|
||||||
|
managementRoomsLock sync.Mutex
|
||||||
|
portalsByMXID map[id.RoomID]*Portal
|
||||||
|
portalsByKey map[database.Key]*Portal
|
||||||
|
portalsLock sync.Mutex
|
||||||
|
puppetsByKey map[database.Key]*Puppet
|
||||||
|
puppetsLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) Init() {
|
||||||
|
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
|
||||||
|
br.RegisterCommands()
|
||||||
|
|
||||||
|
Segment.log = br.ZLog.With().Str("component", "segment").Logger()
|
||||||
|
Segment.key = br.Config.SegmentKey
|
||||||
|
Segment.userID = br.Config.SegmentUserID
|
||||||
|
if Segment.IsEnabled() {
|
||||||
|
Segment.log.Info().Msg("Segment metrics are enabled")
|
||||||
|
if Segment.userID != "" {
|
||||||
|
Segment.log.Info().Str("user_id", Segment.userID).Msg("Overriding Segment user ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
br.DB = database.New(br.Bridge.DB)
|
||||||
|
|
||||||
|
ss := br.Config.Bridge.Provisioning.SharedSecret
|
||||||
|
if len(ss) > 0 && ss != "disable" {
|
||||||
|
//br.Provisioning = &ProvisioningAPI{bridge: br}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) Start() {
|
||||||
|
//if br.Provisioning != nil {
|
||||||
|
// br.ZLog.Debug().Msg("Initializing provisioning API")
|
||||||
|
// br.Provisioning.Init()
|
||||||
|
//}
|
||||||
|
br.WaitWebsocketConnected()
|
||||||
|
go br.StartUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) StartUsers() {
|
||||||
|
br.ZLog.Debug().Msg("Starting users")
|
||||||
|
foundAnySessions := false
|
||||||
|
for _, user := range br.GetAllUsersWithSession() {
|
||||||
|
foundAnySessions = true
|
||||||
|
go user.Connect()
|
||||||
|
}
|
||||||
|
if !foundAnySessions {
|
||||||
|
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
|
||||||
|
}
|
||||||
|
br.ZLog.Debug().Msg("Starting custom puppets")
|
||||||
|
for _, loopuser := range br.GetAllUsersWithDoublePuppet() {
|
||||||
|
go func(user *User) {
|
||||||
|
user.zlog.Debug().Msg("Starting double puppet")
|
||||||
|
err := user.startCustomMXID(true)
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to start double puppet")
|
||||||
|
}
|
||||||
|
}(loopuser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) Stop() {
|
||||||
|
for _, user := range br.usersByPhone {
|
||||||
|
if user.Client == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
br.ZLog.Debug().Str("user_id", user.MXID.String()).Msg("Disconnecting user")
|
||||||
|
user.Client.Disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetExampleConfig() string {
|
||||||
|
return ExampleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetConfigPtr() interface{} {
|
||||||
|
br.Config = &config.Config{
|
||||||
|
BaseConfig: &br.Bridge.Config,
|
||||||
|
}
|
||||||
|
br.Config.BaseConfig.Bridge = &br.Config.Bridge
|
||||||
|
return br.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
br := &GMBridge{
|
||||||
|
usersByMXID: make(map[id.UserID]*User),
|
||||||
|
usersByPhone: make(map[string]*User),
|
||||||
|
spaceRooms: make(map[id.RoomID]*User),
|
||||||
|
managementRooms: make(map[id.RoomID]*User),
|
||||||
|
portalsByMXID: make(map[id.RoomID]*Portal),
|
||||||
|
portalsByKey: make(map[database.Key]*Portal),
|
||||||
|
puppetsByKey: make(map[database.Key]*Puppet),
|
||||||
|
}
|
||||||
|
br.Bridge = bridge.Bridge{
|
||||||
|
Name: "mautrix-gmessages",
|
||||||
|
URL: "https://github.com/mautrix/gmessages",
|
||||||
|
Description: "A Matrix-Google Messages puppeting bridge.",
|
||||||
|
Version: "0.1.0",
|
||||||
|
ProtocolName: "Google Messages",
|
||||||
|
BeeperServiceName: "googlesms",
|
||||||
|
BeeperNetworkName: "googlesms",
|
||||||
|
|
||||||
|
CryptoPickleKey: "go.mau.fi/mautrix-gmessages",
|
||||||
|
|
||||||
|
ConfigUpgrader: &configupgrade.StructUpgrader{
|
||||||
|
SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
|
||||||
|
Blocks: config.SpacedBlocks,
|
||||||
|
Base: ExampleConfig,
|
||||||
|
},
|
||||||
|
|
||||||
|
Child: br,
|
||||||
|
}
|
||||||
|
br.InitVersion(Tag, Commit, BuildTime)
|
||||||
|
|
||||||
|
br.Main()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) CreatePrivatePortal(roomID id.RoomID, brUser bridge.User, brGhost bridge.Ghost) {
|
||||||
|
//TODO implement?
|
||||||
|
}
|
306
messagetracking.go
Normal file
306
messagetracking.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/bridge/status"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errUserNotConnected = errors.New("you are not connected to WhatsApp")
|
||||||
|
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")
|
||||||
|
errMNoticeDisabled = errors.New("bridging m.notice messages is disabled")
|
||||||
|
errUnexpectedParsedContentType = errors.New("unexpected parsed content type")
|
||||||
|
errInvalidGeoURI = errors.New("invalid `geo:` URI in message")
|
||||||
|
errUnknownMsgType = errors.New("unknown msgtype")
|
||||||
|
errMediaUnsupportedType = errors.New("unsupported media type")
|
||||||
|
errTargetNotFound = errors.New("target event not found")
|
||||||
|
errReactionDatabaseNotFound = errors.New("reaction database entry not found")
|
||||||
|
errReactionTargetNotFound = errors.New("reaction target message not found")
|
||||||
|
errTargetIsFake = errors.New("target is a fake event")
|
||||||
|
errReactionSentBySomeoneElse = errors.New("target reaction was sent by someone else")
|
||||||
|
errDMSentByOtherUser = errors.New("target message was sent by the other user in a DM")
|
||||||
|
errPollMissingQuestion = errors.New("poll message is missing question")
|
||||||
|
errPollDuplicateOption = errors.New("poll options must be unique")
|
||||||
|
|
||||||
|
errEditUnknownTarget = errors.New("unknown edit target message")
|
||||||
|
errEditUnknownTargetType = errors.New("unsupported edited message type")
|
||||||
|
errEditDifferentSender = errors.New("can't edit message sent by another user")
|
||||||
|
errEditTooOld = errors.New("message is too old to be edited")
|
||||||
|
|
||||||
|
errBroadcastReactionNotSupported = errors.New("reacting to status messages is not currently supported")
|
||||||
|
errBroadcastSendDisabled = errors.New("sending status messages is disabled")
|
||||||
|
|
||||||
|
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, errUnknownMsgType),
|
||||||
|
errors.Is(err, errInvalidGeoURI),
|
||||||
|
errors.Is(err, errBroadcastReactionNotSupported),
|
||||||
|
errors.Is(err, errBroadcastSendDisabled):
|
||||||
|
return event.MessageStatusUnsupported, event.MessageStatusFail, true, true, ""
|
||||||
|
case errors.Is(err, errMNoticeDisabled):
|
||||||
|
return event.MessageStatusUnsupported, event.MessageStatusFail, true, false, ""
|
||||||
|
case errors.Is(err, errMediaUnsupportedType),
|
||||||
|
errors.Is(err, errPollMissingQuestion),
|
||||||
|
errors.Is(err, errPollDuplicateOption),
|
||||||
|
errors.Is(err, errEditDifferentSender),
|
||||||
|
errors.Is(err, errEditTooOld),
|
||||||
|
errors.Is(err, errEditUnknownTarget),
|
||||||
|
errors.Is(err, errEditUnknownTargetType):
|
||||||
|
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, errTargetNotFound),
|
||||||
|
errors.Is(err, errTargetIsFake),
|
||||||
|
errors.Is(err, errReactionDatabaseNotFound),
|
||||||
|
errors.Is(err, errReactionTargetNotFound),
|
||||||
|
errors.Is(err, errReactionSentBySomeoneElse),
|
||||||
|
errors.Is(err, errDMSentByOtherUser):
|
||||||
|
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):
|
||||||
|
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, false, ""
|
||||||
|
default:
|
||||||
|
return event.MessageStatusGenericError, event.MessageStatusRetriable, false, true, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendErrorMessage(evt *event.Event, err error, msgType string, confirmed bool, editID id.EventID) id.EventID {
|
||||||
|
if !portal.bridge.Config.Bridge.MessageErrorNotices {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
certainty := "may not have been"
|
||||||
|
if confirmed {
|
||||||
|
certainty = "was not"
|
||||||
|
}
|
||||||
|
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(content)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Warnfln("Failed to send bridging error message:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resp.EventID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendStatusEvent(evtID, lastRetry id.EventID, err error) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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(portal.MXID, event.BeeperMessageStatus, &content)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Warnln("Failed to send message status event:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) {
|
||||||
|
if portal.bridge.Config.Bridge.DeliveryReceipts {
|
||||||
|
err := portal.bridge.Bot.SendReceipt(portal.MXID, eventID, event.ReceiptTypeRead, nil)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Debugfln("Failed to send delivery receipt for %s: %v", eventID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part string, ms *metricSender) {
|
||||||
|
var msgType string
|
||||||
|
switch evt.Type {
|
||||||
|
case event.EventMessage:
|
||||||
|
msgType = "message"
|
||||||
|
case event.EventReaction:
|
||||||
|
msgType = "reaction"
|
||||||
|
case event.EventRedaction:
|
||||||
|
msgType = "redaction"
|
||||||
|
default:
|
||||||
|
msgType = "unknown event"
|
||||||
|
}
|
||||||
|
evtDescription := evt.ID.String()
|
||||||
|
if evt.Type == event.EventRedaction {
|
||||||
|
evtDescription += fmt.Sprintf(" of %s", evt.Redacts)
|
||||||
|
}
|
||||||
|
origEvtID := evt.ID
|
||||||
|
if retryMeta := evt.Content.AsMessage().MessageSendRetry; retryMeta != nil {
|
||||||
|
origEvtID = retryMeta.OriginalEventID
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
level := log.LevelError
|
||||||
|
if part == "Ignoring" {
|
||||||
|
level = log.LevelDebug
|
||||||
|
}
|
||||||
|
portal.log.Logfln(level, "%s %s %s from %s: %v", part, msgType, evtDescription, evt.Sender, err)
|
||||||
|
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(evt, err, msgType, isCertain, ms.getNoticeID()))
|
||||||
|
}
|
||||||
|
portal.sendStatusEvent(origEvtID, evt.ID, err)
|
||||||
|
} else {
|
||||||
|
portal.log.Debugfln("Handled Matrix %s %s", msgType, evtDescription)
|
||||||
|
portal.sendDeliveryReceipt(evt.ID)
|
||||||
|
portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, ms.getRetryNum())
|
||||||
|
portal.sendStatusEvent(origEvtID, evt.ID, nil)
|
||||||
|
if prevNotice := ms.popNoticeID(); prevNotice != "" {
|
||||||
|
_, _ = portal.MainIntent().RedactEvent(portal.MXID, prevNotice, mautrix.ReqRedact{
|
||||||
|
Reason: "error resolved",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ms != nil {
|
||||||
|
portal.log.Debugfln("Timings for %s: %s", evt.ID, ms.timings.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) String() string {
|
||||||
|
mt.initReceive = niceRound(mt.initReceive)
|
||||||
|
mt.decrypt = niceRound(mt.decrypt)
|
||||||
|
mt.portalQueue = niceRound(mt.portalQueue)
|
||||||
|
mt.totalReceive = niceRound(mt.totalReceive)
|
||||||
|
mt.implicitRR = niceRound(mt.implicitRR)
|
||||||
|
mt.preproc = niceRound(mt.preproc)
|
||||||
|
mt.convert = niceRound(mt.convert)
|
||||||
|
mt.totalSend = niceRound(mt.totalSend)
|
||||||
|
return fmt.Sprintf("BRIDGE: receive: %s, decrypt: %s, queue: %s, total hs->portal: %s, implicit rr: %s -- PORTAL: preprocess: %s, convert: %s, total send: %s", mt.initReceive, mt.decrypt, mt.implicitRR, mt.portalQueue, mt.totalReceive, mt.preproc, mt.convert, mt.totalSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricSender struct {
|
||||||
|
portal *Portal
|
||||||
|
previousNotice id.EventID
|
||||||
|
lock sync.Mutex
|
||||||
|
completed bool
|
||||||
|
retryNum int
|
||||||
|
timings *messageTimings
|
||||||
|
}
|
||||||
|
|
||||||
|
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(evt, err, part, ms)
|
||||||
|
ms.retryNum++
|
||||||
|
ms.completed = completed
|
||||||
|
}
|
995
portal.go
Normal file
995
portal.go
Normal file
|
@ -0,0 +1,995 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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 (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
log "maunium.net/go/maulogger/v2"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/crypto/attachment"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-gmessages/database"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/binary"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (br *GMBridge) GetPortalByMXID(mxid id.RoomID) *Portal {
|
||||||
|
br.portalsLock.Lock()
|
||||||
|
defer br.portalsLock.Unlock()
|
||||||
|
portal, ok := br.portalsByMXID[mxid]
|
||||||
|
if !ok {
|
||||||
|
dbPortal, err := br.DB.Portal.GetByMXID(context.TODO(), mxid)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Str("mxid", mxid.String()).Msg("Failed to get portal from database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return br.loadDBPortal(dbPortal, nil)
|
||||||
|
}
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetIPortal(mxid id.RoomID) bridge.Portal {
|
||||||
|
p := br.GetPortalByMXID(mxid)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) IsEncrypted() bool {
|
||||||
|
return portal.Encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) MarkEncrypted() {
|
||||||
|
portal.Encrypted = true
|
||||||
|
err := portal.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to save portal to database after marking it as encrypted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) ReceiveMatrixEvent(brUser bridge.User, evt *event.Event) {
|
||||||
|
user := brUser.(*User)
|
||||||
|
if user.RowID == portal.Receiver {
|
||||||
|
portal.matrixMessages <- PortalMatrixMessage{user: user, evt: evt, receivedAt: time.Now()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetPortalByKey(key database.Key) *Portal {
|
||||||
|
br.portalsLock.Lock()
|
||||||
|
defer br.portalsLock.Unlock()
|
||||||
|
portal, ok := br.portalsByKey[key]
|
||||||
|
if !ok {
|
||||||
|
dbPortal, err := br.DB.Portal.GetByKey(context.TODO(), key)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to get portal from database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return br.loadDBPortal(dbPortal, &key)
|
||||||
|
}
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetExistingPortalByKey(key database.Key) *Portal {
|
||||||
|
br.portalsLock.Lock()
|
||||||
|
defer br.portalsLock.Unlock()
|
||||||
|
portal, ok := br.portalsByKey[key]
|
||||||
|
if !ok {
|
||||||
|
dbPortal, err := br.DB.Portal.GetByKey(context.TODO(), key)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to get portal from database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return br.loadDBPortal(dbPortal, nil)
|
||||||
|
}
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllPortals() []*Portal {
|
||||||
|
return br.loadManyPortals(br.DB.Portal.GetAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllPortalsForUser(userID int) []*Portal {
|
||||||
|
return br.loadManyPortals(func(ctx context.Context) ([]*database.Portal, error) {
|
||||||
|
return br.DB.Portal.GetAllForUser(ctx, userID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllIPortals() (iportals []bridge.Portal) {
|
||||||
|
portals := br.GetAllPortals()
|
||||||
|
iportals = make([]bridge.Portal, len(portals))
|
||||||
|
for i, portal := range portals {
|
||||||
|
iportals[i] = portal
|
||||||
|
}
|
||||||
|
return iportals
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) loadManyPortals(query func(ctx context.Context) ([]*database.Portal, error)) []*Portal {
|
||||||
|
br.portalsLock.Lock()
|
||||||
|
defer br.portalsLock.Unlock()
|
||||||
|
dbPortals, err := query(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Msg("Failed to load all portals from database")
|
||||||
|
return []*Portal{}
|
||||||
|
}
|
||||||
|
output := make([]*Portal, len(dbPortals))
|
||||||
|
for index, dbPortal := range dbPortals {
|
||||||
|
if dbPortal == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
portal, ok := br.portalsByKey[dbPortal.Key]
|
||||||
|
if !ok {
|
||||||
|
portal = br.loadDBPortal(dbPortal, nil)
|
||||||
|
}
|
||||||
|
output[index] = portal
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) loadDBPortal(dbPortal *database.Portal, key *database.Key) *Portal {
|
||||||
|
if dbPortal == nil {
|
||||||
|
if key == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dbPortal = br.DB.Portal.New()
|
||||||
|
dbPortal.Key = *key
|
||||||
|
err := dbPortal.Insert(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to insert portal")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portal := br.NewPortal(dbPortal)
|
||||||
|
br.portalsByKey[portal.Key] = portal
|
||||||
|
if len(portal.MXID) > 0 {
|
||||||
|
br.portalsByMXID[portal.MXID] = portal
|
||||||
|
}
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) GetUsers() []*User {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) newBlankPortal(key database.Key) *Portal {
|
||||||
|
portal := &Portal{
|
||||||
|
bridge: br,
|
||||||
|
log: br.Log.Sub(fmt.Sprintf("Portal/%s", key.ID)),
|
||||||
|
zlog: br.ZLog.With().Str("portal_id", key.ID).Int("portal_receiver", key.Receiver).Logger(),
|
||||||
|
|
||||||
|
messages: make(chan PortalMessage, br.Config.Bridge.PortalMessageBuffer),
|
||||||
|
matrixMessages: make(chan PortalMatrixMessage, br.Config.Bridge.PortalMessageBuffer),
|
||||||
|
|
||||||
|
outgoingMessages: make(map[string]id.EventID),
|
||||||
|
}
|
||||||
|
go portal.handleMessageLoop()
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) NewPortal(dbPortal *database.Portal) *Portal {
|
||||||
|
portal := br.newBlankPortal(dbPortal.Key)
|
||||||
|
portal.Portal = dbPortal
|
||||||
|
return portal
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentlyHandledLength = 100
|
||||||
|
|
||||||
|
type PortalMessage struct {
|
||||||
|
evt *binary.Message
|
||||||
|
source *User
|
||||||
|
}
|
||||||
|
|
||||||
|
type PortalMatrixMessage struct {
|
||||||
|
evt *event.Event
|
||||||
|
user *User
|
||||||
|
receivedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Portal struct {
|
||||||
|
*database.Portal
|
||||||
|
|
||||||
|
bridge *GMBridge
|
||||||
|
// Deprecated: use zerolog
|
||||||
|
log log.Logger
|
||||||
|
zlog zerolog.Logger
|
||||||
|
|
||||||
|
roomCreateLock sync.Mutex
|
||||||
|
encryptLock sync.Mutex
|
||||||
|
backfillLock sync.Mutex
|
||||||
|
avatarLock sync.Mutex
|
||||||
|
|
||||||
|
latestEventBackfillLock sync.Mutex
|
||||||
|
|
||||||
|
recentlyHandled [recentlyHandledLength]string
|
||||||
|
recentlyHandledLock sync.Mutex
|
||||||
|
recentlyHandledIndex uint8
|
||||||
|
|
||||||
|
outgoingMessages map[string]id.EventID
|
||||||
|
outgoingMessagesLock sync.Mutex
|
||||||
|
|
||||||
|
currentlyTyping []id.UserID
|
||||||
|
currentlyTypingLock sync.Mutex
|
||||||
|
|
||||||
|
messages chan PortalMessage
|
||||||
|
matrixMessages chan PortalMatrixMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ bridge.Portal = (*Portal)(nil)
|
||||||
|
//_ bridge.ReadReceiptHandlingPortal = (*Portal)(nil)
|
||||||
|
//_ bridge.MembershipHandlingPortal = (*Portal)(nil)
|
||||||
|
//_ bridge.MetaHandlingPortal = (*Portal)(nil)
|
||||||
|
//_ bridge.TypingPortal = (*Portal)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (portal *Portal) handleMessageLoopItem(msg PortalMessage) {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
portal.latestEventBackfillLock.Lock()
|
||||||
|
defer portal.latestEventBackfillLock.Unlock()
|
||||||
|
switch {
|
||||||
|
case msg.evt != nil:
|
||||||
|
portal.handleMessage(msg.source, msg.evt)
|
||||||
|
//case msg.receipt != nil:
|
||||||
|
// portal.handleReceipt(msg.receipt, msg.source)
|
||||||
|
default:
|
||||||
|
portal.zlog.Warn().Interface("portal_message", msg).Msg("Unexpected PortalMessage with no message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) {
|
||||||
|
portal.latestEventBackfillLock.Lock()
|
||||||
|
defer portal.latestEventBackfillLock.Unlock()
|
||||||
|
evtTS := time.UnixMilli(msg.evt.Timestamp)
|
||||||
|
timings := messageTimings{
|
||||||
|
initReceive: msg.evt.Mautrix.ReceivedAt.Sub(evtTS),
|
||||||
|
decrypt: msg.evt.Mautrix.DecryptionDuration,
|
||||||
|
portalQueue: time.Since(msg.receivedAt),
|
||||||
|
totalReceive: time.Since(evtTS),
|
||||||
|
}
|
||||||
|
switch msg.evt.Type {
|
||||||
|
case event.EventMessage, event.EventSticker:
|
||||||
|
portal.HandleMatrixMessage(msg.user, msg.evt, timings)
|
||||||
|
case event.EventReaction:
|
||||||
|
portal.HandleMatrixReaction(msg.user, msg.evt)
|
||||||
|
default:
|
||||||
|
portal.zlog.Warn().
|
||||||
|
Str("event_type", msg.evt.Type.Type).
|
||||||
|
Msg("Unsupported event type in portal message channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) handleMessageLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-portal.messages:
|
||||||
|
portal.handleMessageLoopItem(msg)
|
||||||
|
case msg := <-portal.matrixMessages:
|
||||||
|
portal.handleMatrixMessageLoopItem(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) isOutgoingMessage(evt *binary.Message) id.EventID {
|
||||||
|
portal.outgoingMessagesLock.Lock()
|
||||||
|
defer portal.outgoingMessagesLock.Unlock()
|
||||||
|
evtID, ok := portal.outgoingMessages[evt.TmpId]
|
||||||
|
if ok {
|
||||||
|
portal.markHandled(evt, map[string]id.EventID{"": evtID}, true)
|
||||||
|
return evtID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) handleMessage(source *User, evt *binary.Message) {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
portal.zlog.Warn().Msg("handleMessage called even though portal.MXID is empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log := portal.zlog.With().
|
||||||
|
Str("message_id", evt.MessageID).
|
||||||
|
Str("participant_id", evt.ParticipantID).
|
||||||
|
Str("action", "handleMessage").
|
||||||
|
Logger()
|
||||||
|
if evtID := portal.isOutgoingMessage(evt); evtID != "" {
|
||||||
|
log.Debug().Str("event_id", evtID.String()).Msg("Got echo for outgoing message")
|
||||||
|
return
|
||||||
|
} else if portal.isRecentlyHandled(evt.MessageID) {
|
||||||
|
log.Debug().Msg("Not handling recent duplicate message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existingMsg, err := portal.bridge.DB.Message.GetByID(context.TODO(), portal.Key, evt.MessageID)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to check if message is duplicate")
|
||||||
|
} else if existingMsg != nil {
|
||||||
|
log.Debug().Msg("Not handling duplicate message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var intent *appservice.IntentAPI
|
||||||
|
if evt.GetFrom().GetFromMe() {
|
||||||
|
intent = source.DoublePuppetIntent
|
||||||
|
if intent == nil {
|
||||||
|
log.Debug().Msg("Dropping message from self as double puppeting is not enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
puppet := source.GetPuppetByID(evt.ParticipantID, "")
|
||||||
|
if puppet == nil {
|
||||||
|
log.Debug().Msg("Dropping message from unknown participant")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intent = puppet.IntentFor(portal)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIDs := make(map[string]id.EventID)
|
||||||
|
var lastEventID id.EventID
|
||||||
|
ts := time.UnixMicro(evt.Timestamp).UnixMilli()
|
||||||
|
for _, part := range evt.MessageInfo {
|
||||||
|
var content event.MessageEventContent
|
||||||
|
switch data := part.GetData().(type) {
|
||||||
|
case *binary.MessageInfo_MessageContent:
|
||||||
|
content = event.MessageEventContent{
|
||||||
|
MsgType: event.MsgText,
|
||||||
|
Body: data.MessageContent.GetContent(),
|
||||||
|
}
|
||||||
|
case *binary.MessageInfo_ImageContent:
|
||||||
|
content = event.MessageEventContent{
|
||||||
|
MsgType: event.MsgNotice,
|
||||||
|
Body: fmt.Sprintf("Attachment %s", data.ImageContent.GetImageName()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := portal.sendMessage(intent, event.EventMessage, &content, nil, ts)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to send message")
|
||||||
|
} else {
|
||||||
|
eventIDs[part.OrderInternal] = resp.EventID
|
||||||
|
lastEventID = resp.EventID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portal.markHandled(evt, eventIDs, true)
|
||||||
|
portal.sendDeliveryReceipt(lastEventID)
|
||||||
|
log.Debug().Interface("event_ids", eventIDs).Msg("Handled message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) isRecentlyHandled(id string) bool {
|
||||||
|
start := portal.recentlyHandledIndex
|
||||||
|
for i := start; i != start; i = (i - 1) % recentlyHandledLength {
|
||||||
|
if portal.recentlyHandled[i] == id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) markHandled(info *binary.Message, mxids map[string]id.EventID, recent bool) *database.Message {
|
||||||
|
msg := portal.bridge.DB.Message.New()
|
||||||
|
msg.Chat = portal.Key
|
||||||
|
msg.ID = info.MessageID
|
||||||
|
for _, evtID := range mxids {
|
||||||
|
msg.MXID = evtID
|
||||||
|
}
|
||||||
|
msg.Timestamp = time.UnixMicro(info.Timestamp)
|
||||||
|
msg.Sender = info.ParticipantID
|
||||||
|
err := msg.Insert(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Str("message_id", info.MessageID).Msg("Failed to insert message to database")
|
||||||
|
}
|
||||||
|
|
||||||
|
if recent {
|
||||||
|
portal.recentlyHandledLock.Lock()
|
||||||
|
index := portal.recentlyHandledIndex
|
||||||
|
portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
|
||||||
|
portal.recentlyHandledLock.Unlock()
|
||||||
|
portal.recentlyHandled[index] = info.MessageID
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) SyncParticipants(source *User, metadata *binary.Conversation) (userIDs []id.UserID) {
|
||||||
|
var firstParticipant *binary.Participant
|
||||||
|
var manyParticipants bool
|
||||||
|
for _, participant := range metadata.Participants {
|
||||||
|
if participant.IsMe {
|
||||||
|
continue
|
||||||
|
} else if participant.Id.Number == "" {
|
||||||
|
portal.zlog.Warn().Interface("participant", participant).Msg("No number found in non-self participant entry")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if firstParticipant == nil {
|
||||||
|
firstParticipant = participant
|
||||||
|
} else {
|
||||||
|
manyParticipants = true
|
||||||
|
}
|
||||||
|
portal.zlog.Debug().Interface("participant", participant).Msg("Syncing participant")
|
||||||
|
puppet := source.GetPuppetByID(participant.Id.ParticipantID, participant.Id.Number)
|
||||||
|
userIDs = append(userIDs, puppet.MXID)
|
||||||
|
puppet.Sync(source, participant)
|
||||||
|
if portal.MXID != "" {
|
||||||
|
err := puppet.IntentFor(portal).EnsureJoined(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).
|
||||||
|
Str("user_id", puppet.MXID.String()).
|
||||||
|
Msg("Failed to ensure ghost is joined to portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !metadata.IsGroupChat && !manyParticipants {
|
||||||
|
portal.zlog.Info().
|
||||||
|
Str("old_other_user_id", portal.OtherUserID).
|
||||||
|
Str("new_other_user_id", firstParticipant.Id.ParticipantID).
|
||||||
|
Msg("Found other user ID in DM")
|
||||||
|
portal.OtherUserID = firstParticipant.Id.ParticipantID
|
||||||
|
}
|
||||||
|
return userIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) UpdateName(name string, updateInfo bool) bool {
|
||||||
|
if portal.Name != name || (!portal.NameSet && len(portal.MXID) > 0 && portal.shouldSetDMRoomMetadata()) {
|
||||||
|
portal.zlog.Debug().Str("old_name", portal.Name).Str("new_name", name).Msg("Updating name")
|
||||||
|
portal.Name = name
|
||||||
|
portal.NameSet = false
|
||||||
|
if updateInfo {
|
||||||
|
defer func() {
|
||||||
|
err := portal.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to save portal after updating name")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(portal.MXID) > 0 && !portal.shouldSetDMRoomMetadata() {
|
||||||
|
portal.UpdateBridgeInfo()
|
||||||
|
} else if len(portal.MXID) > 0 {
|
||||||
|
intent := portal.MainIntent()
|
||||||
|
_, err := intent.SetRoomName(portal.MXID, name)
|
||||||
|
if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
|
||||||
|
_, err = portal.MainIntent().SetRoomName(portal.MXID, name)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
portal.NameSet = true
|
||||||
|
if updateInfo {
|
||||||
|
portal.UpdateBridgeInfo()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
portal.zlog.Warn().Err(err).Msg("Failed to set room name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) UpdateMetadata(user *User, info *binary.Conversation) []id.UserID {
|
||||||
|
participants := portal.SyncParticipants(user, info)
|
||||||
|
if portal.IsPrivateChat() {
|
||||||
|
return participants
|
||||||
|
}
|
||||||
|
update := false
|
||||||
|
if portal.MXID != "" {
|
||||||
|
update = portal.addToPersonalSpace(user) || update
|
||||||
|
}
|
||||||
|
update = portal.UpdateName(info.Name, false) || update
|
||||||
|
// TODO avatar
|
||||||
|
if update {
|
||||||
|
err := portal.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to save portal after updating metadata")
|
||||||
|
}
|
||||||
|
if portal.MXID != "" {
|
||||||
|
portal.UpdateBridgeInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return participants
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) ensureUserInvited(user *User) bool {
|
||||||
|
return user.ensureInvited(portal.MainIntent(), portal.MXID, portal.IsPrivateChat())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) UpdateMatrixRoom(user *User, groupInfo *binary.Conversation) bool {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.ensureUserInvited(user)
|
||||||
|
portal.UpdateMetadata(user, groupInfo)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
|
||||||
|
anyone := 0
|
||||||
|
nope := 99
|
||||||
|
invite := 50
|
||||||
|
return &event.PowerLevelsEventContent{
|
||||||
|
UsersDefault: anyone,
|
||||||
|
EventsDefault: anyone,
|
||||||
|
RedactPtr: &anyone,
|
||||||
|
StateDefaultPtr: &nope,
|
||||||
|
BanPtr: &nope,
|
||||||
|
InvitePtr: &invite,
|
||||||
|
Users: map[id.UserID]int{
|
||||||
|
portal.MainIntent().UserID: 100,
|
||||||
|
},
|
||||||
|
Events: map[string]int{
|
||||||
|
event.StateRoomName.Type: anyone,
|
||||||
|
event.StateRoomAvatar.Type: anyone,
|
||||||
|
event.EventReaction.Type: anyone, // TODO only allow reactions in RCS rooms
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) getBridgeInfoStateKey() string {
|
||||||
|
return fmt.Sprintf("fi.mau.gmessages://gmessages/%s", portal.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) {
|
||||||
|
return portal.getBridgeInfoStateKey(), event.BridgeEventContent{
|
||||||
|
BridgeBot: portal.bridge.Bot.UserID,
|
||||||
|
Creator: portal.MainIntent().UserID,
|
||||||
|
Protocol: event.BridgeInfoSection{
|
||||||
|
ID: "gmessages",
|
||||||
|
DisplayName: "Google Messages",
|
||||||
|
AvatarURL: portal.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
|
||||||
|
ExternalURL: "https://messages.google.com/",
|
||||||
|
},
|
||||||
|
Channel: event.BridgeInfoSection{
|
||||||
|
ID: portal.ID,
|
||||||
|
DisplayName: portal.Name,
|
||||||
|
AvatarURL: portal.AvatarMXC.CUString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) UpdateBridgeInfo() {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
portal.zlog.Debug().Msg("Not updating bridge info: no Matrix room created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
portal.zlog.Debug().Msg("Updating bridge info...")
|
||||||
|
stateKey, content := portal.getBridgeInfo()
|
||||||
|
_, err := portal.MainIntent().SendStateEvent(portal.MXID, event.StateBridge, stateKey, content)
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Warn().Err(err).Msg("Failed to update m.bridge")
|
||||||
|
}
|
||||||
|
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
|
||||||
|
_, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateHalfShotBridge, stateKey, content)
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Warn().Err(err).Msg("Failed to update uk.half-shot.bridge")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) shouldSetDMRoomMetadata() bool {
|
||||||
|
return !portal.IsPrivateChat() ||
|
||||||
|
portal.bridge.Config.Bridge.PrivateChatPortalMeta == "always" ||
|
||||||
|
(portal.IsEncrypted() && portal.bridge.Config.Bridge.PrivateChatPortalMeta != "never")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) GetEncryptionEventContent() (evt *event.EncryptionEventContent) {
|
||||||
|
evt = &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}
|
||||||
|
if rot := portal.bridge.Config.Bridge.Encryption.Rotation; rot.EnableCustom {
|
||||||
|
evt.RotationPeriodMillis = rot.Milliseconds
|
||||||
|
evt.RotationPeriodMessages = rot.Messages
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) CreateMatrixRoom(user *User, conv *binary.Conversation) error {
|
||||||
|
portal.roomCreateLock.Lock()
|
||||||
|
defer portal.roomCreateLock.Unlock()
|
||||||
|
if len(portal.MXID) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
members := portal.UpdateMetadata(user, conv)
|
||||||
|
|
||||||
|
if portal.IsPrivateChat() && portal.GetDMPuppet() == nil {
|
||||||
|
portal.zlog.Error().Msg("Didn't find ghost of other user in DM :(")
|
||||||
|
return fmt.Errorf("ghost not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
intent := portal.MainIntent()
|
||||||
|
if err := intent.EnsureRegistered(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
portal.zlog.Info().Msg("Creating Matrix room")
|
||||||
|
|
||||||
|
bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
|
||||||
|
|
||||||
|
initialState := []*event.Event{{
|
||||||
|
Type: event.StatePowerLevels,
|
||||||
|
Content: event.Content{
|
||||||
|
Parsed: portal.GetBasePowerLevels(),
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Type: event.StateBridge,
|
||||||
|
Content: event.Content{Parsed: bridgeInfo},
|
||||||
|
StateKey: &bridgeInfoStateKey,
|
||||||
|
}, {
|
||||||
|
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
|
||||||
|
Type: event.StateHalfShotBridge,
|
||||||
|
Content: event.Content{Parsed: bridgeInfo},
|
||||||
|
StateKey: &bridgeInfoStateKey,
|
||||||
|
}}
|
||||||
|
var invite []id.UserID
|
||||||
|
if portal.bridge.Config.Bridge.Encryption.Default {
|
||||||
|
initialState = append(initialState, &event.Event{
|
||||||
|
Type: event.StateEncryption,
|
||||||
|
Content: event.Content{
|
||||||
|
Parsed: portal.GetEncryptionEventContent(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
portal.Encrypted = true
|
||||||
|
if portal.IsPrivateChat() {
|
||||||
|
invite = append(invite, portal.bridge.Bot.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !portal.AvatarMXC.IsEmpty() && portal.shouldSetDMRoomMetadata() {
|
||||||
|
initialState = append(initialState, &event.Event{
|
||||||
|
Type: event.StateRoomAvatar,
|
||||||
|
Content: event.Content{
|
||||||
|
Parsed: event.RoomAvatarEventContent{URL: portal.AvatarMXC},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
portal.AvatarSet = true
|
||||||
|
} else {
|
||||||
|
portal.AvatarSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
creationContent := make(map[string]interface{})
|
||||||
|
if !portal.bridge.Config.Bridge.FederateRooms {
|
||||||
|
creationContent["m.federate"] = false
|
||||||
|
}
|
||||||
|
autoJoinInvites := portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites)
|
||||||
|
if autoJoinInvites {
|
||||||
|
portal.zlog.Debug().Msg("Hungryserv mode: adding all group members in create request")
|
||||||
|
invite = append(invite, members...)
|
||||||
|
invite = append(invite, user.MXID)
|
||||||
|
}
|
||||||
|
req := &mautrix.ReqCreateRoom{
|
||||||
|
Visibility: "private",
|
||||||
|
Name: portal.Name,
|
||||||
|
Invite: invite,
|
||||||
|
Preset: "private_chat",
|
||||||
|
IsDirect: portal.IsPrivateChat(),
|
||||||
|
InitialState: initialState,
|
||||||
|
CreationContent: creationContent,
|
||||||
|
|
||||||
|
BeeperAutoJoinInvites: autoJoinInvites,
|
||||||
|
}
|
||||||
|
if !portal.shouldSetDMRoomMetadata() {
|
||||||
|
req.Name = ""
|
||||||
|
}
|
||||||
|
resp, err := intent.CreateRoom(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
portal.zlog.Info().Str("room_id", resp.RoomID.String()).Msg("Matrix room created")
|
||||||
|
portal.InSpace = false
|
||||||
|
portal.NameSet = len(req.Name) > 0
|
||||||
|
portal.MXID = resp.RoomID
|
||||||
|
portal.bridge.portalsLock.Lock()
|
||||||
|
portal.bridge.portalsByMXID[portal.MXID] = portal
|
||||||
|
portal.bridge.portalsLock.Unlock()
|
||||||
|
err = portal.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to save portal after creating room")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here.
|
||||||
|
inviteMembership := event.MembershipInvite
|
||||||
|
if autoJoinInvites {
|
||||||
|
inviteMembership = event.MembershipJoin
|
||||||
|
}
|
||||||
|
for _, userID := range invite {
|
||||||
|
portal.bridge.StateStore.SetMembership(portal.MXID, userID, inviteMembership)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoJoinInvites {
|
||||||
|
if !portal.IsPrivateChat() {
|
||||||
|
portal.SyncParticipants(user, conv)
|
||||||
|
} else {
|
||||||
|
if portal.bridge.Config.Bridge.Encryption.Default {
|
||||||
|
err = portal.bridge.Bot.EnsureJoined(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.UpdateDirectChats(map[id.UserID][]id.RoomID{portal.GetDMPuppet().MXID: {portal.MXID}})
|
||||||
|
}
|
||||||
|
portal.ensureUserInvited(user)
|
||||||
|
}
|
||||||
|
user.syncChatDoublePuppetDetails(portal, conv, true)
|
||||||
|
|
||||||
|
go portal.addToPersonalSpace(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) addToPersonalSpace(user *User) bool {
|
||||||
|
spaceID := user.GetSpaceRoom()
|
||||||
|
if len(spaceID) == 0 || portal.InSpace {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := portal.bridge.Bot.SendStateEvent(spaceID, event.StateSpaceChild, portal.MXID.String(), &event.SpaceChildEventContent{
|
||||||
|
Via: []string{portal.bridge.Config.Homeserver.Domain},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Str("space_id", spaceID.String()).Msg("Failed to add room to user's personal filtering space")
|
||||||
|
portal.InSpace = false
|
||||||
|
} else {
|
||||||
|
portal.zlog.Debug().Str("space_id", spaceID.String()).Msg("Added room to user's personal filtering space")
|
||||||
|
portal.InSpace = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) IsPrivateChat() bool {
|
||||||
|
return portal.OtherUserID != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) GetDMPuppet() *Puppet {
|
||||||
|
if portal.IsPrivateChat() {
|
||||||
|
puppet := portal.bridge.GetPuppetByKey(database.Key{Receiver: portal.Receiver, ID: portal.OtherUserID}, "")
|
||||||
|
return puppet
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) MainIntent() *appservice.IntentAPI {
|
||||||
|
if puppet := portal.GetDMPuppet(); puppet != nil {
|
||||||
|
return puppet.DefaultIntent()
|
||||||
|
}
|
||||||
|
return portal.bridge.Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendMainIntentMessage(content *event.MessageEventContent) (*mautrix.RespSendEvent, error) {
|
||||||
|
return portal.sendMessage(portal.MainIntent(), event.EventMessage, content, nil, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) encrypt(intent *appservice.IntentAPI, content *event.Content, eventType event.Type) (event.Type, error) {
|
||||||
|
if !portal.Encrypted || portal.bridge.Crypto == nil {
|
||||||
|
return eventType, nil
|
||||||
|
}
|
||||||
|
intent.AddDoublePuppetValue(content)
|
||||||
|
// TODO maybe the locking should be inside mautrix-go?
|
||||||
|
portal.encryptLock.Lock()
|
||||||
|
defer portal.encryptLock.Unlock()
|
||||||
|
err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, content)
|
||||||
|
if err != nil {
|
||||||
|
return eventType, fmt.Errorf("failed to encrypt event: %w", err)
|
||||||
|
}
|
||||||
|
return event.EventEncrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent, extraContent map[string]interface{}, timestamp int64) (*mautrix.RespSendEvent, error) {
|
||||||
|
wrappedContent := event.Content{Parsed: content, Raw: extraContent}
|
||||||
|
var err error
|
||||||
|
eventType, err = portal.encrypt(intent, &wrappedContent, eventType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = intent.UserTyping(portal.MXID, false, 0)
|
||||||
|
if timestamp == 0 {
|
||||||
|
return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent)
|
||||||
|
} else {
|
||||||
|
return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) encryptFileInPlace(data []byte, mimeType string) (string, *event.EncryptedFileInfo) {
|
||||||
|
if !portal.Encrypted {
|
||||||
|
return mimeType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file := &event.EncryptedFileInfo{
|
||||||
|
EncryptedFile: *attachment.NewEncryptedFile(),
|
||||||
|
URL: "",
|
||||||
|
}
|
||||||
|
file.EncryptInPlace(data)
|
||||||
|
return "application/octet-stream", file
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) uploadMedia(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
|
||||||
|
uploadMimeType, file := portal.encryptFileInPlace(data, content.Info.MimeType)
|
||||||
|
|
||||||
|
req := mautrix.ReqUploadMedia{
|
||||||
|
ContentBytes: data,
|
||||||
|
ContentType: uploadMimeType,
|
||||||
|
}
|
||||||
|
var mxc id.ContentURI
|
||||||
|
if portal.bridge.Config.Homeserver.AsyncMedia {
|
||||||
|
uploaded, err := intent.UploadAsync(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mxc = uploaded.ContentURI
|
||||||
|
} else {
|
||||||
|
uploaded, err := intent.UploadMedia(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mxc = uploaded.ContentURI
|
||||||
|
}
|
||||||
|
|
||||||
|
if file != nil {
|
||||||
|
file.URL = mxc.CUString()
|
||||||
|
content.File = file
|
||||||
|
} else {
|
||||||
|
content.URL = mxc.CUString()
|
||||||
|
}
|
||||||
|
|
||||||
|
content.Info.Size = len(data)
|
||||||
|
if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
|
||||||
|
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
||||||
|
content.Info.Width, content.Info.Height = cfg.Width, cfg.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a hack for bad clients like Element iOS that require a thumbnail (https://github.com/vector-im/element-ios/issues/4004)
|
||||||
|
if strings.HasPrefix(content.Info.MimeType, "image/") && content.Info.ThumbnailInfo == nil {
|
||||||
|
infoCopy := *content.Info
|
||||||
|
content.Info.ThumbnailInfo = &infoCopy
|
||||||
|
if content.File != nil {
|
||||||
|
content.Info.ThumbnailFile = file
|
||||||
|
} else {
|
||||||
|
content.Info.ThumbnailURL = content.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timings messageTimings) {
|
||||||
|
ms := metricSender{portal: portal, timings: &timings}
|
||||||
|
|
||||||
|
log := portal.zlog.With().Str("event_id", evt.ID.String()).Logger()
|
||||||
|
log.Debug().Dur("age", timings.totalReceive).Msg("Handling Matrix message")
|
||||||
|
|
||||||
|
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
txnID := util.GenerateTmpId()
|
||||||
|
portal.outgoingMessagesLock.Lock()
|
||||||
|
portal.outgoingMessages[txnID] = evt.ID
|
||||||
|
portal.outgoingMessagesLock.Unlock()
|
||||||
|
switch content.MsgType {
|
||||||
|
case event.MsgText, event.MsgEmote, event.MsgNotice:
|
||||||
|
text := content.Body
|
||||||
|
if content.MsgType == event.MsgEmote {
|
||||||
|
text = "/me " + text
|
||||||
|
}
|
||||||
|
_, err := sender.Client.Conversations.SendMessage(
|
||||||
|
sender.Client.NewMessageBuilder().
|
||||||
|
SetConversationID(portal.ID).
|
||||||
|
SetSelfParticipantID(portal.SelfUserID).
|
||||||
|
SetContent(text).
|
||||||
|
SetTmpID(txnID), "",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
go ms.sendMessageMetrics(evt, err, "Error sending", true)
|
||||||
|
} else {
|
||||||
|
go ms.sendMessageMetrics(evt, nil, "", true)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
go ms.sendMessageMetrics(evt, fmt.Errorf("unsupported msgtype"), "Ignoring", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Delete() {
|
||||||
|
err := portal.Portal.Delete(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to delete portal from database")
|
||||||
|
}
|
||||||
|
portal.bridge.portalsLock.Lock()
|
||||||
|
delete(portal.bridge.portalsByKey, portal.Key)
|
||||||
|
if len(portal.MXID) > 0 {
|
||||||
|
delete(portal.bridge.portalsByMXID, portal.MXID)
|
||||||
|
}
|
||||||
|
portal.bridge.portalsLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) GetMatrixUsers() ([]id.UserID, error) {
|
||||||
|
members, err := portal.MainIntent().JoinedMembers(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get member list: %w", err)
|
||||||
|
}
|
||||||
|
var users []id.UserID
|
||||||
|
for userID := range members.Joined {
|
||||||
|
_, isPuppet := portal.bridge.ParsePuppetMXID(userID)
|
||||||
|
if !isPuppet && userID != portal.bridge.Bot.UserID {
|
||||||
|
users = append(users, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) CleanupIfEmpty() {
|
||||||
|
users, err := portal.GetMatrixUsers()
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
portal.log.Infoln("Room seems to be empty, cleaning up...")
|
||||||
|
portal.Delete()
|
||||||
|
portal.Cleanup(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (portal *Portal) Cleanup(puppetsOnly bool) {
|
||||||
|
if len(portal.MXID) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intent := portal.MainIntent()
|
||||||
|
if portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
|
||||||
|
err := intent.BeeperDeleteRoom(portal.MXID)
|
||||||
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
||||||
|
portal.zlog.Err(err).Msg("Failed to delete room using hungryserv yeet endpoint")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
members, err := intent.JoinedMembers(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Failed to get portal members for cleanup:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for member := range members.Joined {
|
||||||
|
if member == intent.UserID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
puppet := portal.bridge.GetPuppetByMXID(member)
|
||||||
|
if puppet != nil {
|
||||||
|
_, err = puppet.DefaultIntent().LeaveRoom(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Error leaving as puppet while cleaning up portal:", err)
|
||||||
|
}
|
||||||
|
} else if !puppetsOnly {
|
||||||
|
_, err = intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"})
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Error kicking user while cleaning up portal:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = intent.LeaveRoom(portal.MXID)
|
||||||
|
if err != nil {
|
||||||
|
portal.log.Errorln("Error leaving with main intent while cleaning up portal:", err)
|
||||||
|
}
|
||||||
|
}
|
280
puppet.go
Normal file
280
puppet.go
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"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-gmessages/database"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
var userIDRegex *regexp.Regexp
|
||||||
|
|
||||||
|
func (br *GMBridge) ParsePuppetMXID(mxid id.UserID) (key database.Key, ok bool) {
|
||||||
|
if userIDRegex == nil {
|
||||||
|
userIDRegex = br.Config.MakeUserIDRegex(`([0-9]+)\.([0-9]+)`)
|
||||||
|
}
|
||||||
|
match := userIDRegex.FindStringSubmatch(string(mxid))
|
||||||
|
if len(match) == 3 {
|
||||||
|
var err error
|
||||||
|
key.Receiver, err = strconv.Atoi(match[1])
|
||||||
|
ok = err == nil
|
||||||
|
key.ID = match[2]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
|
||||||
|
key, ok := br.ParsePuppetMXID(mxid)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return br.GetPuppetByKey(key, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetPuppetByKey(key database.Key, phone string) *Puppet {
|
||||||
|
br.puppetsLock.Lock()
|
||||||
|
defer br.puppetsLock.Unlock()
|
||||||
|
puppet, ok := br.puppetsByKey[key]
|
||||||
|
if !ok {
|
||||||
|
dbPuppet, err := br.DB.Puppet.Get(context.TODO(), key)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Object("puppet_key", key).Msg("Failed to get puppet from database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if phone == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if dbPuppet == nil {
|
||||||
|
dbPuppet = br.DB.Puppet.New()
|
||||||
|
dbPuppet.Key = key
|
||||||
|
dbPuppet.Phone = phone
|
||||||
|
err = dbPuppet.Insert(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Object("puppet_key", key).Msg("Failed to insert puppet into database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
puppet = br.NewPuppet(dbPuppet)
|
||||||
|
br.puppetsByKey[puppet.Key] = puppet
|
||||||
|
}
|
||||||
|
return puppet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) IsGhost(id id.UserID) bool {
|
||||||
|
_, ok := br.ParsePuppetMXID(id)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetIGhost(id id.UserID) bridge.Ghost {
|
||||||
|
p := br.GetPuppetByMXID(id)
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) GetMXID() id.UserID {
|
||||||
|
return puppet.MXID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllPuppets() []*Puppet {
|
||||||
|
return br.loadManyPuppets(br.DB.Puppet.GetAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) loadManyPuppets(query func(ctx context.Context) ([]*database.Puppet, error)) []*Puppet {
|
||||||
|
br.puppetsLock.Lock()
|
||||||
|
defer br.puppetsLock.Unlock()
|
||||||
|
dbPuppets, err := query(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Msg("Failed to load all puppets from database")
|
||||||
|
return []*Puppet{}
|
||||||
|
}
|
||||||
|
output := make([]*Puppet, len(dbPuppets))
|
||||||
|
for index, dbPuppet := range dbPuppets {
|
||||||
|
if dbPuppet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
puppet, ok := br.puppetsByKey[dbPuppet.Key]
|
||||||
|
if !ok {
|
||||||
|
puppet = br.NewPuppet(dbPuppet)
|
||||||
|
br.puppetsByKey[puppet.Key] = puppet
|
||||||
|
}
|
||||||
|
output[index] = puppet
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) FormatPuppetMXID(key database.Key) id.UserID {
|
||||||
|
return id.NewUserID(
|
||||||
|
br.Config.Bridge.FormatUsername(key.String()),
|
||||||
|
br.Config.Homeserver.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
|
||||||
|
return &Puppet{
|
||||||
|
Puppet: dbPuppet,
|
||||||
|
bridge: br,
|
||||||
|
log: br.ZLog.With().Str("phone", dbPuppet.Phone).Int("puppet_receiver", dbPuppet.Receiver).Logger(),
|
||||||
|
MXID: br.FormatPuppetMXID(dbPuppet.Key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Puppet struct {
|
||||||
|
*database.Puppet
|
||||||
|
bridge *GMBridge
|
||||||
|
log zerolog.Logger
|
||||||
|
MXID id.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ bridge.GhostWithProfile = (*Puppet)(nil)
|
||||||
|
|
||||||
|
func (puppet *Puppet) GetDisplayname() string {
|
||||||
|
return puppet.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) GetAvatarURL() id.ContentURI {
|
||||||
|
return puppet.AvatarMXC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) SwitchCustomMXID(_ string, _ id.UserID) error {
|
||||||
|
return fmt.Errorf("puppets don't support custom MXIDs here")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) IntentFor(_ *Portal) *appservice.IntentAPI {
|
||||||
|
return puppet.DefaultIntent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) CustomIntent() *appservice.IntentAPI {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
|
||||||
|
return puppet.bridge.AS.Intent(puppet.MXID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) UpdateAvatar(source *User, avatarID string) bool {
|
||||||
|
if puppet.AvatarID == avatarID && puppet.AvatarSet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
puppet.AvatarID = avatarID
|
||||||
|
puppet.AvatarMXC = id.ContentURI{}
|
||||||
|
puppet.AvatarSet = false
|
||||||
|
// TODO bridge avatar
|
||||||
|
if puppet.AvatarMXC.IsEmpty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err := puppet.DefaultIntent().SetAvatarURL(puppet.AvatarMXC)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warn().Err(err).Msg("Failed to set avatar")
|
||||||
|
} else {
|
||||||
|
puppet.AvatarSet = true
|
||||||
|
}
|
||||||
|
go puppet.updatePortalAvatar()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) UpdateName(formattedPhone, fullName, firstName string) bool {
|
||||||
|
newName := puppet.bridge.Config.Bridge.FormatDisplayname(formattedPhone, fullName, firstName)
|
||||||
|
if puppet.Name != newName || !puppet.NameSet {
|
||||||
|
oldName := puppet.Name
|
||||||
|
puppet.Name = newName
|
||||||
|
puppet.NameSet = false
|
||||||
|
err := puppet.DefaultIntent().SetDisplayName(newName)
|
||||||
|
if err == nil {
|
||||||
|
puppet.log.Debug().Str("old_name", oldName).Str("new_name", newName).Msg("Updated displayname")
|
||||||
|
puppet.NameSet = true
|
||||||
|
go puppet.updatePortalName()
|
||||||
|
} else {
|
||||||
|
puppet.log.Warn().Err(err).Msg("Failed to set displayname")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) UpdateContactInfo() bool {
|
||||||
|
if !puppet.bridge.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if puppet.ContactInfoSet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
contactInfo := map[string]any{
|
||||||
|
"com.beeper.bridge.identifiers": []string{
|
||||||
|
fmt.Sprintf("tel:+%s", puppet.Phone),
|
||||||
|
},
|
||||||
|
"com.beeper.bridge.remote_id": puppet.Key.String(),
|
||||||
|
"com.beeper.bridge.service": "gmessages",
|
||||||
|
"com.beeper.bridge.network": "gmessages",
|
||||||
|
}
|
||||||
|
err := puppet.DefaultIntent().BeeperUpdateProfile(contactInfo)
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Warn().Err(err).Msg("Failed to store custom contact info in profile")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
puppet.ContactInfoSet = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updatePortalAvatar() {
|
||||||
|
// TODO implement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) updatePortalName() {
|
||||||
|
// TODO implement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (puppet *Puppet) Sync(source *User, contact *binary.Participant) {
|
||||||
|
err := puppet.DefaultIntent().EnsureRegistered()
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Err(err).Msg("Failed to ensure registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
puppet.log.Debug().Msg("Syncing info")
|
||||||
|
|
||||||
|
update := false
|
||||||
|
if contact != nil {
|
||||||
|
if contact.Id.Number != "" && puppet.Phone != contact.Id.Number {
|
||||||
|
puppet.Phone = contact.Id.Number
|
||||||
|
update = true
|
||||||
|
}
|
||||||
|
update = puppet.UpdateName(contact.GetFormattedNumber(), contact.GetFullName(), contact.GetFirstName()) || update
|
||||||
|
update = puppet.UpdateAvatar(source, contact.GetAvatarID()) || update
|
||||||
|
}
|
||||||
|
update = puppet.UpdateContactInfo() || update
|
||||||
|
if update {
|
||||||
|
err = puppet.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
puppet.log.Err(err).Msg("Failed to save puppet to database after sync")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
segment.go
Normal file
97
segment.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
|
||||||
|
// Copyright (C) 2023 Tulir Asokan, Sumner Evans
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SegmentURL = "https://api.segment.io/v1/track"
|
||||||
|
|
||||||
|
type SegmentClient struct {
|
||||||
|
key string
|
||||||
|
userID string
|
||||||
|
log zerolog.Logger
|
||||||
|
client http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var Segment SegmentClient
|
||||||
|
|
||||||
|
func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var segmentUserID string
|
||||||
|
if Segment.userID != "" {
|
||||||
|
segmentUserID = Segment.userID
|
||||||
|
} else {
|
||||||
|
segmentUserID = userID.String()
|
||||||
|
}
|
||||||
|
err := json.NewEncoder(&buf).Encode(map[string]interface{}{
|
||||||
|
"userId": segmentUserID,
|
||||||
|
"event": event,
|
||||||
|
"properties": properties,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", SegmentURL, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(sc.key, "")
|
||||||
|
resp, err := sc.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SegmentClient) IsEnabled() bool {
|
||||||
|
return len(sc.key) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SegmentClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
|
||||||
|
if !sc.IsEnabled() {
|
||||||
|
return
|
||||||
|
} else if len(properties) > 1 {
|
||||||
|
panic("Track should be called with at most one property map")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
props := map[string]interface{}{}
|
||||||
|
if len(properties) > 0 {
|
||||||
|
props = properties[0]
|
||||||
|
}
|
||||||
|
props["bridge"] = "gmessages"
|
||||||
|
err := sc.trackSync(userID, event, props)
|
||||||
|
if err != nil {
|
||||||
|
sc.log.Err(err).Str("event", event).Msg("Error tracking event")
|
||||||
|
} else {
|
||||||
|
sc.log.Debug().Str("event", event).Msg("Tracked event")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
703
user.go
Normal file
703
user.go
Normal file
|
@ -0,0 +1,703 @@
|
||||||
|
// mautrix-gmessages - A Matrix-Google Messages 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"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"maunium.net/go/maulogger/v2"
|
||||||
|
"maunium.net/go/maulogger/v2/maulogadapt"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/appservice"
|
||||||
|
"maunium.net/go/mautrix/bridge"
|
||||||
|
"maunium.net/go/mautrix/bridge/bridgeconfig"
|
||||||
|
"maunium.net/go/mautrix/bridge/status"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/format"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
"maunium.net/go/mautrix/pushrules"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-gmessages/database"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/binary"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/crypto"
|
||||||
|
"go.mau.fi/mautrix-gmessages/libgm/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
*database.User
|
||||||
|
Client *libgm.Client
|
||||||
|
|
||||||
|
bridge *GMBridge
|
||||||
|
zlog zerolog.Logger
|
||||||
|
// Deprecated
|
||||||
|
log maulogger.Logger
|
||||||
|
|
||||||
|
Admin bool
|
||||||
|
Whitelisted bool
|
||||||
|
PermissionLevel bridgeconfig.PermissionLevel
|
||||||
|
|
||||||
|
mgmtCreateLock sync.Mutex
|
||||||
|
spaceCreateLock sync.Mutex
|
||||||
|
connLock sync.Mutex
|
||||||
|
|
||||||
|
BridgeState *bridge.BridgeStateQueue
|
||||||
|
|
||||||
|
spaceMembershipChecked bool
|
||||||
|
|
||||||
|
DoublePuppetIntent *appservice.IntentAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User {
|
||||||
|
_, isPuppet := br.ParsePuppetMXID(userID)
|
||||||
|
if isPuppet || userID == br.Bot.UserID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
user, ok := br.usersByMXID[userID]
|
||||||
|
if !ok {
|
||||||
|
userIDPtr := &userID
|
||||||
|
if onlyIfExists {
|
||||||
|
userIDPtr = nil
|
||||||
|
}
|
||||||
|
dbUser, err := br.DB.User.GetByMXID(context.TODO(), userID)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).
|
||||||
|
Str("user_id", userID.String()).
|
||||||
|
Msg("Failed to load user from database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return br.loadDBUser(dbUser, userIDPtr)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetUserByMXID(userID id.UserID) *User {
|
||||||
|
return br.getUserByMXID(userID, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetIUser(userID id.UserID, create bool) bridge.User {
|
||||||
|
u := br.getUserByMXID(userID, !create)
|
||||||
|
if u == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetPuppetByID(id, phone string) *Puppet {
|
||||||
|
return user.bridge.GetPuppetByKey(database.Key{Receiver: user.RowID, ID: id}, phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetPortalByID(id string) *Portal {
|
||||||
|
return user.bridge.GetPortalByKey(database.Key{Receiver: user.RowID, ID: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetIDoublePuppet() bridge.DoublePuppet {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetIGhost() bridge.Ghost {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetPermissionLevel() bridgeconfig.PermissionLevel {
|
||||||
|
return user.PermissionLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetManagementRoomID() id.RoomID {
|
||||||
|
return user.ManagementRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetMXID() id.UserID {
|
||||||
|
return user.MXID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetCommandState() map[string]interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetUserByMXIDIfExists(userID id.UserID) *User {
|
||||||
|
return br.getUserByMXID(userID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetUserByPhone(phone string) *User {
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
user, ok := br.usersByPhone[phone]
|
||||||
|
if !ok {
|
||||||
|
dbUser, err := br.DB.User.GetByPhone(context.TODO(), phone)
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).
|
||||||
|
Str("phone", phone).
|
||||||
|
Msg("Failed to load user from database")
|
||||||
|
}
|
||||||
|
return br.loadDBUser(dbUser, nil)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) addToPhoneMap() {
|
||||||
|
user.bridge.usersLock.Lock()
|
||||||
|
user.bridge.usersByPhone[user.Phone] = user
|
||||||
|
user.bridge.usersLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) removeFromPhoneMap(state status.BridgeState) {
|
||||||
|
user.bridge.usersLock.Lock()
|
||||||
|
phoneUser, ok := user.bridge.usersByPhone[user.Phone]
|
||||||
|
if ok && user == phoneUser {
|
||||||
|
delete(user.bridge.usersByPhone, user.Phone)
|
||||||
|
}
|
||||||
|
user.bridge.usersLock.Unlock()
|
||||||
|
user.BridgeState.Send(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllUsersWithSession() []*User {
|
||||||
|
return br.loadManyUsers(br.DB.User.GetAllWithSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) GetAllUsersWithDoublePuppet() []*User {
|
||||||
|
return br.loadManyUsers(br.DB.User.GetAllWithDoublePuppet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) loadManyUsers(query func(ctx context.Context) ([]*database.User, error)) []*User {
|
||||||
|
br.usersLock.Lock()
|
||||||
|
defer br.usersLock.Unlock()
|
||||||
|
dbUsers, err := query(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).Msg("Failed to all load users from database")
|
||||||
|
return []*User{}
|
||||||
|
}
|
||||||
|
output := make([]*User, len(dbUsers))
|
||||||
|
for index, dbUser := range dbUsers {
|
||||||
|
user, ok := br.usersByMXID[dbUser.MXID]
|
||||||
|
if !ok {
|
||||||
|
user = br.loadDBUser(dbUser, nil)
|
||||||
|
}
|
||||||
|
output[index] = user
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User {
|
||||||
|
if dbUser == nil {
|
||||||
|
if mxid == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dbUser = br.DB.User.New()
|
||||||
|
dbUser.MXID = *mxid
|
||||||
|
err := dbUser.Insert(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
br.ZLog.Err(err).
|
||||||
|
Str("user_id", mxid.String()).
|
||||||
|
Msg("Failed to insert user to database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user := br.NewUser(dbUser)
|
||||||
|
br.usersByMXID[user.MXID] = user
|
||||||
|
if user.Session != nil && user.Phone != "" {
|
||||||
|
br.usersByPhone[user.Phone] = user
|
||||||
|
} else {
|
||||||
|
user.Session = nil
|
||||||
|
user.Phone = ""
|
||||||
|
}
|
||||||
|
if len(user.ManagementRoom) > 0 {
|
||||||
|
br.managementRooms[user.ManagementRoom] = user
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br *GMBridge) NewUser(dbUser *database.User) *User {
|
||||||
|
user := &User{
|
||||||
|
User: dbUser,
|
||||||
|
bridge: br,
|
||||||
|
zlog: br.ZLog.With().Str("user_id", dbUser.MXID.String()).Logger(),
|
||||||
|
}
|
||||||
|
user.log = maulogadapt.ZeroAsMau(&user.zlog)
|
||||||
|
|
||||||
|
user.PermissionLevel = user.bridge.Config.Bridge.Permissions.Get(user.MXID)
|
||||||
|
user.Whitelisted = user.PermissionLevel >= bridgeconfig.PermissionLevelUser
|
||||||
|
user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin
|
||||||
|
user.BridgeState = br.NewBridgeStateQueue(user)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) (ok bool) {
|
||||||
|
extraContent := make(map[string]any)
|
||||||
|
if isDirect {
|
||||||
|
extraContent["is_direct"] = true
|
||||||
|
}
|
||||||
|
if user.DoublePuppetIntent != nil {
|
||||||
|
extraContent["fi.mau.will_auto_accept"] = true
|
||||||
|
}
|
||||||
|
_, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: user.MXID}, extraContent)
|
||||||
|
var httpErr mautrix.HTTPError
|
||||||
|
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
|
||||||
|
user.bridge.StateStore.SetMembership(roomID, user.MXID, event.MembershipJoin)
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
user.zlog.Warn().Err(err).Str("room_id", roomID.String()).Msg("Failed to invite user to room")
|
||||||
|
} else {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.DoublePuppetIntent != nil {
|
||||||
|
err = user.DoublePuppetIntent.EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Warn().Err(err).Str("room_id", roomID.String()).Msg("Failed to auto-join room")
|
||||||
|
ok = false
|
||||||
|
} else {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetSpaceRoom() id.RoomID {
|
||||||
|
if !user.bridge.Config.Bridge.PersonalFilteringSpaces {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(user.SpaceRoom) == 0 {
|
||||||
|
user.spaceCreateLock.Lock()
|
||||||
|
defer user.spaceCreateLock.Unlock()
|
||||||
|
if len(user.SpaceRoom) > 0 {
|
||||||
|
return user.SpaceRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := user.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{
|
||||||
|
Visibility: "private",
|
||||||
|
Name: "Google Messages",
|
||||||
|
Topic: "Your Google Messages bridged chats",
|
||||||
|
InitialState: []*event.Event{{
|
||||||
|
Type: event.StateRoomAvatar,
|
||||||
|
Content: event.Content{
|
||||||
|
Parsed: &event.RoomAvatarEventContent{
|
||||||
|
URL: user.bridge.Config.AppService.Bot.ParsedAvatar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
CreationContent: map[string]interface{}{
|
||||||
|
"type": event.RoomTypeSpace,
|
||||||
|
},
|
||||||
|
PowerLevelOverride: &event.PowerLevelsEventContent{
|
||||||
|
Users: map[id.UserID]int{
|
||||||
|
user.bridge.Bot.UserID: 9001,
|
||||||
|
user.MXID: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to auto-create space room")
|
||||||
|
} else {
|
||||||
|
user.SpaceRoom = resp.RoomID
|
||||||
|
err = user.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to update database after creating space room")
|
||||||
|
}
|
||||||
|
user.ensureInvited(user.bridge.Bot, user.SpaceRoom, false)
|
||||||
|
}
|
||||||
|
} else if !user.spaceMembershipChecked && !user.bridge.StateStore.IsInRoom(user.SpaceRoom, user.MXID) {
|
||||||
|
user.ensureInvited(user.bridge.Bot, user.SpaceRoom, false)
|
||||||
|
}
|
||||||
|
user.spaceMembershipChecked = true
|
||||||
|
|
||||||
|
return user.SpaceRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetManagementRoom() id.RoomID {
|
||||||
|
if len(user.ManagementRoom) == 0 {
|
||||||
|
user.mgmtCreateLock.Lock()
|
||||||
|
defer user.mgmtCreateLock.Unlock()
|
||||||
|
if len(user.ManagementRoom) > 0 {
|
||||||
|
return user.ManagementRoom
|
||||||
|
}
|
||||||
|
creationContent := make(map[string]interface{})
|
||||||
|
if !user.bridge.Config.Bridge.FederateRooms {
|
||||||
|
creationContent["m.federate"] = false
|
||||||
|
}
|
||||||
|
resp, err := user.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{
|
||||||
|
Topic: "Google Messages bridge notices",
|
||||||
|
IsDirect: true,
|
||||||
|
CreationContent: creationContent,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to auto-create management room")
|
||||||
|
} else {
|
||||||
|
user.SetManagementRoom(resp.RoomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user.ManagementRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetManagementRoom(roomID id.RoomID) {
|
||||||
|
log := user.zlog.With().
|
||||||
|
Str("management_room_id", roomID.String()).
|
||||||
|
Str("action", "SetManagementRoom").
|
||||||
|
Logger()
|
||||||
|
existingUser, ok := user.bridge.managementRooms[roomID]
|
||||||
|
if ok {
|
||||||
|
existingUser.ManagementRoom = ""
|
||||||
|
err := existingUser.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).
|
||||||
|
Str("prev_user_id", existingUser.MXID.String()).
|
||||||
|
Msg("Failed to clear management room from previous user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ManagementRoom = roomID
|
||||||
|
user.bridge.managementRooms[user.ManagementRoom] = user
|
||||||
|
err := user.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to update database with management room ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrAlreadyLoggedIn = errors.New("already logged in")
|
||||||
|
|
||||||
|
func (user *User) createClient() {
|
||||||
|
var devicePair *libgm.DevicePair
|
||||||
|
var cryptor *crypto.Cryptor
|
||||||
|
if user.Session != nil && user.Session.WebAuthKey != nil {
|
||||||
|
devicePair = &libgm.DevicePair{
|
||||||
|
Mobile: user.Session.PhoneInfo,
|
||||||
|
Browser: user.Session.BrowserInfo,
|
||||||
|
}
|
||||||
|
cryptor = &crypto.Cryptor{
|
||||||
|
AESCTR256Key: user.Session.AESKey,
|
||||||
|
SHA256Key: user.Session.HMACKey,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cryptor = crypto.NewCryptor(nil, nil)
|
||||||
|
user.Session = &database.Session{
|
||||||
|
AESKey: cryptor.AESCTR256Key,
|
||||||
|
HMACKey: cryptor.SHA256Key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.Client = libgm.NewClient(devicePair, cryptor, user.zlog.With().Str("component", "libgm").Logger(), nil)
|
||||||
|
user.Client.SetEventHandler(user.HandleEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Login(ctx context.Context) (<-chan string, error) {
|
||||||
|
user.connLock.Lock()
|
||||||
|
defer user.connLock.Unlock()
|
||||||
|
if user.Session != nil {
|
||||||
|
return nil, ErrAlreadyLoggedIn
|
||||||
|
} else if user.Client != nil {
|
||||||
|
user.unlockedDeleteConnection()
|
||||||
|
}
|
||||||
|
user.createClient()
|
||||||
|
pairer, err := user.Client.NewPairer(nil, 20)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize pairer: %w", err)
|
||||||
|
}
|
||||||
|
resp, err := pairer.RegisterPhoneRelay()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to register phone relay: %w", err)
|
||||||
|
}
|
||||||
|
err = user.Client.Connect(resp.Field5.RpcKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Google Messages: %w", err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Connect() bool {
|
||||||
|
user.connLock.Lock()
|
||||||
|
defer user.connLock.Unlock()
|
||||||
|
if user.Client != nil {
|
||||||
|
return true
|
||||||
|
} else if user.Session == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
user.zlog.Debug().Msg("Connecting to Google Messages")
|
||||||
|
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting, Error: WAConnecting})
|
||||||
|
err := user.Client.Connect(user.Session.WebAuthKey)
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Error connecting to Google Messages")
|
||||||
|
user.BridgeState.Send(status.BridgeState{
|
||||||
|
StateEvent: status.StateUnknownError,
|
||||||
|
Error: WAConnectionFailed,
|
||||||
|
Info: map[string]interface{}{
|
||||||
|
"go_error": err.Error(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) unlockedDeleteConnection() {
|
||||||
|
if user.Client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Client.Disconnect()
|
||||||
|
user.Client.SetEventHandler(nil)
|
||||||
|
user.Client = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) DeleteConnection() {
|
||||||
|
user.connLock.Lock()
|
||||||
|
defer user.connLock.Unlock()
|
||||||
|
user.unlockedDeleteConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) HasSession() bool {
|
||||||
|
return user.Session != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) DeleteSession() {
|
||||||
|
user.Session = nil
|
||||||
|
user.Phone = ""
|
||||||
|
err := user.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to delete session from database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) IsConnected() bool {
|
||||||
|
return user.Client != nil && user.Client.IsConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) IsLoggedIn() bool {
|
||||||
|
return user.IsConnected() && user.Client.IsLoggedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) tryAutomaticDoublePuppeting() {
|
||||||
|
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) || user.DoublePuppetIntent != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.loginWithSharedSecret(); err != nil {
|
||||||
|
user.zlog.Warn().Err(err).Msg("Failed to login with shared secret for double puppeting")
|
||||||
|
} else if err = user.startCustomMXID(false); err != nil {
|
||||||
|
user.zlog.Warn().Err(err).Msg("Failed to start double puppet after logging in with shared secret")
|
||||||
|
} else {
|
||||||
|
user.zlog.Info().Msg("Successfully automatically enabled double puppet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface{}) {
|
||||||
|
if user.bridge.Config.Bridge.DisableBridgeAlerts {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notice := fmt.Sprintf(formatString, args...)
|
||||||
|
content := format.RenderMarkdown(notice, true, false)
|
||||||
|
_, err := user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, content)
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Warn().Err(err).Str("notice", notice).Msg("Failed to send bridge alert")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) HandleEvent(event interface{}) {
|
||||||
|
switch v := event.(type) {
|
||||||
|
case *events.QR:
|
||||||
|
// These should be here
|
||||||
|
user.zlog.Info().Msg(v.URL)
|
||||||
|
case *events.PairSuccessful:
|
||||||
|
user.Phone = v.PairDeviceData.Mobile.RegistrationID
|
||||||
|
user.Session.PhoneInfo = v.PairDeviceData.Mobile
|
||||||
|
user.Session.BrowserInfo = v.PairDeviceData.Browser
|
||||||
|
user.Session.WebAuthKey = v.PairDeviceData.WebAuthKeyData.GetWebAuthKey()
|
||||||
|
user.addToPhoneMap()
|
||||||
|
err := user.Update(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Failed to update session in database")
|
||||||
|
}
|
||||||
|
case *binary.Event_ConversationEvent:
|
||||||
|
portal := user.GetPortalByID(v.ConversationEvent.GetData().GetConversationID())
|
||||||
|
if portal.MXID != "" {
|
||||||
|
portal.UpdateMetadata(user, v.ConversationEvent.GetData())
|
||||||
|
} else {
|
||||||
|
err := portal.CreateMatrixRoom(user, v.ConversationEvent.GetData())
|
||||||
|
if err != nil {
|
||||||
|
user.zlog.Err(err).Msg("Error creating Matrix room from conversation event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *binary.Event_MessageEvent:
|
||||||
|
portal := user.GetPortalByID(v.MessageEvent.GetData().GetConversationID())
|
||||||
|
portal.messages <- PortalMessage{evt: v.MessageEvent.GetData(), source: user}
|
||||||
|
case *events.ClientReady:
|
||||||
|
user.zlog.Trace().Any("data", v).Msg("Client is ready!")
|
||||||
|
case *events.BrowserActive:
|
||||||
|
user.zlog.Trace().Any("data", v).Msg("Browser active")
|
||||||
|
case *events.Battery:
|
||||||
|
user.zlog.Trace().Any("data", v).Msg("Battery")
|
||||||
|
case *events.DataConnection:
|
||||||
|
user.zlog.Trace().Any("data", v).Msg("Data connection")
|
||||||
|
default:
|
||||||
|
user.zlog.Trace().Any("data", v).Msg("Unknown event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) updateChatMute(portal *Portal, mutedUntil time.Time) {
|
||||||
|
intent := user.DoublePuppetIntent
|
||||||
|
if intent == nil || len(portal.MXID) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if mutedUntil.IsZero() && mutedUntil.Before(time.Now()) {
|
||||||
|
user.log.Debugfln("Portal %s is muted until %s, unmuting...", portal.MXID, mutedUntil)
|
||||||
|
err = intent.DeletePushRule("global", pushrules.RoomRule, string(portal.MXID))
|
||||||
|
} else {
|
||||||
|
user.log.Debugfln("Portal %s is muted until %s, muting...", portal.MXID, mutedUntil)
|
||||||
|
err = intent.PutPushRule("global", pushrules.RoomRule, string(portal.MXID), &mautrix.ReqPutPushRule{
|
||||||
|
Actions: []pushrules.PushActionType{pushrules.ActionDontNotify},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
||||||
|
user.log.Warnfln("Failed to update push rule for %s through double puppet: %v", portal.MXID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomTagData struct {
|
||||||
|
Order json.Number `json:"order"`
|
||||||
|
DoublePuppet string `json:"fi.mau.double_puppet_source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomTagEventContent struct {
|
||||||
|
Tags map[string]CustomTagData `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) updateChatTag(portal *Portal, tag string, active bool) {
|
||||||
|
intent := user.DoublePuppetIntent
|
||||||
|
if intent == nil || len(portal.MXID) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var existingTags CustomTagEventContent
|
||||||
|
err := intent.GetTagsWithCustomData(portal.MXID, &existingTags)
|
||||||
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
||||||
|
user.log.Warnfln("Failed to get tags of %s: %v", portal.MXID, err)
|
||||||
|
}
|
||||||
|
currentTag, ok := existingTags.Tags[tag]
|
||||||
|
if active && !ok {
|
||||||
|
user.log.Debugln("Adding tag", tag, "to", portal.MXID)
|
||||||
|
data := CustomTagData{Order: "0.5", DoublePuppet: user.bridge.Name}
|
||||||
|
err = intent.AddTagWithCustomData(portal.MXID, tag, &data)
|
||||||
|
} else if !active && ok && currentTag.DoublePuppet == user.bridge.Name {
|
||||||
|
user.log.Debugln("Removing tag", tag, "from", portal.MXID)
|
||||||
|
err = intent.RemoveTag(portal.MXID, tag)
|
||||||
|
} else {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnfln("Failed to update tag %s for %s through double puppet: %v", tag, portal.MXID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomReadReceipt struct {
|
||||||
|
Timestamp int64 `json:"ts,omitempty"`
|
||||||
|
DoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomReadMarkers struct {
|
||||||
|
mautrix.ReqSetReadMarkers
|
||||||
|
ReadExtra CustomReadReceipt `json:"com.beeper.read.extra"`
|
||||||
|
FullyReadExtra CustomReadReceipt `json:"com.beeper.fully_read.extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) syncChatDoublePuppetDetails(portal *Portal, conv *binary.Conversation, justCreated bool) {
|
||||||
|
if user.DoublePuppetIntent == nil || len(portal.MXID) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate {
|
||||||
|
//user.updateChatMute(portal, chat.MutedUntil)
|
||||||
|
//user.updateChatTag(portal, user.bridge.Config.Bridge.ArchiveTag, conv.Status == 2)
|
||||||
|
//user.updateChatTag(portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) {
|
||||||
|
if !user.bridge.Config.Bridge.SyncDirectChatList || user.DoublePuppetIntent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intent := user.DoublePuppetIntent
|
||||||
|
method := http.MethodPatch
|
||||||
|
//if chats == nil {
|
||||||
|
// chats = user.getDirectChats()
|
||||||
|
// method = http.MethodPut
|
||||||
|
//}
|
||||||
|
user.zlog.Debug().Msg("Updating m.direct list on homeserver")
|
||||||
|
var err error
|
||||||
|
if user.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareAsmux {
|
||||||
|
urlPath := intent.BuildClientURL("unstable", "com.beeper.asmux", "dms")
|
||||||
|
_, err = intent.MakeFullRequest(mautrix.FullRequest{
|
||||||
|
Method: method,
|
||||||
|
URL: urlPath,
|
||||||
|
Headers: http.Header{"X-Asmux-Auth": {user.bridge.AS.Registration.AppToken}},
|
||||||
|
RequestJSON: chats,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
existingChats := make(map[id.UserID][]id.RoomID)
|
||||||
|
err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to get m.direct list to update it:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for userID, rooms := range existingChats {
|
||||||
|
if _, ok := user.bridge.ParsePuppetMXID(userID); !ok {
|
||||||
|
// This is not a ghost user, include it in the new list
|
||||||
|
chats[userID] = rooms
|
||||||
|
} else if _, ok := chats[userID]; !ok && method == http.MethodPatch {
|
||||||
|
// This is a ghost user, but we're not replacing the whole list, so include it too
|
||||||
|
chats[userID] = rooms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
user.log.Warnln("Failed to update m.direct list:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) markUnread(portal *Portal, unread bool) {
|
||||||
|
if user.DoublePuppetIntent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log := user.zlog.With().Str("room_id", portal.MXID.String()).Logger()
|
||||||
|
|
||||||
|
err := user.DoublePuppetIntent.SetRoomAccountData(portal.MXID, "m.marked_unread", map[string]bool{"unread": unread})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("event_type", "m.marked_unread").
|
||||||
|
Msg("Failed to mark room as unread")
|
||||||
|
} else {
|
||||||
|
log.Debug().Str("event_type", "m.marked_unread").Msg("Marked room as unread")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.DoublePuppetIntent.SetRoomAccountData(portal.MXID, "com.famedly.marked_unread", map[string]bool{"unread": unread})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("event_type", "com.famedly.marked_unread").
|
||||||
|
Msg("Failed to mark room as unread")
|
||||||
|
} else {
|
||||||
|
log.Debug().Str("event_type", "com.famedly.marked_unread").Msg("Marked room as unread")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue