Make relay mode more like the Signal bridge

This commit is contained in:
Tulir Asokan 2021-10-28 13:57:15 +03:00
parent d5bf8dd417
commit bb9a0f6528
10 changed files with 129 additions and 143 deletions

View file

@ -93,17 +93,11 @@ func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message stri
Args: args[1:],
}
handler.log.Debugfln("%s sent '%s' in %s", user.MXID, message, roomID)
if roomID == handler.bridge.Config.Bridge.Relaybot.ManagementRoom {
handler.CommandRelaybot(ce)
} else {
handler.CommandMux(ce)
}
handler.CommandMux(ce)
}
func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
switch ce.Command {
case "relaybot":
handler.CommandRelaybot(ce)
case "login":
handler.CommandLogin(ce)
case "logout-matrix":
@ -134,7 +128,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
handler.CommandLogout(ce)
case "toggle":
handler.CommandToggle(ce)
case "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
case "set-relay", "unset-relay", "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
if !ce.User.HasSession() {
ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.")
return
@ -144,6 +138,10 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
}
switch ce.Command {
case "set-relay":
handler.CommandSetRelay(ce)
case "unset-relay":
handler.CommandUnsetRelay(ce)
case "login-matrix":
handler.CommandLoginMatrix(ce)
case "list":
@ -175,22 +173,35 @@ func (handler *CommandHandler) CommandDiscardMegolmSession(ce *CommandEvent) {
}
}
func (handler *CommandHandler) CommandRelaybot(ce *CommandEvent) {
if handler.bridge.Relaybot == nil {
ce.Reply("The relaybot is disabled")
} else if !ce.User.Admin {
ce.Reply("Only admins can manage the relaybot")
const cmdSetRelayHelp = `set-relay - Relay messages in this room through your WhatsApp account.`
func (handler *CommandHandler) CommandSetRelay(ce *CommandEvent) {
if !handler.bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Portal == nil {
ce.Reply("This is not a portal room")
} else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
} else {
if ce.Command == "relaybot" {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `relaybot <command>`")
return
}
ce.Command = strings.ToLower(ce.Args[0])
ce.Args = ce.Args[1:]
}
ce.User = handler.bridge.Relaybot
handler.CommandMux(ce)
ce.Portal.RelayUserID = ce.User.MXID
ce.Portal.Update()
ce.Reply("Messages from non-logged-in users in this room will now be bridged through your WhatsApp account")
}
}
const cmdUnsetRelayHelp = `set-relay - Stop relaying messages in this room.`
func (handler *CommandHandler) CommandUnsetRelay(ce *CommandEvent) {
if !handler.bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Portal == nil {
ce.Reply("This is not a portal room")
} else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
} else {
ce.Portal.RelayUserID = ""
ce.Portal.Update()
ce.Reply("Messages from non-logged-in users will no longer be bridged in this room")
}
}
@ -627,7 +638,7 @@ const cmdHelpHelp = `help - Prints this help`
// CommandHelp handles help command
func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix := ""
if ce.User.ManagementRoom != ce.RoomID || ce.User.IsRelaybot {
if ce.User.ManagementRoom != ce.RoomID {
cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " "
}
@ -640,6 +651,8 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix + cmdReconnectHelp,
cmdPrefix + cmdDisconnectHelp,
cmdPrefix + cmdPingHelp,
cmdPrefix + cmdSetRelayHelp,
cmdPrefix + cmdUnsetRelayHelp,
cmdPrefix + cmdLoginMatrixHelp,
cmdPrefix + cmdLogoutMatrixHelp,
cmdPrefix + cmdToggleHelp,
@ -871,14 +884,9 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
puppet.SyncContact(user, true)
portal := user.GetPortalByJID(puppet.JID)
if len(portal.MXID) > 0 {
if !user.IsRelaybot {
_, err = portal.MainIntent().Client.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
err = nil
}
}
if err != nil {
portal.log.Warnfln("Failed to invite %s to portal: %v. Creating new portal", user.MXID, err)
ok := portal.ensureUserInvited(user)
if !ok {
portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", user.MXID)
portal.MXID = ""
} else {
ce.Reply("You already have a private chat portal with that user at [%s](https://matrix.to/#/%s)", puppet.Displayname, portal.MXID)

View file

@ -85,7 +85,7 @@ type BridgeConfig struct {
Permissions PermissionConfig `yaml:"permissions"`
Relaybot RelaybotConfig `yaml:"relaybot"`
Relay RelaybotConfig `yaml:"relay"`
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
@ -110,6 +110,8 @@ func (bc *BridgeConfig) setDefaults() {
bc.BridgeNotices = true
bc.EnableStatusBroadcast = true
bc.Relay.AdminOnly = true
}
type umBridgeConfig BridgeConfig
@ -271,10 +273,8 @@ func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel
}
type RelaybotConfig struct {
Enabled bool `yaml:"enabled"`
ManagementRoom id.RoomID `yaml:"management"`
InviteUsers []id.UserID `yaml:"invites"`
Enabled bool `yaml:"enabled"`
AdminOnly bool `yaml:"admin_only"`
MessageFormats map[event.MessageType]string `yaml:"message_formats"`
messageTemplates *template.Template `yaml:"-"`
}

View file

@ -121,11 +121,13 @@ type Portal struct {
FirstEventID id.EventID
NextBatchID id.BatchID
RelayUserID id.UserID
}
func (portal *Portal) Scan(row Scannable) *Portal {
var mxid, avatarURL, firstEventID, nextBatchID sql.NullString
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID)
var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID)
if err != nil {
if err != sql.ErrNoRows {
portal.log.Errorln("Database scan failed:", err)
@ -136,6 +138,7 @@ func (portal *Portal) Scan(row Scannable) *Portal {
portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
portal.FirstEventID = id.EventID(firstEventID.String)
portal.NextBatchID = id.BatchID(nextBatchID.String)
portal.RelayUserID = id.UserID(relayUserID.String)
return portal
}
@ -146,17 +149,24 @@ func (portal *Portal) mxidPtr() *id.RoomID {
return nil
}
func (portal *Portal) relayUserPtr() *id.UserID {
if len(portal.RelayUserID) > 0 {
return &portal.RelayUserID
}
return nil
}
func (portal *Portal) Insert() {
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String())
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
if err != nil {
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
}
}
func (portal *Portal) Update() {
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6, first_event_id=$7, next_batch_id=$8 WHERE jid=$9 AND receiver=$10",
portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.Key.JID, portal.Key.Receiver)
_, err := portal.db.Exec("UPDATE portal SET mxid=$3, name=$4, topic=$5, avatar=$6, avatar_url=$7, encrypted=$8, first_event_id=$9, next_batch_id=$10, relay_user_id=$11 WHERE jid=$1 AND receiver=$2",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
if err != nil {
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
}

View file

@ -0,0 +1,12 @@
package upgrades
import (
"database/sql"
)
func init() {
upgrades[28] = upgrade{"Add relay user field to portal table", func(tx *sql.Tx, ctx context) error {
_, err := tx.Exec(`ALTER TABLE portal ADD COLUMN relay_user_id TEXT`)
return err
}}
}

View file

@ -39,7 +39,7 @@ type upgrade struct {
fn upgradeFunc
}
const NumberOfUpgrades = 28
const NumberOfUpgrades = 29
var upgrades [NumberOfUpgrades]upgrade

View file

@ -204,15 +204,12 @@ bridge:
"example.com": user
"@admin:example.com": admin
relaybot:
# Whether or not relaybot support is enabled.
relay:
# Whether relay mode should be allowed. If allowed, `!signal set-relay` can be used to turn any
# authenticated user into a relaybot for that chat.
enabled: false
# The management room for the bot. This is where all status notifications are posted and
# in this room, you can use `!wa <command>` instead of `!wa relaybot <command>`. Omitting
# the command prefix completely like in user management rooms is not possible.
management: "!foo:example.com"
# List of users to invite to all created rooms that include the relaybot.
invites: []
# Should only admins be allowed to set themselves as relay users?
admin_only: true
# The formats to use when sending messages to WhatsApp via the relaybot.
message_formats:
m.text: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"

39
main.go
View file

@ -47,21 +47,29 @@ import (
"maunium.net/go/mautrix-whatsapp/database/upgrades"
)
// The name and repo URL of the bridge.
var (
// These are static
Name = "mautrix-whatsapp"
URL = "https://github.com/mautrix/whatsapp"
// This is changed when making a release
Version = "0.1.8"
// This is filled by init()
WAVersion = ""
VersionString = ""
// These are filled at build time with the -X linker flag
)
// Information to find out exactly which commit the bridge was built from.
// These are filled at build time with the -X linker flag.
var (
Tag = "unknown"
Commit = "unknown"
BuildTime = "unknown"
)
var (
// Version is the version number of the bridge. Changed manually when making a release.
Version = "0.1.8"
// WAVersion is the version number exposed to WhatsApp. Filled in init()
WAVersion = ""
// VersionString is the bridge version, plus commit information. Filled in init() using the build-time values.
VersionString = ""
)
func init() {
if len(Tag) > 0 && Tag[0] == 'v' {
Tag = Tag[1:]
@ -151,7 +159,6 @@ type Bridge struct {
Provisioning *ProvisioningAPI
Bot *appservice.IntentAPI
Formatter *Formatter
Relaybot *User
Crypto Crypto
Metrics *MetricsHandler
WAContainer *sqlstore.Container
@ -320,7 +327,6 @@ func (bridge *Bridge) Start() {
bridge.Log.Debugln("Initializing provisioning API")
bridge.Provisioning.Init()
}
bridge.LoadRelaybot()
bridge.Log.Debugln("Starting application service HTTP server")
go bridge.AS.Start()
bridge.Log.Debugln("Starting event processor")
@ -353,21 +359,6 @@ func (bridge *Bridge) ResendBridgeInfo() {
bridge.Log.Infoln("Finished re-sending bridge info state events")
}
func (bridge *Bridge) LoadRelaybot() {
if !bridge.Config.Bridge.Relaybot.Enabled {
return
}
bridge.Relaybot = bridge.GetUserByMXID("relaybot")
if bridge.Relaybot.HasSession() {
bridge.Log.Debugln("Relaybot is enabled")
} else {
bridge.Log.Debugln("Relaybot is enabled, but not logged in")
}
bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
bridge.Relaybot.IsRelaybot = true
bridge.Relaybot.Connect()
}
func (bridge *Bridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot

View file

@ -114,12 +114,6 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
return
}
if evt.RoomID == mx.bridge.Config.Bridge.Relaybot.ManagementRoom {
_, _ = intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.")
mx.log.Debugln("Joined relaybot management room", evt.RoomID, "after invite from", evt.Sender)
return
}
hasPuppets := false
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == evt.Sender {

View file

@ -190,7 +190,7 @@ type Portal struct {
messages chan PortalMessage
hasRelaybot *bool
relayUser *User
}
func (portal *Portal) handleMessageLoop() {
@ -519,14 +519,17 @@ func (portal *Portal) SyncParticipants(source *User, metadata *types.GroupInfo)
participantMap := make(map[types.JID]bool)
for _, participant := range metadata.Participants {
participantMap[participant.JID] = true
user := portal.bridge.GetUserByJID(participant.JID)
portal.userMXIDAction(user, portal.ensureMXIDInvited)
puppet := portal.bridge.GetPuppetByJID(participant.JID)
puppet.SyncContact(source, true)
err = puppet.IntentFor(portal).EnsureJoined(portal.MXID)
if err != nil {
portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err)
user := portal.bridge.GetUserByJID(participant.JID)
if user != nil {
portal.ensureUserInvited(user)
}
if user == nil || !puppet.IntentFor(portal).IsCustomPuppet {
err = puppet.IntentFor(portal).EnsureJoined(portal.MXID)
if err != nil {
portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err)
}
}
expectedLevel := 0
@ -692,20 +695,6 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
return update
}
func (portal *Portal) userMXIDAction(user *User, fn func(mxid id.UserID)) {
if user == nil {
return
}
if user == portal.bridge.Relaybot {
for _, mxid := range portal.bridge.Config.Bridge.Relaybot.InviteUsers {
fn(mxid)
}
} else {
fn(user.MXID)
}
}
func (portal *Portal) ensureMXIDInvited(mxid id.UserID) {
err := portal.MainIntent().EnsureInvited(portal.MXID, mxid)
if err != nil {
@ -713,12 +702,7 @@ func (portal *Portal) ensureMXIDInvited(mxid id.UserID) {
}
}
func (portal *Portal) ensureUserInvited(user *User) {
if user.IsRelaybot {
portal.userMXIDAction(user, portal.ensureMXIDInvited)
return
}
func (portal *Portal) ensureUserInvited(user *User) (ok bool) {
inviteContent := event.Content{
Parsed: &event.MemberEventContent{
Membership: event.MembershipInvite,
@ -734,26 +718,28 @@ func (portal *Portal) ensureUserInvited(user *User) {
var httpErr mautrix.HTTPError
if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
portal.bridge.StateStore.SetMembership(portal.MXID, user.MXID, event.MembershipJoin)
ok = true
} else if err != nil {
portal.log.Warnfln("Failed to invite %s: %v", user.MXID, err)
} else {
ok = true
}
if customPuppet != nil && customPuppet.CustomIntent() != nil {
err = customPuppet.CustomIntent().EnsureJoined(portal.MXID)
if err != nil {
portal.log.Warnfln("Failed to auto-join portal as %s: %v", user.MXID, err)
ok = false
} else {
ok = true
}
}
return
}
func (portal *Portal) Sync(user *User) bool {
portal.log.Infoln("Syncing portal for", user.MXID)
if user.IsRelaybot {
yes := true
portal.hasRelaybot = &yes
}
if len(portal.MXID) == 0 {
err := portal.CreateMatrixRoom(user)
if err != nil {
@ -1299,9 +1285,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
}
var invite []id.UserID
if user.IsRelaybot {
invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers
}
if portal.bridge.Config.Bridge.Encryption.Default {
initialState = append(initialState, &event.Event{
@ -1354,7 +1337,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
//if broadcastMetadata != nil {
// portal.SyncBroadcastRecipients(user, broadcastMetadata)
//}
if portal.IsPrivateChat() && !user.IsRelaybot {
if portal.IsPrivateChat() {
puppet := user.bridge.GetPuppetByJID(portal.Key.JID)
if portal.bridge.Config.Bridge.Encryption.Default {
@ -1397,15 +1380,16 @@ func (portal *Portal) IsStatusBroadcastList() bool {
}
func (portal *Portal) HasRelaybot() bool {
if portal.bridge.Relaybot == nil {
return false
} else if portal.hasRelaybot == nil {
// FIXME
//val := portal.bridge.Relaybot.IsInPortal(portal.Key)
val := true
portal.hasRelaybot = &val
return portal.bridge.Config.Bridge.Relay.Enabled && len(portal.RelayUserID) > 0
}
func (portal *Portal) GetRelayUser() *User {
if !portal.HasRelaybot() {
return nil
} else if portal.relayUser == nil {
portal.relayUser = portal.bridge.GetUserByMXID(portal.RelayUserID)
}
return *portal.hasRelaybot
return portal.relayUser
}
func (portal *Portal) MainIntent() *appservice.IntentAPI {
@ -1426,8 +1410,8 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, replyToID typ
portal.log.Warnln("Failed to get reply target:", err)
return
}
_ = evt.Content.ParseRaw(evt.Type)
if evt.Type == event.EventEncrypted {
_ = evt.Content.ParseRaw(evt.Type)
decryptedEvt, err := portal.bridge.Crypto.Decrypt(evt)
if err != nil {
portal.log.Warnln("Failed to decrypt reply target:", err)
@ -1435,7 +1419,6 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, replyToID typ
evt = decryptedEvt
}
}
_ = evt.Content.ParseRaw(evt.Type)
content.SetReply(evt)
}
return
@ -2176,7 +2159,7 @@ func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEven
content.FormattedBody = strings.Replace(html.EscapeString(content.Body), "\n", "<br/>", -1)
content.Format = event.FormatHTML
}
data, err := portal.bridge.Config.Bridge.Relaybot.FormatMessage(content, sender.MXID, member)
data, err := portal.bridge.Config.Bridge.Relay.FormatMessage(content, sender.MXID, member)
if err != nil {
portal.log.Errorln("Failed to apply relaybot format:", err)
}
@ -2243,18 +2226,13 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
}
}
relaybotFormatted := false
if sender.NeedsRelaybot(portal) {
if !sender.HasSession() {
if !portal.HasRelaybot() {
if sender.HasSession() {
portal.log.Debugln("Database says", sender.MXID, "not in chat and no relaybot, but trying to send anyway")
} else {
portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot")
return nil, sender
}
} else {
relaybotFormatted = portal.addRelaybotFormat(sender, content)
sender = portal.bridge.Relaybot
portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot")
return nil, sender
}
relaybotFormatted = portal.addRelaybotFormat(sender, content)
sender = portal.GetRelayUser()
}
if evt.Type == event.EventSticker {
content.MsgType = event.MsgImage

View file

@ -58,8 +58,6 @@ type User struct {
Whitelisted bool
RelaybotWhitelisted bool
IsRelaybot bool
mgmtCreateLock sync.Mutex
connLock sync.Mutex
@ -179,8 +177,6 @@ func (bridge *Bridge) NewUser(dbUser *database.User) *User {
log: bridge.Log.Sub("User").Sub(string(dbUser.MXID)),
historySyncs: make(chan *events.HistorySync, 32),
IsRelaybot: false,
}
user.RelaybotWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelaybotWhitelisted(user.MXID)
user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID)