Merge branch 'multidevice'

Fixes #330
This commit is contained in:
Tulir Asokan 2021-10-30 18:53:34 +03:00
commit b80e0c8db5
33 changed files with 2662 additions and 3551 deletions

View file

@ -55,7 +55,7 @@ build docker amd64:
DOCKER_ARCH: amd64
after_script:
- |
if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
if [[ "$CI_COMMIT_BRANCH" == "legacy" && "$CI_JOB_STATUS" == "success" ]]; then
apk add --update curl jq
rm -rf /var/cache/apk/*

View file

@ -1,11 +1,10 @@
# mautrix-whatsapp
A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)
implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project.
A Matrix-WhatsApp puppeting bridge based on [whatsmeow](https://github.com/tulir/whatsmeow).
### Documentation
All setup and usage instructions are located on
[docs.mau.fi](https://docs.mau.fi/bridges/go/whatsapp/index.html).
Some quick links:
All setup and usage instructions are located on [docs.mau.fi]. Some quick links:
[docs.mau.fi]: https://docs.mau.fi/bridges/go/whatsapp/index.html
* [Bridge setup](https://docs.mau.fi/bridges/go/whatsapp/setup/index.html)
(or [with Docker](https://docs.mau.fi/bridges/go/whatsapp/setup/docker.html))

View file

@ -12,14 +12,14 @@
* [x] Read receipts
* [ ] Power level
* [ ] Membership actions
* [x] Invite
* [ ] Invite
* [ ] Join
* [x] Leave
* [x] Kick
* [ ] Leave
* [ ] Kick
* [ ] Room metadata changes
* [x] Name
* [ ] Avatar<sup>[1]</sup>
* [x] Topic
* [ ] Name
* [ ] Avatar
* [ ] Topic
* [ ] Initial room metadata
* WhatsApp → Matrix
* [x] Message content
@ -32,10 +32,10 @@
* [ ] Chat types
* [x] Private chat
* [x] Group chat
* [ ] Broadcast list<sup>[2]</sup>
* [ ] Broadcast list
* [x] Message deletions
* [x] Avatars
* [x] Presence
* [ ] Presence
* [x] Typing notifications
* [x] Read receipts
* [x] Admin/superadmin status
@ -49,8 +49,8 @@
* [x] Avatar
* [x] Description
* [x] Initial group metadata
* [ ] User metadata changes
* [ ] Display name<sup>[3]</sup>
* [x] User metadata changes
* [x] Display name
* [x] Avatar
* [x] Initial user metadata
* [x] Display name
@ -63,7 +63,3 @@
* [x] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
* [x] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients
* [x] Shared group chat portals
<sup>[1]</sup> May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp
<sup>[2]</sup> May already work
<sup>[3]</sup> May not be possible

View file

@ -20,16 +20,11 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
@ -38,7 +33,6 @@ import (
type BridgeStateEvent string
const (
StateStarting BridgeStateEvent = "STARTING"
StateUnconfigured BridgeStateEvent = "UNCONFIGURED"
StateRunning BridgeStateEvent = "RUNNING"
StateConnecting BridgeStateEvent = "CONNECTING"
@ -56,20 +50,14 @@ const (
WANotLoggedIn BridgeErrorCode = "wa-logged-out"
WANotConnected BridgeErrorCode = "wa-not-connected"
WAConnecting BridgeErrorCode = "wa-connecting"
WATimeout BridgeErrorCode = "wa-timeout"
WAServerTimeout BridgeErrorCode = "wa-server-timeout"
WAPingFalse BridgeErrorCode = "wa-ping-false"
WAPingError BridgeErrorCode = "wa-ping-error"
)
var bridgeHumanErrors = map[BridgeErrorCode]string{
WANotLoggedIn: "You're not logged into WhatsApp",
WANotConnected: "You're not connected to WhatsApp",
WAConnecting: "Trying to reconnect to WhatsApp. Please make sure WhatsApp is running on your phone and connected to the internet.",
WATimeout: "WhatsApp on your phone is not responding. Please make sure it is running and connected to the internet.",
WAServerTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
WAPingFalse: "WhatsApp returned an error, reconnecting. Please make sure WhatsApp is running on your phone and connected to the internet.",
WAPingError: "WhatsApp returned an unknown error",
}
type BridgeState struct {
@ -94,8 +82,8 @@ type GlobalBridgeState struct {
func (pong BridgeState) fill(user *User) BridgeState {
if user != nil {
pong.UserID = user.MXID
pong.RemoteID = strings.TrimSuffix(user.JID, whatsapp.NewUserSuffix)
pong.RemoteName = fmt.Sprintf("+%s", pong.RemoteID)
pong.RemoteID = fmt.Sprintf("%s_a%d_d%d", user.JID.User, user.JID.Agent, user.JID.Device)
pong.RemoteName = fmt.Sprintf("+%s", user.JID.User)
}
pong.Timestamp = time.Now().Unix()
@ -116,32 +104,6 @@ func (pong *BridgeState) shouldDeduplicate(newPong *BridgeState) bool {
return pong.Timestamp+int64(pong.TTL/5) > time.Now().Unix()
}
func (user *User) setupAdminTestHooks() {
if len(user.bridge.Config.Homeserver.StatusEndpoint) == 0 {
return
}
user.Conn.AdminTestHook = func(err error) {
if errors.Is(err, whatsapp.ErrConnectionTimeout) {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
} else if errors.Is(err, whatsapp.ErrPingFalse) {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingFalse})
} else if err == nil {
user.sendBridgeState(BridgeState{StateEvent: StateConnected})
} else {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingError})
}
}
user.Conn.CountTimeoutHook = func(wsKeepaliveErrorCount int) {
if wsKeepaliveErrorCount > 0 {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
} else {
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
}
}
}
func (bridge *Bridge) createBridgeStateRequest(ctx context.Context, state *BridgeState) (req *http.Request, err error) {
var body bytes.Buffer
if err = json.NewEncoder(&body).Encode(&state); err != nil {
@ -210,8 +172,6 @@ func (user *User) sendBridgeState(state BridgeState) {
}
}
var bridgeStatePingID uint32 = 0
func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
if !prov.bridge.AS.CheckServerToken(w, r) {
return
@ -221,37 +181,12 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
var global BridgeState
global.StateEvent = StateRunning
var remote BridgeState
if user.Conn != nil {
if user.Conn.IsConnected() && user.Conn.IsLoggedIn() {
pingID := atomic.AddUint32(&bridgeStatePingID, 1)
user.log.Debugfln("Pinging WhatsApp mobile due to bridge status /ping API request (ID %d)", pingID)
err := user.Conn.AdminTestWithSuppress(true)
if errors.Is(r.Context().Err(), context.Canceled) {
user.log.Warnfln("Ping request %d was canceled before we responded (response was %v)", pingID, err)
user.prevBridgeStatus = nil
return
}
user.log.Debugfln("Ping %d response: %v", pingID, err)
remote.StateEvent = StateTransientDisconnect
if err == whatsapp.ErrPingFalse {
user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
go user.HandleError(err)
remote.Error = WAPingFalse
} else if errors.Is(err, whatsapp.ErrConnectionTimeout) {
remote.Error = WATimeout
} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
remote.Error = WAServerTimeout
} else if err != nil {
remote.Error = WAPingError
} else {
remote.StateEvent = StateConnected
}
} else if user.Conn.IsLoginInProgress() && user.Session != nil {
if user.IsConnected() {
if user.Client.IsLoggedIn {
remote.StateEvent = StateConnected
} else if user.Session != nil {
remote.StateEvent = StateConnecting
remote.Error = WAConnecting
} else if !user.Conn.IsConnected() && user.Session != nil {
remote.StateEvent = StateBadCredentials
remote.Error = WANotConnected
} // else: unconfigured
} else if user.Session != nil {
remote.StateEvent = StateBadCredentials

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -20,22 +20,21 @@ import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"github.com/Rhymen/go-whatsapp"
"github.com/skip2/go-qrcode"
"maunium.net/go/maulogger/v2"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
)
type CommandHandler struct {
@ -94,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":
@ -119,8 +112,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
handler.CommandDisconnect(ce)
case "ping":
handler.CommandPing(ce)
case "delete-connection":
handler.CommandDeleteConnection(ce)
case "delete-session":
handler.CommandDeleteSession(ce)
case "delete-portal":
@ -137,20 +128,22 @@ 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
} else if !ce.User.IsConnected() {
} else if !ce.User.IsLoggedIn() {
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect.")
return
}
switch ce.Command {
case "set-relay":
handler.CommandSetRelay(ce)
case "unset-relay":
handler.CommandUnsetRelay(ce)
case "login-matrix":
handler.CommandLoginMatrix(ce)
case "sync":
handler.CommandSync(ce)
case "list":
handler.CommandList(ce)
case "open":
@ -180,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")
}
}
@ -226,12 +232,14 @@ func (handler *CommandHandler) CommandInviteLink(ce *CommandEvent) {
return
}
link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
if err != nil {
ce.Reply("Failed to get invite link: %v", err)
return
}
ce.Reply("%s%s", inviteLinkPrefix, link)
ce.Reply("Not yet implemented")
// TODO reimplement
//link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
//if err != nil {
// ce.Reply("Failed to get invite link: %v", err)
// return
//}
//ce.Reply("%s%s", inviteLinkPrefix, link)
}
const cmdJoinHelp = `join <invite link> - Join a group chat with an invite link.`
@ -246,26 +254,28 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) {
return
}
jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
if err != nil {
ce.Reply("Failed to join group: %v", err)
return
}
handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
if len(portal.MXID) > 0 {
portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
} else {
err = portal.CreateMatrixRoom(ce.User)
if err != nil {
ce.Reply("Failed to create portal room: %v", err)
return
}
ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
}
ce.Reply("Not yet implemented")
// TODO reimplement
//jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
//if err != nil {
// ce.Reply("Failed to join group: %v", err)
// return
//}
//
//handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
//if len(portal.MXID) > 0 {
// portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
// ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
//} else {
// err = portal.CreateMatrixRoom(ce.User)
// if err != nil {
// ce.Reply("Failed to create portal room: %v", err)
// return
// }
//
// ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
//}
}
const cmdCreateHelp = `create - Create a group chat.`
@ -299,43 +309,45 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) {
return
}
participants := []string{ce.User.JID}
participants := []types.JID{ce.User.JID.ToNonAD()}
for userID := range members.Joined {
jid, ok := handler.bridge.ParsePuppetMXID(userID)
if ok && jid != ce.User.JID {
if ok && jid.User != ce.User.JID.User {
participants = append(participants, jid)
}
}
resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
if err != nil {
ce.Reply("Failed to create group: %v", err)
return
}
portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if len(portal.MXID) != 0 {
portal.log.Warnln("Detected race condition in room creation")
// TODO race condition, clean up the old room
}
portal.MXID = ce.RoomID
portal.Name = roomNameEvent.Name
portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
_, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
if err != nil {
portal.log.Warnln("Failed to enable e2be:", err)
}
portal.Encrypted = true
}
portal.Update()
portal.UpdateBridgeInfo()
ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
inCommunity := ce.User.addPortalToCommunity(portal)
ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
ce.Reply("Not yet implemented")
// TODO reimplement
//resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
//if err != nil {
// ce.Reply("Failed to create group: %v", err)
// return
//}
//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
//portal.roomCreateLock.Lock()
//defer portal.roomCreateLock.Unlock()
//if len(portal.MXID) != 0 {
// portal.log.Warnln("Detected race condition in room creation")
// // TODO race condition, clean up the old room
//}
//portal.MXID = ce.RoomID
//portal.Name = roomNameEvent.Name
//portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
//if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
// _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
// if err != nil {
// portal.log.Warnln("Failed to enable e2be:", err)
// }
// portal.Encrypted = true
//}
//
//portal.Update()
//portal.UpdateBridgeInfo()
//
//ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
//inCommunity := ce.User.addPortalToCommunity(portal)
//ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
}
const cmdSetPowerLevelHelp = `set-pl [user ID] <power level> - Change the power level in a portal room. Only for bridge admins.`
@ -378,25 +390,94 @@ func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) {
}
}
const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
const cmdLoginHelp = `login - Link the bridge to your WhatsApp account as a web client`
// CommandLogin handles login command
func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
if !ce.User.Connect(true) {
ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
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
}
ce.User.Login(ce)
qrChan, 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
}
var qrEventID id.EventID
for item := range qrChan {
switch item {
case whatsmeow.QRChannelSuccess:
jid := ce.User.Client.Store.ID
ce.Reply("Successfully logged in as +%s (device #%d)", jid.User, jid.Device)
case whatsmeow.QRChannelTimeout:
ce.Reply("QR code timed out. Please restart the login.")
case whatsmeow.QRChannelErrUnexpectedEvent:
ce.Reply("Failed to log in: unexpected connection event from server")
case whatsmeow.QRChannelScannedWithoutMultidevice:
ce.Reply("Please enable the WhatsApp multidevice beta and scan the QR code again.")
default:
qrEventID = ce.User.sendQR(ce, string(item), qrEventID)
}
}
_, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
}
const cmdLogoutHelp = `logout - Logout from WhatsApp`
func (user *User) sendQR(ce *CommandEvent, 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 *CommandEvent, 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
}
const cmdLogoutHelp = `logout - Unlink the bridge from your WhatsApp account`
// CommandLogout handles !logout command
func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
if ce.User.Session == nil {
ce.Reply("You're not logged in.")
return
} else if !ce.User.IsConnected() {
} else if !ce.User.IsLoggedIn() {
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.")
return
}
@ -407,17 +488,16 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
}
}
err := ce.User.Conn.Logout()
err := ce.User.Client.Logout()
if err != nil {
ce.User.log.Warnln("Error while logging out:", err)
ce.Reply("Unknown error while logging out: %v", err)
return
}
ce.User.Session = nil
ce.User.removeFromJIDMap(StateLoggedOut)
// TODO this causes a foreign key violation, which should be fixed
//ce.User.JID = ""
ce.User.SetSession(nil)
ce.User.DeleteConnection()
ce.User.DeleteSession()
ce.Reply("Logged out successfully.")
}
@ -439,16 +519,16 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
}
if ce.Args[0] == "presence" || ce.Args[0] == "all" {
customPuppet.EnablePresence = !customPuppet.EnablePresence
var newPresence whatsapp.Presence
var newPresence types.Presence
if customPuppet.EnablePresence {
newPresence = whatsapp.PresenceAvailable
newPresence = types.PresenceAvailable
ce.Reply("Enabled presence bridging")
} else {
newPresence = whatsapp.PresenceUnavailable
newPresence = types.PresenceUnavailable
ce.Reply("Disabled presence bridging")
}
if ce.User.IsConnected() {
_, err := ce.User.Conn.Presence("", newPresence)
if ce.User.IsLoggedIn() {
err := ce.User.Client.SendPresence(newPresence)
if err != nil {
ce.User.log.Warnln("Failed to set presence:", err)
}
@ -468,130 +548,96 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request`
func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) {
if ce.User.Session == nil && ce.User.Conn == nil {
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.JID = ""
ce.User.removeFromJIDMap(StateLoggedOut)
ce.User.SetSession(nil)
ce.User.DeleteConnection()
ce.User.DeleteSession()
ce.Reply("Session information purged")
}
const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp`
func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) {
if ce.User.Conn == nil {
if ce.User.Session == nil {
ce.Reply("No existing connection and no session. Did you mean `login`?")
} else {
ce.Reply("No existing connection, creating one...")
ce.User.Connect(false)
}
return
}
wasConnected := true
err := ce.User.Conn.Disconnect()
if err == whatsapp.ErrNotConnected {
wasConnected = false
} else if err != nil {
ce.User.log.Warnln("Error while disconnecting:", err)
}
ctx := context.Background()
err = ce.User.Conn.Restore(true, ctx)
if err == whatsapp.ErrInvalidSession {
if ce.User.Session != nil {
ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
ce.User.Conn.SetSession(*ce.User.Session)
err = ce.User.Conn.Restore(true, ctx)
} else {
ce.Reply("You are not logged in.")
return
}
} else if err == whatsapp.ErrLoginInProgress {
ce.Reply("A login or reconnection is already in progress.")
return
} else if err == whatsapp.ErrAlreadyLoggedIn {
ce.Reply("You were already connected.")
return
}
if err != nil {
ce.User.log.Warnln("Error while reconnecting:", err)
ce.Reply("Unknown error while reconnecting: %v", err)
ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
err = ce.User.Conn.Disconnect()
if err != nil {
ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
}
return
}
ce.User.ConnectionErrors = 0
var msg string
if wasConnected {
msg = "Reconnected successfully."
} else {
msg = "Connected successfully."
}
ce.Reply(msg)
ce.User.PostLogin()
}
const cmdDeleteConnectionHelp = `delete-connection - Disconnect ignoring errors and delete internal connection state.`
func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) {
if ce.User.Conn == nil {
ce.Reply("You don't have a WhatsApp connection.")
return
}
ce.User.DeleteConnection()
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
ce.Reply("Not yet implemented")
// TODO reimplement
//if ce.User.Client == nil {
// if ce.User.Session == nil {
// ce.Reply("No existing connection and no session. Did you mean `login`?")
// } else {
// ce.Reply("No existing connection, creating one...")
// ce.User.Connect(false)
// }
// return
//}
//
//wasConnected := true
//ce.User.Client.Disconnect()
//ctx := context.Background()
//connected := ce.User.Connect(false)
//
//err = ce.User.Conn.Restore(true, ctx)
//if err == whatsapp.ErrInvalidSession {
// if ce.User.Session != nil {
// ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
// ce.User.Conn.SetSession(*ce.User.Session)
// err = ce.User.Conn.Restore(true, ctx)
// } else {
// ce.Reply("You are not logged in.")
// return
// }
//} else if err == whatsapp.ErrLoginInProgress {
// ce.Reply("A login or reconnection is already in progress.")
// return
//} else if err == whatsapp.ErrAlreadyLoggedIn {
// ce.Reply("You were already connected.")
// return
//}
//if err != nil {
// ce.User.log.Warnln("Error while reconnecting:", err)
// ce.Reply("Unknown error while reconnecting: %v", err)
// ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
// err = ce.User.Conn.Disconnect()
// if err != nil {
// ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
// }
// return
//}
//ce.User.ConnectionErrors = 0
//
//var msg string
//if wasConnected {
// msg = "Reconnected successfully."
//} else {
// msg = "Connected successfully."
//}
//ce.Reply(msg)
//ce.User.PostLogin()
}
const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without logging out)`
func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) {
if ce.User.Conn == nil {
if ce.User.Client == nil {
ce.Reply("You don't have a WhatsApp connection.")
return
}
err := ce.User.Conn.Disconnect()
if err == whatsapp.ErrNotConnected {
ce.Reply("You were not connected.")
return
} else if err != nil {
ce.User.log.Warnln("Error while disconnecting:", err)
ce.Reply("Unknown error while disconnecting: %v", err)
return
}
ce.User.bridge.Metrics.TrackConnectionState(ce.User.JID, false)
ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
ce.User.DeleteConnection()
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
}
const cmdPingHelp = `ping - Check your connection to WhatsApp.`
func (handler *CommandHandler) CommandPing(ce *CommandEvent) {
if ce.User.Session == nil {
if ce.User.IsLoginInProgress() {
ce.Reply("You're not logged into WhatsApp, but there's a login in progress.")
} else {
ce.Reply("You're not logged into WhatsApp.")
}
} else if ce.User.Conn == nil {
ce.Reply("You're not logged into WhatsApp.")
} else if ce.User.Client == nil || !ce.User.Client.IsConnected() {
ce.Reply("You don't have a WhatsApp connection.")
} else if err := ce.User.Conn.AdminTest(); err != nil {
if ce.User.IsLoginInProgress() {
ce.Reply("Connection not OK: %v, but login in progress", err)
} else {
ce.Reply("Connection not OK: %v", err)
}
} else {
ce.Reply("Connection to WhatsApp OK")
ce.Reply("Connection to WhatsApp OK (probably)")
}
}
@ -600,7 +646,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 + " "
}
@ -612,12 +658,12 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix + cmdDeleteSessionHelp,
cmdPrefix + cmdReconnectHelp,
cmdPrefix + cmdDisconnectHelp,
cmdPrefix + cmdDeleteConnectionHelp,
cmdPrefix + cmdPingHelp,
cmdPrefix + cmdSetRelayHelp,
cmdPrefix + cmdUnsetRelayHelp,
cmdPrefix + cmdLoginMatrixHelp,
cmdPrefix + cmdLogoutMatrixHelp,
cmdPrefix + cmdToggleHelp,
cmdPrefix + cmdSyncHelp,
cmdPrefix + cmdListHelp,
cmdPrefix + cmdOpenHelp,
cmdPrefix + cmdPMHelp,
@ -630,35 +676,23 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
}, "\n* "))
}
const cmdSyncHelp = `sync [--create-all] - Synchronize contacts from phone and optionally create portals for group chats.`
// CommandSync handles sync command
func (handler *CommandHandler) CommandSync(ce *CommandEvent) {
user := ce.User
create := len(ce.Args) > 0 && ce.Args[0] == "--create-all"
ce.Reply("Updating contact and chat list...")
handler.log.Debugln("Importing contacts of", user.MXID)
_, err := user.Conn.Contacts()
func canDeletePortal(portal *Portal, userID id.UserID) bool {
members, err := portal.MainIntent().JoinedMembers(portal.MXID)
if err != nil {
user.log.Errorln("Error updating contacts:", err)
ce.Reply("Failed to sync contact list (see logs for details)")
return
portal.log.Errorfln("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err)
return false
}
handler.log.Debugln("Importing chats of", user.MXID)
_, err = user.Conn.Chats()
if err != nil {
user.log.Errorln("Error updating chats:", err)
ce.Reply("Failed to sync chat list (see logs for details)")
return
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
}
}
ce.Reply("Syncing contacts...")
user.syncPuppets(nil)
ce.Reply("Syncing chats...")
user.syncPortals(nil, create)
ce.Reply("Sync complete.")
return true
}
const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.`
@ -669,12 +703,9 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
return
}
if !ce.User.Admin {
users := ce.Portal.GetUserIDs()
if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) {
ce.Reply("Only bridge admins can delete portals with other Matrix users")
return
}
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.")
@ -682,17 +713,23 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
ce.Portal.Cleanup(false)
}
const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals that aren't used by any other user.'`
const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all portals.`
func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
portals := ce.User.GetPortals()
portalsToDelete := make([]*Portal, 0, len(portals))
for _, portal := range portals {
users := portal.GetUserIDs()
if len(users) == 1 && users[0] == ce.User.MXID {
portalsToDelete = append(portalsToDelete, portal)
portals := handler.bridge.GetAllPortals()
var portalsToDelete []*Portal
if ce.User.Admin {
portals = portalsToDelete
} else {
portalsToDelete = portals[:0]
for _, portal := range portals {
if canDeletePortal(portal, ce.User.MXID) {
portalsToDelete = append(portalsToDelete, portal)
}
}
}
leave := func(portal *Portal) {
if len(portal.MXID) > 0 {
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
@ -711,13 +748,12 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
}
}
}
ce.Reply("Found %d portals with no other users, deleting...", len(portalsToDelete))
ce.Reply("Found %d portals, deleting...", len(portalsToDelete))
for _, portal := range portalsToDelete {
portal.Delete()
leave(portal)
}
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background. " +
"You may already continue using the bridge. Use `sync` to recreate portals.")
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
go func() {
for _, portal := range portalsToDelete {
@ -729,21 +765,21 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
const cmdListHelp = `list <contacts|groups> [page] [items per page] - Get a list of all contacts and groups.`
func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
for jid, contact := range input {
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
continue
}
if contacts {
result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
} else {
result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
}
}
sort.Sort(sort.StringSlice(result))
return
}
//func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
// for jid, contact := range input {
// if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
// continue
// }
//
// if contacts {
// result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
// } else {
// result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
// }
// }
// sort.Sort(sort.StringSlice(result))
// return
//}
func (handler *CommandHandler) CommandList(ce *CommandEvent) {
if len(ce.Args) == 0 {
@ -774,33 +810,35 @@ func (handler *CommandHandler) CommandList(ce *CommandEvent) {
ce.Reply("Warning: a high number of items per page may fail to send a reply")
}
}
contacts := mode[0] == 'c'
typeName := "Groups"
if contacts {
typeName = "Contacts"
}
ce.User.Conn.Store.ContactsLock.RLock()
result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
ce.User.Conn.Store.ContactsLock.RUnlock()
if len(result) == 0 {
ce.Reply("No %s found", strings.ToLower(typeName))
return
}
pages := int(math.Ceil(float64(len(result)) / float64(max)))
if (page-1)*max >= len(result) {
if pages == 1 {
ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
} else {
ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
}
return
}
lastIndex := page * max
if lastIndex > len(result) {
lastIndex = len(result)
}
result = result[(page-1)*max : lastIndex]
ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
ce.Reply("Not yet implemented")
// TODO reimplement
//contacts := mode[0] == 'c'
//typeName := "Groups"
//if contacts {
// typeName = "Contacts"
//}
//ce.User.Conn.Store.ContactsLock.RLock()
//result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
//ce.User.Conn.Store.ContactsLock.RUnlock()
//if len(result) == 0 {
// ce.Reply("No %s found", strings.ToLower(typeName))
// return
//}
//pages := int(math.Ceil(float64(len(result)) / float64(max)))
//if (page-1)*max >= len(result) {
// if pages == 1 {
// ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
// } else {
// ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
// }
// return
//}
//lastIndex := page * max
//if lastIndex > len(result) {
// lastIndex = len(result)
//}
//result = result[(page-1)*max : lastIndex]
//ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
}
const cmdOpenHelp = `open <_group JID_> - Open a group chat portal.`
@ -810,91 +848,75 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
ce.Reply("**Usage:** `open <group JID>`")
return
}
ce.Reply("Not yet implemented")
user := ce.User
jid := ce.Args[0]
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
return
}
user.Conn.Store.ContactsLock.RLock()
contact, ok := user.Conn.Store.Contacts[jid]
user.Conn.Store.ContactsLock.RUnlock()
if !ok {
ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
return
}
handler.log.Debugln("Importing", jid, "for", user)
portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
if len(portal.MXID) > 0 {
portal.Sync(user, contact)
ce.Reply("Portal room synced.")
} else {
portal.Sync(user, contact)
ce.Reply("Portal room created.")
}
_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
// TODO reimplement
//user := ce.User
//jid := ce.Args[0]
//if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
// ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
// return
//}
//
//user.Conn.Store.ContactsLock.RLock()
//contact, ok := user.Conn.Store.Contacts[jid]
//user.Conn.Store.ContactsLock.RUnlock()
//if !ok {
// ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
// return
//}
//handler.log.Debugln("Importing", jid, "for", user)
//portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
//if len(portal.MXID) > 0 {
// portal.Sync(user, contact)
// ce.Reply("Portal room synced.")
//} else {
// portal.Sync(user, contact)
// ce.Reply("Portal room created.")
//}
//_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
}
const cmdPMHelp = `pm [--force] <_international phone number_> - Open a private chat with the given phone number.`
const cmdPMHelp = `pm <_international phone number_> - Open a private chat with the given phone number.`
func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `pm [--force] <international phone number>`")
ce.Reply("**Usage:** `pm <international phone number>`")
return
}
force := ce.Args[0] == "--force"
if force {
ce.Args = ce.Args[1:]
}
user := ce.User
number := strings.Join(ce.Args, "")
if number[0] == '+' {
number = number[1:]
resp, err := ce.User.Client.IsOnWhatsApp([]string{number})
if err != nil {
ce.Reply("Failed to check if user is on WhatsApp: %v", err)
return
} else if len(resp) == 0 {
ce.Reply("Didn't get a response to checking if the user is on WhatsApp")
return
}
for _, char := range number {
if char < '0' || char > '9' {
ce.Reply("Invalid phone number.")
return
}
targetUser := resp[0]
if !targetUser.IsIn {
ce.Reply("The server said +%s is not on WhatsApp", targetUser.JID.User)
return
}
jid := number + whatsapp.NewUserSuffix
handler.log.Debugln("Importing", jid, "for", user)
user.Conn.Store.ContactsLock.RLock()
contact, ok := user.Conn.Store.Contacts[jid]
user.Conn.Store.ContactsLock.RUnlock()
if !ok {
if !force {
ce.Reply("Phone number not found in contacts. Try syncing contacts with `sync` first. " +
"To create a portal anyway, use `pm --force <number>`.")
return
}
contact = whatsapp.Contact{JID: jid}
}
puppet := user.bridge.GetPuppetByJID(contact.JID)
puppet.Sync(user, contact)
portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.JID, user.JID))
handler.log.Debugln("Importing", targetUser.JID, "for", user)
puppet := user.bridge.GetPuppetByJID(targetUser.JID)
puppet.SyncContact(user, true)
portal := user.GetPortalByJID(puppet.JID)
if len(portal.MXID) > 0 {
var err error
if !user.IsRelaybot {
err = portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
}
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)
return
}
}
err := portal.CreateMatrixRoom(user)
err = portal.CreateMatrixRoom(user)
if err != nil {
ce.Reply("Failed to create portal room: %v", err)
return

View file

@ -1,132 +0,0 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"fmt"
"net/http"
"maunium.net/go/mautrix"
)
func (user *User) inviteToCommunity() {
url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", user.MXID)
reqBody := map[string]interface{}{}
_, err := user.bridge.Bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
if err != nil {
user.log.Warnfln("Failed to invite user to personal filtering community %s: %v", user.CommunityID, err)
}
}
func (user *User) updateCommunityProfile() {
url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "profile")
profileReq := struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
ShortDescription string `json:"short_description"`
}{"WhatsApp", user.bridge.Config.AppService.Bot.Avatar, "Your WhatsApp bridged chats"}
_, err := user.bridge.Bot.MakeRequest(http.MethodPost, url, &profileReq, nil)
if err != nil {
user.log.Warnfln("Failed to update metadata of %s: %v", user.CommunityID, err)
}
}
func (user *User) createCommunity() {
if user.IsRelaybot || !user.bridge.Config.Bridge.EnableCommunities() {
return
}
localpart, server, _ := user.MXID.Parse()
community := user.bridge.Config.Bridge.FormatCommunity(localpart, server)
user.log.Debugln("Creating personal filtering community", community)
bot := user.bridge.Bot
req := struct {
Localpart string `json:"localpart"`
}{community}
resp := struct {
GroupID string `json:"group_id"`
}{}
_, err := bot.MakeRequest(http.MethodPost, bot.BuildURL("create_group"), &req, &resp)
if err != nil {
if httpErr, ok := err.(mautrix.HTTPError); ok {
if httpErr.RespError.Err != "Group already exists" {
user.log.Warnln("Server responded with error creating personal filtering community:", err)
return
} else {
user.log.Debugln("Personal filtering community", resp.GroupID, "already existed")
user.CommunityID = fmt.Sprintf("+%s:%s", req.Localpart, user.bridge.Config.Homeserver.Domain)
}
} else {
user.log.Warnln("Unknown error creating personal filtering community:", err)
return
}
} else {
user.log.Infoln("Created personal filtering community %s", resp.GroupID)
user.CommunityID = resp.GroupID
user.inviteToCommunity()
user.updateCommunityProfile()
}
}
func (user *User) addPuppetToCommunity(puppet *Puppet) bool {
if user.IsRelaybot || len(user.CommunityID) == 0 {
return false
}
bot := user.bridge.Bot
url := bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", puppet.MXID)
blankReqBody := map[string]interface{}{}
_, err := bot.MakeRequest(http.MethodPut, url, &blankReqBody, nil)
if err != nil {
user.log.Warnfln("Failed to invite %s to %s: %v", puppet.MXID, user.CommunityID, err)
return false
}
reqBody := map[string]map[string]string{
"m.visibility": {
"type": "private",
},
}
url = bot.BuildURLWithQuery(mautrix.URLPath{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{
"user_id": puppet.MXID.String(),
})
_, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
if err != nil {
user.log.Warnfln("Failed to join %s as %s: %v", user.CommunityID, puppet.MXID, err)
return false
}
user.log.Debugln("Added", puppet.MXID, "to", user.CommunityID)
return true
}
func (user *User) addPortalToCommunity(portal *Portal) bool {
if user.IsRelaybot || len(user.CommunityID) == 0 || len(portal.MXID) == 0 {
return false
}
bot := user.bridge.Bot
url := bot.BuildURL("groups", user.CommunityID, "admin", "rooms", portal.MXID)
reqBody := map[string]map[string]string{
"m.visibility": {
"type": "private",
},
}
_, err := bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
if err != nil {
user.log.Warnfln("Failed to add %s to %s: %v", portal.MXID, user.CommunityID, err)
return false
}
user.log.Debugln("Added", portal.MXID, "to", user.CommunityID)
return true
}

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -17,12 +17,11 @@
package config
import (
"bytes"
"strconv"
"strings"
"text/template"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -31,18 +30,8 @@ import (
type BridgeConfig struct {
UsernameTemplate string `yaml:"username_template"`
DisplaynameTemplate string `yaml:"displayname_template"`
CommunityTemplate string `yaml:"community_template"`
ConnectionTimeout int `yaml:"connection_timeout"`
FetchMessageOnTimeout bool `yaml:"fetch_message_on_timeout"`
DeliveryReceipts bool `yaml:"delivery_receipts"`
MaxConnectionAttempts int `yaml:"max_connection_attempts"`
ConnectionRetryDelay int `yaml:"connection_retry_delay"`
ReportConnectionRetry bool `yaml:"report_connection_retry"`
AggressiveReconnect bool `yaml:"aggressive_reconnect"`
ChatListWait int `yaml:"chat_list_wait"`
PortalSyncWait int `yaml:"portal_sync_wait"`
UserMessageBuffer int `yaml:"user_message_buffer"`
PortalMessageBuffer int `yaml:"portal_message_buffer"`
CallNotices struct {
@ -50,15 +39,13 @@ type BridgeConfig struct {
End bool `yaml:"end"`
} `yaml:"call_notices"`
InitialChatSync int `yaml:"initial_chat_sync_count"`
InitialHistoryFill int `yaml:"initial_history_fill_count"`
HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"`
RecoverChatSync int `yaml:"recovery_chat_sync_count"`
RecoverHistory bool `yaml:"recovery_history_backfill"`
ChatMetaSync bool `yaml:"chat_meta_sync"`
UserAvatarSync bool `yaml:"user_avatar_sync"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncChatMaxAge int64 `yaml:"sync_max_chat_age"`
HistorySync struct {
CreatePortals bool `yaml:"create_portals"`
Backfill bool `yaml:"backfill"`
DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
} `yaml:"history_sync"`
UserAvatarSync bool `yaml:"user_avatar_sync"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
@ -66,16 +53,15 @@ type BridgeConfig struct {
DefaultBridgePresence bool `yaml:"default_bridge_presence"`
LoginSharedSecret string `yaml:"login_shared_secret"`
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
PrivateChatPortalMeta bool `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"`
MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
PrivateChatPortalMeta bool `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"`
MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
@ -103,44 +89,26 @@ type BridgeConfig struct {
Permissions PermissionConfig `yaml:"permissions"`
Relaybot RelaybotConfig `yaml:"relaybot"`
Relay RelaybotConfig `yaml:"relay"`
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
communityTemplate *template.Template `yaml:"-"`
}
func (bc *BridgeConfig) setDefaults() {
bc.ConnectionTimeout = 20
bc.FetchMessageOnTimeout = false
bc.DeliveryReceipts = false
bc.MaxConnectionAttempts = 3
bc.ConnectionRetryDelay = -1
bc.ReportConnectionRetry = true
bc.ChatListWait = 30
bc.PortalSyncWait = 600
bc.UserMessageBuffer = 1024
bc.PortalMessageBuffer = 128
bc.CallNotices.Start = true
bc.CallNotices.End = true
bc.InitialChatSync = 10
bc.InitialHistoryFill = 20
bc.RecoverChatSync = -1
bc.RecoverHistory = true
bc.ChatMetaSync = true
bc.HistorySync.CreatePortals = true
bc.UserAvatarSync = true
bc.BridgeMatrixLeave = true
bc.SyncChatMaxAge = 259200
bc.SyncWithCustomPuppets = true
bc.DefaultBridgePresence = true
bc.DefaultBridgeReceipts = true
bc.LoginSharedSecret = ""
bc.InviteOwnPuppetForBackfilling = true
bc.PrivateChatPortalMeta = false
bc.BridgeNotices = true
bc.EnableStatusBroadcast = true
@ -167,13 +135,6 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}
if len(bc.CommunityTemplate) > 0 {
bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate)
if err != nil {
return err
}
}
return nil
}
@ -181,44 +142,43 @@ type UsernameTemplateArgs struct {
UserID id.UserID
}
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
var buf bytes.Buffer
if index := strings.IndexRune(contact.JID, '@'); index > 0 {
contact.JID = "+" + contact.JID[:index]
}
bc.displaynameTemplate.Execute(&buf, contact)
type legacyContactInfo struct {
types.ContactInfo
Phone string
Notify string
VName string
Name string
Short string
JID string
}
func (bc BridgeConfig) FormatDisplayname(jid types.JID, contact types.ContactInfo) (string, int8) {
var buf strings.Builder
_ = bc.displaynameTemplate.Execute(&buf, legacyContactInfo{
ContactInfo: contact,
Notify: contact.PushName,
VName: contact.BusinessName,
Name: contact.FullName,
Short: contact.FirstName,
Phone: "+" + jid.User,
JID: "+" + jid.User,
})
var quality int8
switch {
case len(contact.Notify) > 0 || len(contact.VName) > 0:
case len(contact.PushName) > 0 || len(contact.BusinessName) > 0:
quality = 3
case len(contact.Name) > 0 || len(contact.Short) > 0:
case len(contact.FullName) > 0 || len(contact.FirstName) > 0:
quality = 2
case len(contact.JID) > 0:
quality = 1
default:
quality = 0
quality = 1
}
return buf.String(), quality
}
func (bc BridgeConfig) FormatUsername(userID whatsapp.JID) string {
var buf bytes.Buffer
bc.usernameTemplate.Execute(&buf, userID)
return buf.String()
}
type CommunityTemplateArgs struct {
Localpart string
Server string
}
func (bc BridgeConfig) EnableCommunities() bool {
return bc.communityTemplate != nil
}
func (bc BridgeConfig) FormatCommunity(localpart, server string) string {
var buf bytes.Buffer
bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server})
func (bc BridgeConfig) FormatUsername(username string) string {
var buf strings.Builder
_ = bc.usernameTemplate.Execute(&buf, username)
return buf.String()
}
@ -227,10 +187,10 @@ type PermissionConfig map[string]PermissionLevel
type PermissionLevel int
const (
PermissionLevelDefault PermissionLevel = 0
PermissionLevelRelaybot PermissionLevel = 5
PermissionLevelUser PermissionLevel = 10
PermissionLevelAdmin PermissionLevel = 100
PermissionLevelDefault PermissionLevel = 0
PermissionLevelRelay PermissionLevel = 5
PermissionLevelUser PermissionLevel = 10
PermissionLevelAdmin PermissionLevel = 100
)
func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
@ -245,8 +205,8 @@ func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
}
for key, value := range rawPC {
switch strings.ToLower(value) {
case "relaybot":
(*pc)[key] = PermissionLevelRelaybot
case "relaybot", "relay":
(*pc)[key] = PermissionLevelRelay
case "user":
(*pc)[key] = PermissionLevelUser
case "admin":
@ -270,8 +230,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
rawPC := make(map[string]string)
for key, value := range *pc {
switch value {
case PermissionLevelRelaybot:
rawPC[key] = "relaybot"
case PermissionLevelRelay:
rawPC[key] = "relay"
case PermissionLevelUser:
rawPC[key] = "user"
case PermissionLevelAdmin:
@ -283,8 +243,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
return rawPC, nil
}
func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool {
return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot
func (pc PermissionConfig) IsRelayWhitelisted(userID id.UserID) bool {
return pc.GetPermissionLevel(userID) >= PermissionLevelRelay
}
func (pc PermissionConfig) IsWhitelisted(userID id.UserID) bool {
@ -316,10 +276,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

@ -14,6 +14,7 @@
// 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/>.
//go:build cgo && !nocrypto
// +build cgo,!nocrypto
package main
@ -100,7 +101,8 @@ func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info ev
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"}
}
user := helper.bridge.GetUserByMXID(device.UserID)
if !user.Admin && !user.IsInPortal(portal.Key) {
// FIXME reimplement IsInPortal
if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ {
helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID)
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"}
}

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -23,7 +23,7 @@ import (
"errors"
"time"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
@ -160,7 +160,7 @@ func (puppet *Puppet) stopSyncing() {
}
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
if !puppet.customUser.IsConnected() {
if !puppet.customUser.IsLoggedIn() {
puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
return nil
}
@ -200,14 +200,14 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
}
func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
presence := whatsapp.PresenceAvailable
presence := types.PresenceAvailable
if event.Content.Raw["presence"].(string) != "online" {
presence = whatsapp.PresenceUnavailable
presence = types.PresenceUnavailable
puppet.customUser.log.Debugln("Marking offline")
} else {
puppet.customUser.log.Debugln("Marking online")
}
_, err := puppet.customUser.Conn.Presence("", presence)
err := puppet.customUser.Client.SendPresence(presence)
if err != nil {
puppet.customUser.log.Warnln("Failed to set presence:", err)
}
@ -222,7 +222,7 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
// Ignore double puppeted read receipts.
} else if message := puppet.bridge.DB.Message.GetByMXID(eventID); message != nil {
puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
_, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
err := puppet.customUser.Client.MarkRead([]types.MessageID{message.JID}, time.UnixMilli(receipt.Timestamp), portal.Key.JID, message.Sender)
if err != nil {
puppet.customUser.log.Warnln("Error marking read:", err)
}
@ -240,14 +240,14 @@ func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
}
if puppet.customTypingIn[evt.RoomID] != isTyping {
puppet.customTypingIn[evt.RoomID] = isTyping
presence := whatsapp.PresenceComposing
presence := types.ChatPresenceComposing
if !isTyping {
puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
presence = whatsapp.PresencePaused
presence = types.ChatPresencePaused
} else {
puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
}
_, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID)
if err != nil {
puppet.customUser.log.Warnln("Error setting typing:", err)
}

View file

@ -14,6 +14,7 @@
// 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/>.
//go:build cgo && !nocrypto
// +build cgo,!nocrypto
package database

View file

@ -19,14 +19,19 @@ package database
import (
"database/sql"
_ "github.com/lib/pq"
"github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
log "maunium.net/go/maulogger/v2"
"go.mau.fi/whatsmeow/store/sqlstore"
"maunium.net/go/mautrix-whatsapp/database/upgrades"
)
func init() {
sqlstore.PostgresArrayWrapper = pq.Array
}
type Database struct {
*sql.DB
log log.Logger

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -21,11 +21,11 @@ import (
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow/types"
)
type MessageQuery struct {
@ -40,45 +40,66 @@ func (mq *MessageQuery) New() *Message {
}
}
const (
getAllMessagesQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
WHERE chat_jid=$1 AND chat_receiver=$2
`
getMessageByJIDQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3
`
getMessageByMXIDQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
WHERE mxid=$1
`
getLastMessageInChatQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1
`
getFirstMessageInChatQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND sent=true ORDER BY timestamp ASC LIMIT 1
`
)
func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
rows, err := mq.db.Query("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent FROM message WHERE chat_jid=$1 AND chat_receiver=$2", chat.JID, chat.Receiver)
rows, err := mq.db.Query(getAllMessagesQuery, chat.JID, chat.Receiver)
if err != nil || rows == nil {
return nil
}
defer rows.Close()
for rows.Next() {
messages = append(messages, mq.New().Scan(rows))
}
return
}
func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.MessageID) *Message {
return mq.maybeScan(mq.db.QueryRow(getMessageByJIDQuery, chat.JID, chat.Receiver, jid))
}
func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
"FROM message WHERE mxid=$1", mxid)
return mq.maybeScan(mq.db.QueryRow(getMessageByMXIDQuery, mxid))
}
func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message {
return mq.GetLastInChatBefore(chat, time.Now().Unix()+60)
return mq.GetLastInChatBefore(chat, time.Now().Add(60*time.Second))
}
func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp int64) *Message {
msg := mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1",
chat.JID, chat.Receiver, maxTimestamp)
if msg == nil || msg.Timestamp == 0 {
func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp time.Time) *Message {
msg := mq.maybeScan(mq.db.QueryRow(getLastMessageInChatQuery, chat.JID, chat.Receiver, maxTimestamp.Unix()))
if msg == nil || msg.Timestamp.IsZero() {
// Old db, we don't know what the last message is.
return nil
}
return msg
}
func (mq *MessageQuery) get(query string, args ...interface{}) *Message {
row := mq.db.QueryRow(query, args...)
func (mq *MessageQuery) GetFirstInChat(chat PortalKey) *Message {
return mq.maybeScan(mq.db.QueryRow(getFirstMessageInChatQuery, chat.JID, chat.Receiver))
}
func (mq *MessageQuery) maybeScan(row *sql.Row) *Message {
if row == nil {
return nil
}
@ -90,11 +111,13 @@ type Message struct {
log log.Logger
Chat PortalKey
JID whatsapp.MessageID
JID types.MessageID
MXID id.EventID
Sender whatsapp.JID
Timestamp int64
Sender types.JID
Timestamp time.Time
Sent bool
DecryptionError bool
}
func (msg *Message) IsFakeMXID() bool {
@ -102,22 +125,30 @@ func (msg *Message) IsFakeMXID() bool {
}
func (msg *Message) Scan(row Scannable) *Message {
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &msg.Sent)
var ts int64
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &ts, &msg.Sent, &msg.DecryptionError)
if err != nil {
if err != sql.ErrNoRows {
msg.log.Errorln("Database scan failed:", err)
}
return nil
}
if ts != 0 {
msg.Timestamp = time.Unix(ts, 0)
}
return msg
}
func (msg *Message) Insert() {
var sender interface{} = msg.Sender
// Slightly hacky hack to allow inserting empty senders (used for post-backfill dummy events)
if msg.Sender.IsEmpty() {
sender = ""
}
_, err := msg.db.Exec(`INSERT INTO message
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.Timestamp, msg.Sent)
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.Timestamp.Unix(), msg.Sent, msg.DecryptionError)
if err != nil {
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
}
@ -131,6 +162,15 @@ func (msg *Message) MarkSent() {
}
}
func (msg *Message) UpdateMXID(mxid id.EventID, stillDecryptionError bool) {
msg.MXID = mxid
msg.DecryptionError = stillDecryptionError
_, err := msg.db.Exec("UPDATE message SET mxid=$4, decryption_error=$5 WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID, mxid, stillDecryptionError)
if err != nil {
msg.log.Warnfln("Failed to update %s@%s: %v", msg.Chat, msg.JID, err)
}
}
func (msg *Message) Delete() {
_, err := msg.db.Exec("DELETE FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID)
if err != nil {

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@ -18,42 +18,40 @@ package database
import (
"database/sql"
"strings"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow/types"
)
type PortalKey struct {
JID whatsapp.JID
Receiver whatsapp.JID
JID types.JID
Receiver types.JID
}
func GroupPortalKey(jid whatsapp.JID) PortalKey {
return PortalKey{
JID: jid,
Receiver: jid,
}
func GroupPortalKey(jid types.JID) PortalKey {
return NewPortalKey(jid, jid)
}
func NewPortalKey(jid, receiver whatsapp.JID) PortalKey {
if strings.HasSuffix(jid, whatsapp.GroupSuffix) {
func NewPortalKey(jid, receiver types.JID) PortalKey {
if jid.Server == types.GroupServer {
receiver = jid
} else if jid.Server == types.LegacyUserServer {
jid.Server = types.DefaultUserServer
}
return PortalKey{
JID: jid,
Receiver: receiver,
JID: jid.ToNonAD(),
Receiver: receiver.ToNonAD(),
}
}
func (key PortalKey) String() string {
if key.Receiver == key.JID {
return key.JID
return key.JID.String()
}
return key.JID + "-" + key.Receiver
return key.JID.String() + "-" + key.Receiver.String()
}
type PortalQuery struct {
@ -80,12 +78,12 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
}
func (pq *PortalQuery) GetAllByJID(jid whatsapp.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
func (pq *PortalQuery) GetAllByJID(jid types.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid.ToNonAD())
}
func (pq *PortalQuery) FindPrivateChats(receiver whatsapp.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver)
func (pq *PortalQuery) FindPrivateChats(receiver types.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver.ToNonAD())
}
func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) {
@ -120,11 +118,16 @@ type Portal struct {
Avatar string
AvatarURL id.ContentURI
Encrypted bool
FirstEventID id.EventID
NextBatchID id.BatchID
RelayUserID id.UserID
}
func (portal *Portal) Scan(row Scannable) *Portal {
var mxid, avatarURL sql.NullString
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted)
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)
@ -133,6 +136,9 @@ func (portal *Portal) Scan(row Scannable) *Portal {
}
portal.MXID = id.RoomID(mxid.String)
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
}
@ -143,21 +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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted)
_, 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() {
var mxid *id.RoomID
if len(portal.MXID) > 0 {
mxid = &portal.MXID
}
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6 WHERE jid=$7 AND receiver=$8",
mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, 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)
}
@ -169,26 +178,3 @@ func (portal *Portal) Delete() {
portal.log.Warnfln("Failed to delete %s: %v", portal.Key, err)
}
}
func (portal *Portal) GetUserIDs() []id.UserID {
rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
WHERE "user".jid=user_portal.user_jid
AND user_portal.portal_jid=$1
AND user_portal.portal_receiver=$2`,
portal.Key.JID, portal.Key.Receiver)
if err != nil {
portal.log.Debugln("Failed to get portal user ids:", err)
return nil
}
var userIDs []id.UserID
for rows.Next() {
var userID id.UserID
err = rows.Scan(&userID)
if err != nil {
portal.log.Warnln("Failed to scan row:", err)
continue
}
userIDs = append(userIDs, userID)
}
return userIDs
}

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -20,10 +20,9 @@ import (
"database/sql"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow/types"
)
type PuppetQuery struct {
@ -42,7 +41,7 @@ func (pq *PuppetQuery) New() *Puppet {
}
func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet")
rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet")
if err != nil || rows == nil {
return nil
}
@ -53,8 +52,8 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) {
return
}
func (pq *PuppetQuery) Get(jid whatsapp.JID) *Puppet {
row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE jid=$1", jid)
func (pq *PuppetQuery) Get(jid types.JID) *Puppet {
row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE username=$1", jid.User)
if row == nil {
return nil
}
@ -62,7 +61,7 @@ func (pq *PuppetQuery) Get(jid whatsapp.JID) *Puppet {
}
func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet {
row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid)
row := pq.db.QueryRow("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid=$1", mxid)
if row == nil {
return nil
}
@ -70,7 +69,7 @@ func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet {
}
func (pq *PuppetQuery) GetAllWithCustomMXID() (puppets []*Puppet) {
rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''")
rows, err := pq.db.Query("SELECT username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts FROM puppet WHERE custom_mxid<>''")
if err != nil || rows == nil {
return nil
}
@ -85,7 +84,7 @@ type Puppet struct {
db *Database
log log.Logger
JID whatsapp.JID
JID types.JID
Avatar string
AvatarURL id.ContentURI
Displayname string
@ -102,13 +101,15 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
var displayname, avatar, avatarURL, customMXID, accessToken, nextBatch sql.NullString
var quality sql.NullInt64
var enablePresence, enableReceipts sql.NullBool
err := row.Scan(&puppet.JID, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts)
var username string
err := row.Scan(&username, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch, &enablePresence, &enableReceipts)
if err != nil {
if err != sql.ErrNoRows {
puppet.log.Errorln("Database scan failed:", err)
}
return nil
}
puppet.JID = types.NewJID(username, types.DefaultUserServer)
puppet.Displayname = displayname.String
puppet.Avatar = avatar.String
puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
@ -122,16 +123,20 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
}
func (puppet *Puppet) Insert() {
_, err := puppet.db.Exec("INSERT INTO puppet (jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
puppet.JID, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts)
if puppet.JID.Server != types.DefaultUserServer {
puppet.log.Warnfln("Not inserting %s: not a user", puppet.JID)
return
}
_, err := puppet.db.Exec("INSERT INTO puppet (username, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch, enable_presence, enable_receipts) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
puppet.JID.User, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts)
if err != nil {
puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err)
}
}
func (puppet *Puppet) Update() {
_, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7, enable_presence=$8, enable_receipts=$9 WHERE jid=$10",
puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, puppet.JID)
_, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7, enable_presence=$8, enable_receipts=$9 WHERE username=$10",
puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.EnablePresence, puppet.EnableReceipts, puppet.JID.User)
if err != nil {
puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
}

View file

@ -0,0 +1,13 @@
package upgrades
import (
"database/sql"
"go.mau.fi/whatsmeow/store/sqlstore"
)
func init() {
upgrades[24] = upgrade{"Add whatsmeow state store", func(tx *sql.Tx, ctx context) error {
return sqlstore.Upgrades[0](tx, sqlstore.NewWithDB(ctx.db, ctx.dialect.String(), nil))
}}
}

View file

@ -0,0 +1,93 @@
package upgrades
import (
"database/sql"
)
func init() {
upgrades[25] = upgrade{"Update things for multidevice", func(tx *sql.Tx, ctx context) error {
// This is probably not necessary
_, err := tx.Exec("DROP TABLE user_portal")
if err != nil {
return err
}
// Remove invalid puppet rows
_, err = tx.Exec("DELETE FROM puppet WHERE jid LIKE '%@g.us' OR jid LIKE '%@broadcast'")
if err != nil {
return err
}
// Remove the suffix from puppets since they'll all have the same suffix
_, err = tx.Exec("UPDATE puppet SET jid=REPLACE(jid, '@s.whatsapp.net', '')")
if err != nil {
return err
}
// Rename column to correctly represent the new content
_, err = tx.Exec("ALTER TABLE puppet RENAME COLUMN jid TO username")
if err != nil {
return err
}
if ctx.dialect == SQLite {
// Message content was removed from the main message table earlier, but the backup table still exists for SQLite
_, err = tx.Exec("DROP TABLE IF EXISTS old_message")
_, err = tx.Exec(`ALTER TABLE "user" RENAME TO old_user`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE "user" (
mxid TEXT PRIMARY KEY,
username TEXT UNIQUE,
agent SMALLINT,
device SMALLINT,
management_room TEXT
)`)
if err != nil {
return err
}
// No need to copy auth data, users need to relogin anyway
_, err = tx.Exec(`INSERT INTO "user" (mxid, management_room, last_connection) SELECT mxid, management_room, last_connection FROM old_user`)
if err != nil {
return err
}
_, err = tx.Exec("DROP TABLE old_user")
if err != nil {
return err
}
} else {
// The jid column never actually contained the full JID, so let's rename it.
_, err = tx.Exec(`ALTER TABLE "user" RENAME COLUMN jid TO username`)
if err != nil {
return err
}
// The auth data is now in the whatsmeow_device table.
for _, column := range []string{"last_connection", "client_id", "client_token", "server_token", "enc_key", "mac_key"} {
_, err = tx.Exec(`ALTER TABLE "user" DROP COLUMN ` + column)
if err != nil {
return err
}
}
// The whatsmeow_device table is keyed by the full JID, so we need to store the other parts of the JID here too.
_, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN agent SMALLINT`)
if err != nil {
return err
}
_, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN device SMALLINT`)
if err != nil {
return err
}
// Clear all usernames, the users need to relogin anyway.
_, err = tx.Exec(`UPDATE "user" SET username=null`)
if err != nil {
return err
}
}
return nil
}}
}

View file

@ -0,0 +1,19 @@
package upgrades
import (
"database/sql"
)
func init() {
upgrades[26] = upgrade{"Add columns to store infinite backfill pointers for portals", func(tx *sql.Tx, ctx context) error {
_, err := tx.Exec(`ALTER TABLE portal ADD COLUMN first_event_id TEXT NOT NULL DEFAULT ''`)
if err != nil {
return err
}
_, err = tx.Exec(`ALTER TABLE portal ADD COLUMN next_batch_id TEXT NOT NULL DEFAULT ''`)
if err != nil {
return err
}
return nil
}}
}

View file

@ -0,0 +1,12 @@
package upgrades
import (
"database/sql"
)
func init() {
upgrades[27] = upgrade{"Add marker for WhatsApp decryption errors in message table", func(tx *sql.Tx, ctx context) error {
_, err := tx.Exec(`ALTER TABLE message ADD COLUMN decryption_error BOOLEAN NOT NULL DEFAULT false`)
return 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 = 24
const NumberOfUpgrades = 29
var upgrades [NumberOfUpgrades]upgrade

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
@ -18,15 +18,11 @@ package database
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow/types"
)
type UserQuery struct {
@ -42,7 +38,7 @@ func (uq *UserQuery) New() *User {
}
func (uq *UserQuery) GetAll() (users []*User) {
rows, err := uq.db.Query(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user"`)
rows, err := uq.db.Query(`SELECT mxid, username, agent, device, management_room FROM "user"`)
if err != nil || rows == nil {
return nil
}
@ -54,15 +50,15 @@ func (uq *UserQuery) GetAll() (users []*User) {
}
func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE mxid=$1`, userID)
row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room FROM "user" WHERE mxid=$1`, userID)
if row == nil {
return nil
}
return uq.New().Scan(row)
}
func (uq *UserQuery) GetByJID(userID whatsapp.JID) *User {
row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE jid=$1`, stripSuffix(userID))
func (uq *UserQuery) GetByUsername(username string) *User {
row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room FROM "user" WHERE username=$1`, username)
if row == nil {
return nil
}
@ -74,185 +70,151 @@ type User struct {
log log.Logger
MXID id.UserID
JID whatsapp.JID
JID types.JID
ManagementRoom id.RoomID
Session *whatsapp.Session
LastConnection int64
}
func (user *User) Scan(row Scannable) *User {
var jid, clientID, clientToken, serverToken sql.NullString
var encKey, macKey []byte
err := row.Scan(&user.MXID, &jid, &user.ManagementRoom, &user.LastConnection, &clientID, &clientToken, &serverToken, &encKey, &macKey)
var username sql.NullString
var device, agent sql.NullByte
err := row.Scan(&user.MXID, &username, &agent, &device, &user.ManagementRoom)
if err != nil {
if err != sql.ErrNoRows {
user.log.Errorln("Database scan failed:", err)
}
return nil
}
if len(jid.String) > 0 && len(clientID.String) > 0 {
user.JID = jid.String + whatsapp.NewUserSuffix
user.Session = &whatsapp.Session{
ClientID: clientID.String,
ClientToken: clientToken.String,
ServerToken: serverToken.String,
EncKey: encKey,
MacKey: macKey,
Wid: jid.String + whatsapp.OldUserSuffix,
}
} else {
user.Session = nil
if len(username.String) > 0 {
user.JID = types.NewADJID(username.String, agent.Byte, device.Byte)
}
return user
}
func stripSuffix(jid whatsapp.JID) string {
if len(jid) == 0 {
return jid
}
index := strings.IndexRune(jid, '@')
if index < 0 {
return jid
}
return jid[:index]
}
func (user *User) jidPtr() *string {
if len(user.JID) > 0 {
str := stripSuffix(user.JID)
return &str
func (user *User) usernamePtr() *string {
if !user.JID.IsEmpty() {
return &user.JID.User
}
return nil
}
func (user *User) sessionUnptr() (sess whatsapp.Session) {
if user.Session != nil {
sess = *user.Session
func (user *User) agentPtr() *uint8 {
if !user.JID.IsEmpty() {
return &user.JID.Agent
}
return
return nil
}
func (user *User) devicePtr() *uint8 {
if !user.JID.IsEmpty() {
return &user.JID.Device
}
return nil
}
func (user *User) Insert() {
sess := user.sessionUnptr()
_, err := user.db.Exec(`INSERT INTO "user" (mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
user.MXID, user.jidPtr(),
user.ManagementRoom, user.LastConnection,
sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey)
_, err := user.db.Exec(`INSERT INTO "user" (mxid, username, agent, device, management_room) VALUES ($1, $2, $3, $4, $5)`,
user.MXID, user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom)
if err != nil {
user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
}
}
func (user *User) UpdateLastConnection() {
user.LastConnection = time.Now().Unix()
_, err := user.db.Exec(`UPDATE "user" SET last_connection=$1 WHERE mxid=$2`,
user.LastConnection, user.MXID)
if err != nil {
user.log.Warnfln("Failed to update last connection ts: %v", err)
}
}
func (user *User) Update() {
sess := user.sessionUnptr()
_, err := user.db.Exec(`UPDATE "user" SET jid=$1, management_room=$2, last_connection=$3, client_id=$4, client_token=$5, server_token=$6, enc_key=$7, mac_key=$8 WHERE mxid=$9`,
user.jidPtr(), user.ManagementRoom, user.LastConnection,
sess.ClientID, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey,
user.MXID)
_, err := user.db.Exec(`UPDATE "user" SET username=$1, agent=$2, device=$3, management_room=$4 WHERE mxid=$5`,
user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.MXID)
if err != nil {
user.log.Warnfln("Failed to update %s: %v", user.MXID, err)
}
}
type PortalKeyWithMeta struct {
PortalKey
InCommunity bool
}
func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error {
tx, err := user.db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr())
if err != nil {
_ = tx.Rollback()
return err
}
valueStrings := make([]string, len(newKeys))
values := make([]interface{}, len(newKeys)*4)
for i, key := range newKeys {
pos := i * 4
valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
values[pos] = user.jidPtr()
values[pos+1] = key.JID
values[pos+2] = key.Receiver
values[pos+3] = key.InCommunity
}
query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s",
strings.Join(valueStrings, ", "))
_, err = tx.Exec(query, values...)
if err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
func (user *User) IsInPortal(key PortalKey) bool {
row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver)
var exists bool
_ = row.Scan(&exists)
return exists
}
func (user *User) GetPortalKeys() []PortalKey {
rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr())
if err != nil {
user.log.Warnln("Failed to get user portal keys:", err)
return nil
}
var keys []PortalKey
for rows.Next() {
var key PortalKey
err = rows.Scan(&key.JID, &key.Receiver)
if err != nil {
user.log.Warnln("Failed to scan row:", err)
continue
}
keys = append(keys, key)
}
return keys
}
func (user *User) GetInCommunityMap() map[PortalKey]bool {
rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr())
if err != nil {
user.log.Warnln("Failed to get user portal keys:", err)
return nil
}
keys := make(map[PortalKey]bool)
for rows.Next() {
var key PortalKey
var inCommunity bool
err = rows.Scan(&key.JID, &key.Receiver, &inCommunity)
if err != nil {
user.log.Warnln("Failed to scan row:", err)
continue
}
keys[key] = inCommunity
}
return keys
}
func (user *User) CreateUserPortal(newKey PortalKeyWithMeta) {
user.log.Debugfln("Creating new portal %s for %s", newKey.PortalKey.JID, newKey.PortalKey.Receiver)
_, err := user.db.Exec(`INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES ($1, $2, $3, $4)`,
user.jidPtr(),
newKey.PortalKey.JID, newKey.PortalKey.Receiver,
newKey.InCommunity)
if err != nil {
user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
}
}
//type PortalKeyWithMeta struct {
// PortalKey
// InCommunity bool
//}
//
//func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error {
// tx, err := user.db.Begin()
// if err != nil {
// return err
// }
// _, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr())
// if err != nil {
// _ = tx.Rollback()
// return err
// }
// valueStrings := make([]string, len(newKeys))
// values := make([]interface{}, len(newKeys)*4)
// for i, key := range newKeys {
// pos := i * 4
// valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4)
// values[pos] = user.jidPtr()
// values[pos+1] = key.JID
// values[pos+2] = key.Receiver
// values[pos+3] = key.InCommunity
// }
// query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s",
// strings.Join(valueStrings, ", "))
// _, err = tx.Exec(query, values...)
// if err != nil {
// _ = tx.Rollback()
// return err
// }
// return tx.Commit()
//}
//
//func (user *User) IsInPortal(key PortalKey) bool {
// row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver)
// var exists bool
// _ = row.Scan(&exists)
// return exists
//}
//
//func (user *User) GetPortalKeys() []PortalKey {
// rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr())
// if err != nil {
// user.log.Warnln("Failed to get user portal keys:", err)
// return nil
// }
// var keys []PortalKey
// for rows.Next() {
// var key PortalKey
// err = rows.Scan(&key.JID, &key.Receiver)
// if err != nil {
// user.log.Warnln("Failed to scan row:", err)
// continue
// }
// keys = append(keys, key)
// }
// return keys
//}
//
//func (user *User) GetInCommunityMap() map[PortalKey]bool {
// rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr())
// if err != nil {
// user.log.Warnln("Failed to get user portal keys:", err)
// return nil
// }
// keys := make(map[PortalKey]bool)
// for rows.Next() {
// var key PortalKey
// var inCommunity bool
// err = rows.Scan(&key.JID, &key.Receiver, &inCommunity)
// if err != nil {
// user.log.Warnln("Failed to scan row:", err)
// continue
// }
// keys[key] = inCommunity
// }
// return keys
//}
//
//func (user *User) CreateUserPortal(newKey PortalKeyWithMeta) {
// user.log.Debugfln("Creating new portal %s for %s", newKey.PortalKey.JID, newKey.PortalKey.Receiver)
// _, err := user.db.Exec(`INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES ($1, $2, $3, $4)`,
// user.jidPtr(),
// newKey.PortalKey.JID, newKey.PortalKey.Receiver,
// newKey.InCommunity)
// if err != nil {
// user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
// }
//}

View file

@ -27,6 +27,7 @@ appservice:
# The database URI.
# SQLite: File name is enough. 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: mautrix-whatsapp.db
# Maximum number of connections. Mostly relevant for Postgres.
max_open_conns: 20
@ -63,9 +64,10 @@ metrics:
whatsapp:
# Device name that's shown in the "WhatsApp Web" section in the mobile app.
os_name: Mautrix-WhatsApp bridge
# Browser name that determines the logo shown in the mobile app. If the name is unrecognized, a generic icon is shown.
# Use the name of an actual browser (Chrome, Firefox, Safari, IE, Edge, Opera) if you want a specific icon.
browser_name: mx-wa
# Browser name that determines the logo shown in the mobile app.
# Must be "unknown" for a generic icon or a valid browser name if you want a specific icon.
# List of valid browser names: https://github.com/tulir/whatsmeow/blob/2a72655ef600a7fd7a2e98d53ec6da029759c4b8/binary/proto/def.proto#L1582-L1594
browser_name: unknown
# Bridge config
bridge:
@ -73,49 +75,19 @@ bridge:
# {{.}} is replaced with the phone number of the WhatsApp user.
username_template: whatsapp_{{.}}
# Displayname template for WhatsApp users.
# {{.Notify}} - nickname set by the WhatsApp user
# {{.VName}} - validated WhatsApp business name
# {{.JID}} - phone number (international format)
# {{.PushName}} - nickname set by the WhatsApp user
# {{.BusinessName}} - validated WhatsApp business name
# {{.Phone}} - phone number (international format)
# The following variables are also available, but will cause problems on multi-user instances:
# {{.Name}} - display name from contact list
# {{.Short}} - short display name from contact list
displayname_template: "{{if .Notify}}{{.Notify}}{{else if .VName}}{{.VName}}{{else}}{{.JID}}{{end}} (WA)"
# Localpart template for per-user room grouping community IDs.
# On startup, the bridge will try to create these communities, add all of the specific user's
# portals to the community, and invite the Matrix user to it.
# (Note that, by default, non-admins might not have your homeserver's permission to create
# communities.)
# {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user.
# whatsapp_{{.Localpart}}={{.Server}} is a good value that should work for any user.
community_template: null
# {{.FullName}} - full name from contact list
# {{.FirstName}} - first name from contact list
displayname_template: "{{if .PushName}}{{.PushName}}{{else if .BusinessName}}{{.BusinessName}}{{else}}{{.JID}}{{end}} (WA)"
# WhatsApp connection timeout in seconds.
connection_timeout: 20
# If WhatsApp doesn't respond within connection_timeout, should the bridge try to fetch the message
# to see if it was actually bridged? Use this if you have problems with sends timing out but actually
# succeeding.
fetch_message_on_timeout: false
# Whether or not the bridge should send a read receipt from the bridge bot when a message has been
# sent to WhatsApp. If fetch_message_on_timeout is enabled, a successful post-timeout fetch will
# trigger a read receipt too.
delivery_receipts: false
# Maximum number of times to retry connecting on connection error.
max_connection_attempts: 3
# Number of seconds to wait between connection attempts.
# Negative numbers are exponential backoff: -connection_retry_delay + 1 + 2^attempts
connection_retry_delay: -1
# Whether or not the bridge should send a notice to the user's management room when it retries connecting.
# If false, it will only report when it stops retrying.
report_connection_retry: true
# Whether or not the bridge should reconnect even if WhatsApp says another web client connected.
aggressive_reconnect: false
# Maximum number of seconds to wait for chats to be sent at startup.
# If this is too low and you have lots of chats, it could cause backfilling to fail.
chat_list_wait: 30
# Maximum number of seconds to wait to sync portals before force unlocking message processing.
# If this is too low and you have lots of chats, it could cause backfilling to fail.
portal_sync_wait: 600
user_message_buffer: 1024
portal_message_buffer: 128
# Whether or not to send call start/end notices to Matrix.
@ -123,32 +95,20 @@ bridge:
start: true
end: true
# Number of chats to sync for new users.
initial_chat_sync_count: 10
# Number of old messages to fill when creating new portal rooms.
initial_history_fill_count: 20
# Whether or not notifications should be turned off while filling initial history.
# Only applicable when using double puppeting.
initial_history_disable_notifications: false
# Maximum number of chats to sync when recovering from downtime.
# Set to -1 to sync all new chats during downtime.
recovery_chat_sync_limit: -1
# Whether or not to sync history when recovering from downtime.
recovery_history_backfill: true
# Whether or not portal info should be fetched from the server when syncing,
# instead of relying on finding any changes in the message history.
# If you get 599 errors often, you should try disabling this.
chat_meta_sync: true
history_sync:
# Whether to create portals from history sync payloads from WhatsApp.
create_portals: true
# Whether to enable backfilling history sync payloads from WhatsApp using batch sending
# This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
# It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml.
backfill: false
# Whether to use custom puppet for backfilling.
# In order to use this, the custom puppets must be in the appservice's user ID namespace.
double_puppet_backfill: false
# Whether or not puppet avatars should be fetched from the server even if an avatar is already set.
# If you get 599 errors often, you should try disabling this.
user_avatar_sync: true
# Whether or not Matrix users leaving groups should be bridged to WhatsApp
bridge_matrix_leave: true
# Maximum number of seconds since last message in chat to skip
# syncing the chat in any case. This setting will take priority
# over both recovery_chat_sync_limit and initial_chat_sync_count.
# Default is 3 days = 259200 seconds
sync_max_chat_age: 259200
# Whether or not to sync with custom puppets to receive EDUs that
# are not normally sent to appservices.
@ -169,20 +129,12 @@ bridge:
# manually.
login_shared_secret: null
# Whether or not to invite own WhatsApp user's Matrix puppet into private
# chat portals when backfilling if needed.
# This always uses the default puppet instead of custom puppets due to
# rate limits and timestamp massaging.
invite_own_puppet_for_backfilling: true
# Whether or not to explicitly set the avatar and room name for private
# chat portal rooms. This can be useful if the previous field works fine,
# but causes room avatar/name bugs.
# Whether to explicitly set the avatar and room name for private chat portal rooms.
private_chat_portal_meta: false
# Whether or not Matrix m.notice-type messages should be bridged.
# Whether Matrix m.notice-type messages should 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.
# 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
@ -246,7 +198,7 @@ bridge:
# Permissions for using the bridge.
# Permitted values:
# relaybot - Talk through the relaybot (if enabled), no access otherwise
# relay - Talk through the relaybot (if enabled), no access otherwise
# user - Access to use the bridge to chat with a WhatsApp account.
# admin - User level and some additional administration tools
# Permitted keys:
@ -254,19 +206,16 @@ bridge:
# domain - All users on that homeserver
# mxid - Specific user
permissions:
"*": relaybot
"*": relay
"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 }}"

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -22,7 +22,7 @@ import (
"regexp"
"strings"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
@ -57,32 +57,22 @@ func NewFormatter(bridge *Bridge) *Formatter {
if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
if puppet != nil {
jids, ok := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
jids, ok := ctx[mentionedJIDsContextKey].([]string)
if !ok {
ctx[mentionedJIDsContextKey] = []whatsapp.JID{puppet.JID}
ctx[mentionedJIDsContextKey] = []string{puppet.JID.String()}
} else {
ctx[mentionedJIDsContextKey] = append(jids, puppet.JID)
ctx[mentionedJIDsContextKey] = append(jids, puppet.JID.String())
}
return "@" + puppet.PhoneNumber()
return "@" + puppet.JID.User
}
}
return mxid
},
BoldConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("*%s*", text)
},
ItalicConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("_%s_", text)
},
StrikethroughConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("~%s~", text)
},
MonospaceConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("```%s```", text)
},
MonospaceBlockConverter: func(text, language string, _ format.Context) string {
return fmt.Sprintf("```%s```", text)
},
BoldConverter: func(text string, _ format.Context) string { return fmt.Sprintf("*%s*", text) },
ItalicConverter: func(text string, _ format.Context) string { return fmt.Sprintf("_%s_", text) },
StrikethroughConverter: func(text string, _ format.Context) string { return fmt.Sprintf("~%s~", text) },
MonospaceConverter: func(text string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
MonospaceBlockConverter: func(text, language string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
},
waReplString: map[*regexp.Regexp]string{
italicRegex: "$1<em>$2</em>$3",
@ -99,12 +89,11 @@ func NewFormatter(bridge *Bridge) *Formatter {
return fmt.Sprintf("<code>%s</code>", str)
},
}
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{
}
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{}
return formatter
}
func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID, displayname string) {
func (formatter *Formatter) getMatrixInfoByJID(jid types.JID) (mxid id.UserID, displayname string) {
if user := formatter.bridge.GetUserByJID(jid); user != nil {
mxid = user.MXID
displayname = string(user.MXID)
@ -115,7 +104,7 @@ func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID
return
}
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []whatsapp.JID) {
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []string) {
output := html.EscapeString(content.Body)
for regex, replacement := range formatter.waReplString {
output = regex.ReplaceAllString(output, replacement)
@ -123,14 +112,20 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
for regex, replacer := range formatter.waReplFunc {
output = regex.ReplaceAllStringFunc(output, replacer)
}
for _, jid := range mentionedJIDs {
for _, rawJID := range mentionedJIDs {
jid, err := types.ParseJID(rawJID)
if err != nil {
continue
} else if jid.Server == types.LegacyUserServer {
jid.Server = types.DefaultUserServer
}
mxid, displayname := formatter.getMatrixInfoByJID(jid)
number := "@" + strings.Replace(jid, whatsapp.NewUserSuffix, "", 1)
output = strings.Replace(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname), -1)
content.Body = strings.Replace(content.Body, number, displayname, -1)
number := "@" + jid.User
output = strings.ReplaceAll(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname))
content.Body = strings.ReplaceAll(content.Body, number, displayname)
}
if output != content.Body {
output = strings.Replace(output, "\n", "<br/>", -1)
output = strings.ReplaceAll(output, "\n", "<br/>")
content.FormattedBody = output
content.Format = event.FormatHTML
for regex, replacer := range formatter.waReplFuncText {
@ -139,9 +134,9 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
}
}
func (formatter *Formatter) ParseMatrix(html string) (string, []whatsapp.JID) {
func (formatter *Formatter) ParseMatrix(html string) (string, []string) {
ctx := make(format.Context)
result := formatter.matrixHTMLParser.Parse(html, ctx)
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]string)
return result, mentionedJIDs
}

35
go.mod
View file

@ -1,19 +1,40 @@
module maunium.net/go/mautrix-whatsapp
go 1.14
go 1.17
require (
github.com/Rhymen/go-whatsapp v0.1.0
github.com/gorilla/websocket v1.4.2
github.com/lib/pq v1.10.2
github.com/mattn/go-sqlite3 v1.14.8
github.com/lib/pq v1.10.3
github.com/mattn/go-sqlite3 v1.14.9
github.com/prometheus/client_golang v1.11.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
google.golang.org/protobuf v1.27.1
gopkg.in/yaml.v2 v2.4.0
maunium.net/go/mauflag v1.0.0
maunium.net/go/maulogger/v2 v2.3.0
maunium.net/go/mautrix v0.9.27
maunium.net/go/maulogger/v2 v2.3.1
maunium.net/go/mautrix v0.9.31
)
replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.5.12
require (
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcutil v1.0.2 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/gjson v1.10.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.3 // indirect
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
)

69
go.sum
View file

@ -1,4 +1,6 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -44,9 +46,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@ -76,12 +77,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -128,26 +127,27 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
github.com/tulir/go-whatsapp v0.5.12 h1:JGU5yhoh+CyDcSMUilwy7FL0gFo0zqqepsHRqEjrjKc=
github.com/tulir/go-whatsapp v0.5.12/go.mod h1:7J3IIL3bEQiBJGtiZst1N4PgXHlWIartdVQLe6lcx9A=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
github.com/tidwall/gjson v1.10.2/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.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c h1:ZmmT3L8pMKLW3JhcP6Rt0dJg09N+20a8fROxr8MUKzg=
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -157,9 +157,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -172,16 +172,16 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -198,8 +198,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
@ -217,8 +217,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/maulogger/v2 v2.3.0 h1:TMCcO65fLk6+pJXo7sl38tzjzW0KBFgc6JWJMBJp4GE=
maunium.net/go/maulogger/v2 v2.3.0/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is=
maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=
maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.9.31 h1:n7UF5tqq2zCyfdNsv++RyQ2anjjrFVOmOA2VkZCSgZc=
maunium.net/go/mautrix v0.9.31/go.mod h1:3U7pOAx4bxdIVJuunLDAToI+M7YwxcGMm74zBmX5aY0=

92
main.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -21,12 +21,18 @@ import (
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/Rhymen/go-whatsapp"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
flag "maunium.net/go/mauflag"
log "maunium.net/go/maulogger/v2"
@ -41,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.9"
// 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.2.0+dev"
// 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:]
@ -145,19 +159,19 @@ type Bridge struct {
Provisioning *ProvisioningAPI
Bot *appservice.IntentAPI
Formatter *Formatter
Relaybot *User
Crypto Crypto
Metrics *MetricsHandler
WAContainer *sqlstore.Container
usersByMXID map[id.UserID]*User
usersByJID map[whatsapp.JID]*User
usersByUsername map[string]*User
usersLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
portalsByMXID map[id.RoomID]*Portal
portalsByJID map[database.PortalKey]*Portal
portalsLock sync.Mutex
puppets map[whatsapp.JID]*Puppet
puppets map[types.JID]*Puppet
puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex
}
@ -176,11 +190,11 @@ type Crypto interface {
func NewBridge() *Bridge {
bridge := &Bridge{
usersByMXID: make(map[id.UserID]*User),
usersByJID: make(map[whatsapp.JID]*User),
usersByUsername: make(map[string]*User),
managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[id.RoomID]*Portal),
portalsByJID: make(map[database.PortalKey]*Portal),
puppets: make(map[whatsapp.JID]*Puppet),
puppets: make(map[types.JID]*Puppet),
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
}
@ -259,6 +273,8 @@ func (bridge *Bridge) Init() {
bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
bridge.WAContainer = sqlstore.NewWithDB(bridge.DB.DB, bridge.Config.AppService.Database.Type, nil)
ss := bridge.Config.AppService.Provisioning.SharedSecret
if len(ss) > 0 && ss != "disable" {
bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
@ -271,6 +287,23 @@ func (bridge *Bridge) Init() {
bridge.Formatter = NewFormatter(bridge)
bridge.Crypto = NewCryptoHelper(bridge)
bridge.Metrics = NewMetricsHandler(bridge.Config.Metrics.Listen, bridge.Log.Sub("Metrics"), bridge.DB)
store.BaseClientPayload.UserAgent.OsVersion = proto.String(WAVersion)
store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(WAVersion)
store.CompanionProps.Os = proto.String(bridge.Config.WhatsApp.OSName)
versionParts := strings.Split(WAVersion, ".")
if len(versionParts) > 2 {
primary, _ := strconv.Atoi(versionParts[0])
secondary, _ := strconv.Atoi(versionParts[1])
tertiary, _ := strconv.Atoi(versionParts[2])
store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
}
platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(bridge.Config.WhatsApp.BrowserName)]
if ok {
store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
}
}
func (bridge *Bridge) Start() {
@ -289,12 +322,10 @@ func (bridge *Bridge) Start() {
os.Exit(19)
}
}
bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateStarting}.fill(nil))
if bridge.Provisioning != nil {
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")
@ -327,21 +358,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(false)
}
func (bridge *Bridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot
@ -374,10 +390,10 @@ func (bridge *Bridge) StartUsers() {
bridge.Log.Debugln("Starting users")
foundAnySessions := false
for _, user := range bridge.GetAllUsers() {
if user.Session != nil {
if !user.JID.IsEmpty() {
foundAnySessions = true
}
go user.Connect(false)
go user.Connect()
}
if !foundAnySessions {
bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))
@ -401,15 +417,13 @@ func (bridge *Bridge) Stop() {
bridge.AS.Stop()
bridge.Metrics.Stop()
bridge.EventProcessor.Stop()
for _, user := range bridge.usersByJID {
if user.Conn == nil {
for _, user := range bridge.usersByUsername {
if user.Client == nil {
continue
}
bridge.Log.Debugln("Disconnecting", user.MXID)
err := user.Conn.Disconnect()
if err != nil {
bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
}
user.Client.Disconnect()
close(user.historySyncs)
}
}

View file

@ -121,29 +121,9 @@ 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 {
continue
} else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok {
hasPuppets = true
continue
}
mx.log.Debugln("Leaving multi-user room", evt.RoomID, "after accepting invite from", evt.Sender)
_, _ = intent.SendNotice(evt.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
_, _ = intent.LeaveRoom(evt.RoomID)
return
}
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.Welcome)
if !hasPuppets && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
if len(members.Joined) == 2 && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
_, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender)
@ -223,13 +203,10 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
portal.UpdateBridgeInfo()
_, _ = intent.SendNotice(roomID, "Private chat portal created")
err := portal.FillInitialHistory(inviter)
if err != nil {
portal.log.Errorln("Failed to fill history:", err)
}
inviter.addPortalToCommunity(portal)
inviter.addPuppetToCommunity(puppet)
//err := portal.FillInitialHistory(inviter)
//if err != nil {
// portal.log.Errorln("Failed to fill history:", err)
//}
}
func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
@ -281,7 +258,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsConnected() {
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
@ -322,7 +299,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsConnected() {
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
@ -343,7 +320,7 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
return true
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if !user.RelaybotWhitelisted {
if !user.RelayWhitelisted {
return true
}
return false
@ -461,7 +438,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
if !user.HasSession() {
return
} else if !user.IsConnected() {
} else if !user.IsLoggedIn() {
msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+
"You are not connected to WhatsApp, so your redaction was not bridged. "+
"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -28,7 +28,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -59,16 +59,12 @@ type MetricsHandler struct {
unencryptedGroupCount prometheus.Gauge
unencryptedPrivateCount prometheus.Gauge
connected prometheus.Gauge
connectedState map[whatsapp.JID]bool
connectedStateLock sync.Mutex
loggedIn prometheus.Gauge
loggedInState map[whatsapp.JID]bool
loggedInStateLock sync.Mutex
syncLocked prometheus.Gauge
syncLockedState map[whatsapp.JID]bool
syncLockedStateLock sync.Mutex
bufferLength *prometheus.GaugeVec
connected prometheus.Gauge
connectedState map[string]bool
connectedStateLock sync.Mutex
loggedIn prometheus.Gauge
loggedInState map[string]bool
loggedInStateLock sync.Mutex
}
func NewMetricsHandler(address string, log log.Logger, db *database.Database) *MetricsHandler {
@ -125,21 +121,12 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
Name: "bridge_logged_in",
Help: "Users logged into the bridge",
}),
loggedInState: make(map[whatsapp.JID]bool),
loggedInState: make(map[string]bool),
connected: promauto.NewGauge(prometheus.GaugeOpts{
Name: "bridge_connected",
Help: "Bridge users connected to WhatsApp",
}),
connectedState: make(map[whatsapp.JID]bool),
syncLocked: promauto.NewGauge(prometheus.GaugeOpts{
Name: "bridge_sync_locked",
Help: "Bridge users locked in post-login sync",
}),
syncLockedState: make(map[whatsapp.JID]bool),
bufferLength: promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "bridge_buffer_size",
Help: "Number of messages in buffer",
}, []string{"user_id"}),
connectedState: make(map[string]bool),
}
}
@ -158,7 +145,7 @@ func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
}
}
func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType string) func() {
func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp time.Time, messageType string) func() {
if !mh.running {
return noop
}
@ -169,7 +156,7 @@ func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType str
mh.whatsappMessageHandling.
With(prometheus.Labels{"message_type": messageType}).
Observe(duration.Seconds())
mh.whatsappMessageAge.Observe(time.Now().Sub(time.Unix(int64(timestamp), 0)).Seconds())
mh.whatsappMessageAge.Observe(time.Now().Sub(timestamp).Seconds())
}
}
@ -180,15 +167,15 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
}
func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
func (mh *MetricsHandler) TrackLoginState(jid types.JID, loggedIn bool) {
if !mh.running {
return
}
mh.loggedInStateLock.Lock()
defer mh.loggedInStateLock.Unlock()
currentVal, ok := mh.loggedInState[jid]
currentVal, ok := mh.loggedInState[jid.User]
if !ok || currentVal != loggedIn {
mh.loggedInState[jid] = loggedIn
mh.loggedInState[jid.User] = loggedIn
if loggedIn {
mh.loggedIn.Inc()
} else {
@ -197,16 +184,15 @@ func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
}
}
func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool) {
func (mh *MetricsHandler) TrackConnectionState(jid types.JID, connected bool) {
if !mh.running {
return
}
mh.connectedStateLock.Lock()
defer mh.connectedStateLock.Unlock()
currentVal, ok := mh.connectedState[jid]
currentVal, ok := mh.connectedState[jid.User]
if !ok || currentVal != connected {
mh.connectedState[jid] = connected
mh.connectedState[jid.User] = connected
if connected {
mh.connected.Inc()
} else {
@ -215,30 +201,6 @@ func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool)
}
}
func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
if !mh.running {
return
}
mh.syncLockedStateLock.Lock()
defer mh.syncLockedStateLock.Unlock()
currentVal, ok := mh.syncLockedState[jid]
if !ok || currentVal != locked {
mh.syncLockedState[jid] = locked
if locked {
mh.syncLocked.Inc()
} else {
mh.syncLocked.Dec()
}
}
}
func (mh *MetricsHandler) TrackBufferLength(id id.UserID, length int) {
if !mh.running {
return
}
mh.bufferLength.With(prometheus.Labels{"user_id": string(id)}).Set(float64(length))
}
func (mh *MetricsHandler) updateStats() {
start := time.Now()
var puppetCount int

View file

@ -1,3 +1,4 @@
//go:build !cgo || nocrypto
// +build !cgo nocrypto
package main

2121
portal.go

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -29,7 +29,7 @@ import (
"github.com/gorilla/websocket"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow"
log "maunium.net/go/maulogger/v2"
@ -50,11 +50,13 @@ func (prov *ProvisioningAPI) Init() {
r.HandleFunc("/login", prov.Login).Methods(http.MethodGet)
r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost)
r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost)
r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost)
r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost)
r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost)
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost)
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost)
// Deprecated, just use /disconnect
r.HandleFunc("/delete_connection", prov.Disconnect).Methods(http.MethodPost)
}
type responseWrap struct {
@ -122,7 +124,7 @@ type Response struct {
func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user.Session == nil && user.Conn == nil {
if user.Session == nil && user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "Nothing to purge: no session information stored and no active connection.",
ErrCode: "no session",
@ -130,128 +132,43 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
return
}
user.DeleteConnection()
user.SetSession(nil)
user.DeleteSession()
jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
}
func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user.Conn == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You don't have a WhatsApp connection.",
ErrCode: "not connected",
})
return
}
user.DeleteConnection()
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"})
user.removeFromJIDMap(StateLoggedOut)
}
func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user.Conn == nil {
if user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You don't have a WhatsApp connection.",
ErrCode: "no connection",
})
return
}
err := user.Conn.Disconnect()
if err == whatsapp.ErrNotConnected {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You were not connected",
ErrCode: "not connected",
})
return
} else if err != nil {
user.log.Warnln("Error while disconnecting:", err)
jsonResponse(w, http.StatusInternalServerError, Error{
Error: fmt.Sprintf("Unknown error while disconnecting: %v", err),
ErrCode: err.Error(),
})
return
}
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.DeleteConnection()
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
}
func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user.Conn == nil {
if user.Client == nil {
if user.Session == nil {
jsonResponse(w, http.StatusForbidden, Error{
Error: "No existing connection and no session. Please log in first.",
ErrCode: "no session",
})
} else {
user.Connect(false)
jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."})
user.Connect()
jsonResponse(w, http.StatusAccepted, Response{true, "Created connection to WhatsApp."})
}
return
}
user.log.Debugln("Received /reconnect request, disconnecting")
wasConnected := true
err := user.Conn.Disconnect()
if err == whatsapp.ErrNotConnected {
wasConnected = false
} else if err != nil {
user.log.Warnln("Error while disconnecting:", err)
}
user.log.Debugln("Restoring session for /reconnect")
err = user.Conn.Restore(true, r.Context())
user.log.Debugfln("Restore session for /reconnect responded with %v", err)
if err == whatsapp.ErrInvalidSession {
if user.Session != nil {
user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
user.Conn.SetSession(*user.Session)
err = user.Conn.Restore(true, r.Context())
} else {
jsonResponse(w, http.StatusForbidden, Error{
Error: "You're not logged in",
ErrCode: "not logged in",
})
return
}
}
if err == whatsapp.ErrLoginInProgress {
jsonResponse(w, http.StatusConflict, Error{
Error: "A login or reconnection is already in progress.",
ErrCode: "login in progress",
})
return
} else if err == whatsapp.ErrAlreadyLoggedIn {
jsonResponse(w, http.StatusConflict, Error{
Error: "You were already connected.",
ErrCode: err.Error(),
})
return
}
if err != nil {
user.log.Warnln("Error while reconnecting:", err)
jsonResponse(w, http.StatusInternalServerError, Error{
Error: fmt.Sprintf("Unknown error while reconnecting: %v", err),
ErrCode: err.Error(),
})
user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
err = user.Conn.Disconnect()
if err != nil {
user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
}
return
}
user.ConnectionErrors = 0
user.PostLogin()
var msg string
if wasConnected {
msg = "Reconnected successfully."
} else {
msg = "Connected successfully."
user.DeleteConnection()
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WANotConnected})
user.Connect()
jsonResponse(w, http.StatusAccepted, Response{true, "Restarted connection to WhatsApp"})
}
jsonResponse(w, http.StatusOK, Response{true, msg})
}
func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
@ -259,39 +176,23 @@ func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
wa := map[string]interface{}{
"has_session": user.Session != nil,
"management_room": user.ManagementRoom,
"jid": user.JID,
"conn": nil,
"ping": nil,
}
if user.Conn != nil {
if user.JID.IsEmpty() {
wa["jid"] = user.JID.String()
}
if user.Client != nil {
wa["conn"] = map[string]interface{}{
"is_connected": user.Conn.IsConnected(),
"is_logged_in": user.Conn.IsLoggedIn(),
"is_login_in_progress": user.Conn.IsLoginInProgress(),
"is_connected": user.Client.IsConnected(),
"is_logged_in": user.Client.IsLoggedIn,
}
user.log.Debugln("Pinging WhatsApp mobile due to /ping API request")
err := user.Conn.AdminTest()
var errStr string
if err == whatsapp.ErrPingFalse {
user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
go user.HandleError(err)
}
if err != nil {
errStr = err.Error()
}
wa["ping"] = map[string]interface{}{
"ok": err == nil,
"err": errStr,
}
user.log.Debugfln("Admin test response for /ping: %v (conn: %t, login: %t, in progress: %t)",
err, user.Conn.IsConnected(), user.Conn.IsLoggedIn(), user.Conn.IsLoginInProgress())
}
resp := map[string]interface{}{
"mxid": user.MXID,
"admin": user.Admin,
"whitelisted": user.Whitelisted,
"relaybot_whitelisted": user.RelaybotWhitelisted,
"whatsapp": wa,
"mxid": user.MXID,
"admin": user.Admin,
"whitelisted": user.Whitelisted,
"relay_whitelisted": user.RelayWhitelisted,
"whatsapp": wa,
}
jsonResponse(w, http.StatusOK, resp)
}
@ -314,7 +215,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
force := strings.ToLower(r.URL.Query().Get("force")) != "false"
if user.Conn == nil {
if user.Client == nil {
if !force {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You're not connected",
@ -322,7 +223,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
})
}
} else {
err := user.Conn.Logout()
err := user.Client.Logout()
if err != nil {
user.log.Warnln("Error while logging out:", err)
if !force {
@ -332,16 +233,15 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
})
return
}
} else {
user.Session = nil
}
user.DeleteConnection()
}
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.removeFromJIDMap(StateLoggedOut)
// TODO this causes a foreign key violation, which should be fixed
//ce.User.JID = ""
user.SetSession(nil)
user.DeleteSession()
jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
}
@ -361,26 +261,10 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
prov.log.Errorln("Failed to upgrade connection to websocket:", err)
return
}
defer c.Close()
if !user.Connect(true) {
user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
_ = c.WriteJSON(Error{
Error: "Failed to connect to WhatsApp",
ErrCode: "connection error",
})
return
}
qrChan := make(chan string, 3)
go func() {
for code := range qrChan {
if code == "stop" {
return
}
_ = c.WriteJSON(map[string]interface{}{
"code": code,
})
defer func() {
err := c.Close()
if err != nil {
user.log.Debugln("Error closing websocket:", err)
}
}()
@ -400,40 +284,63 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
return nil
})
user.log.Debugln("Starting login via provisioning API")
session, jid, err := user.Conn.Login(qrChan, ctx)
qrChan <- "stop"
qrChan, err := user.Login(ctx)
if err != nil {
var msg string
if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
msg = "You're already logged in"
} else if errors.Is(err, whatsapp.ErrLoginInProgress) {
msg = "You have a login in progress already."
} else if errors.Is(err, whatsapp.ErrLoginTimedOut) {
msg = "QR code scan timed out. Please try again."
} else if errors.Is(err, whatsapp.ErrInvalidWebsocket) {
msg = "WhatsApp connection error. Please try again."
// TODO might need to make sure it reconnects?
} else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
msg = "WhatsApp multi-device is not currently supported. Please disable it and try again."
user.log.Errorf("Failed to log in from provisioning API:", err)
if errors.Is(err, ErrAlreadyLoggedIn) {
go user.Connect()
_ = c.WriteJSON(Error{
Error: "You're already logged into WhatsApp",
ErrCode: "already logged in",
})
} else {
msg = fmt.Sprintf("Unknown error while logging in: %v", err)
_ = c.WriteJSON(Error{
Error: "Failed to connect to WhatsApp",
ErrCode: "connection error",
})
}
}
user.log.Debugln("Started login via provisioning API")
for {
select {
case evt := <-qrChan:
switch evt {
case whatsmeow.QRChannelSuccess:
jid := user.Client.Store.ID
user.log.Debugln("Successful login as", jid, "via provisioning API")
_ = c.WriteJSON(map[string]interface{}{
"success": true,
"jid": jid,
"phone": fmt.Sprintf("+%s", jid.User),
})
case whatsmeow.QRChannelTimeout:
user.log.Debugln("Login via provisioning API timed out")
_ = c.WriteJSON(Error{
Error: "QR code scan timed out. Please try again.",
ErrCode: "login timed out",
})
case whatsmeow.QRChannelErrUnexpectedEvent:
user.log.Debugln("Login via provisioning API failed due to unexpected event")
_ = c.WriteJSON(Error{
Error: "Got unexpected event while waiting for QRs, perhaps you're already logged in?",
ErrCode: "unexpected event",
})
case whatsmeow.QRChannelScannedWithoutMultidevice:
_ = c.WriteJSON(Error{
Error: "Please enable the WhatsApp multidevice beta and scan the QR code again.",
ErrCode: "multidevice not enabled",
})
continue
default:
_ = c.WriteJSON(map[string]interface{}{
"code": string(evt),
})
continue
}
return
case <-ctx.Done():
return
}
user.log.Warnln("Failed to log in:", err)
_ = c.WriteJSON(Error{
Error: msg,
ErrCode: err.Error(),
})
return
}
user.log.Debugln("Successful login as", jid, "via provisioning API")
user.ConnectionErrors = 0
user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
user.addToJIDMap()
user.SetSession(&session)
_ = c.WriteJSON(map[string]interface{}{
"success": true,
"jid": user.JID,
})
user.PostLogin()
}

156
puppet.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2020 Tulir Asokan
// Copyright (C) 2021 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
@ -17,15 +17,19 @@
package main
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/Rhymen/go-whatsapp"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
@ -34,19 +38,18 @@ import (
var userIDRegex *regexp.Regexp
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (whatsapp.JID, bool) {
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (jid types.JID, ok bool) {
if userIDRegex == nil {
userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
bridge.Config.Bridge.FormatUsername("([0-9]+)"),
bridge.Config.Homeserver.Domain))
}
match := userIDRegex.FindStringSubmatch(string(mxid))
if match == nil || len(match) != 2 {
return "", false
if len(match) == 2 {
jid = types.NewJID(match[1], types.DefaultUserServer)
ok = true
}
jid := whatsapp.JID(match[1] + whatsapp.NewUserSuffix)
return jid, true
return
}
func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
@ -58,7 +61,13 @@ func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
return bridge.GetPuppetByJID(jid)
}
func (bridge *Bridge) GetPuppetByJID(jid whatsapp.JID) *Puppet {
func (bridge *Bridge) GetPuppetByJID(jid types.JID) *Puppet {
jid = jid.ToNonAD()
if jid.Server == types.LegacyUserServer {
jid.Server = types.DefaultUserServer
} else if jid.Server != types.DefaultUserServer {
return nil
}
bridge.puppetsLock.Lock()
defer bridge.puppetsLock.Unlock()
puppet, ok := bridge.puppets[jid]
@ -123,12 +132,9 @@ func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet
return output
}
func (bridge *Bridge) FormatPuppetMXID(jid whatsapp.JID) id.UserID {
func (bridge *Bridge) FormatPuppetMXID(jid types.JID) id.UserID {
return id.NewUserID(
bridge.Config.Bridge.FormatUsername(
strings.Replace(
jid,
whatsapp.NewUserSuffix, "", 1)),
bridge.Config.Bridge.FormatUsername(jid.User),
bridge.Config.Homeserver.Domain)
}
@ -149,7 +155,7 @@ type Puppet struct {
log log.Logger
typingIn id.RoomID
typingAt int64
typingAt time.Time
MXID id.UserID
@ -160,14 +166,8 @@ type Puppet struct {
syncLock sync.Mutex
}
func (puppet *Puppet) PhoneNumber() string {
return strings.Replace(puppet.JID, whatsapp.NewUserSuffix, "", 1)
}
func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
if (!portal.IsPrivateChat() && puppet.customIntent == nil) ||
(portal.backfilling && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling) ||
portal.Key.JID == puppet.JID {
if (!portal.IsPrivateChat() && puppet.customIntent == nil) || portal.Key.JID == puppet.JID {
return puppet.DefaultIntent()
}
return puppet.customIntent
@ -181,63 +181,64 @@ func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
return puppet.bridge.AS.Intent(puppet.MXID)
}
func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsapp.ProfilePicInfo) bool {
if avatar == nil {
var err error
avatar, err = source.Conn.GetProfilePicThumb(puppet.JID)
if err != nil {
puppet.log.Warnln("Failed to get avatar:", err)
return false
}
}
if avatar.Status == 404 {
avatar.Tag = "remove"
avatar.Status = 0
} else if avatar.Status == 401 && puppet.Avatar != "unauthorized" {
puppet.Avatar = "unauthorized"
return true
}
if avatar.Status != 0 || avatar.Tag == puppet.Avatar {
return false
}
if avatar.Tag == "remove" || len(avatar.URL) == 0 {
err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{})
if err != nil {
puppet.log.Warnln("Failed to remove avatar:", err)
}
puppet.AvatarURL = id.ContentURI{}
puppet.Avatar = avatar.Tag
go puppet.updatePortalAvatar()
return true
}
data, err := avatar.DownloadBytes()
func reuploadAvatar(intent *appservice.IntentAPI, url string) (id.ContentURI, error) {
getResp, err := http.DefaultClient.Get(url)
if err != nil {
puppet.log.Warnln("Failed to download avatar:", err)
return false
return id.ContentURI{}, fmt.Errorf("failed to download avatar: %w", err)
}
data, err := io.ReadAll(getResp.Body)
_ = getResp.Body.Close()
if err != nil {
return id.ContentURI{}, fmt.Errorf("failed to read avatar bytes: %w", err)
}
mime := http.DetectContentType(data)
resp, err := puppet.DefaultIntent().UploadBytes(data, mime)
resp, err := intent.UploadBytes(data, mime)
if err != nil {
puppet.log.Warnln("Failed to upload avatar:", err)
return id.ContentURI{}, fmt.Errorf("failed to upload avatar to Matrix: %w", err)
}
return resp.ContentURI, nil
}
func (puppet *Puppet) UpdateAvatar(source *User) bool {
avatar, err := source.Client.GetProfilePictureInfo(puppet.JID, false)
if err != nil {
if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
puppet.log.Warnln("Failed to get avatar URL:", err)
}
return false
} else if avatar == nil {
if puppet.Avatar == "remove" {
return false
}
puppet.AvatarURL = id.ContentURI{}
avatar = &types.ProfilePictureInfo{ID: "remove"}
} else if avatar.ID == puppet.Avatar {
return false
} else if len(avatar.URL) == 0 {
puppet.log.Warnln("Didn't get URL in response to avatar query")
return false
} else {
url, err := reuploadAvatar(puppet.DefaultIntent(), avatar.URL)
if err != nil {
puppet.log.Warnln("Failed to reupload avatar:", err)
return false
}
puppet.AvatarURL = url
}
puppet.AvatarURL = resp.ContentURI
err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
if err != nil {
puppet.log.Warnln("Failed to set avatar:", err)
}
puppet.Avatar = avatar.Tag
puppet.Avatar = avatar.ID
go puppet.updatePortalAvatar()
return true
}
func (puppet *Puppet) UpdateName(source *User, contact whatsapp.Contact) bool {
newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
func (puppet *Puppet) UpdateName(source *User, contact types.ContactInfo) bool {
newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(puppet.JID, contact)
if puppet.Displayname != newName && quality >= puppet.NameQuality {
err := puppet.DefaultIntent().SetDisplayName(newName)
if err == nil {
@ -288,25 +289,21 @@ func (puppet *Puppet) updatePortalName() {
})
}
func (puppet *Puppet) SyncContactIfNecessary(source *User) {
if len(puppet.Displayname) > 0 {
func (puppet *Puppet) SyncContact(source *User, onlyIfNoName bool) {
if onlyIfNoName && len(puppet.Displayname) > 0 {
return
}
source.Conn.Store.ContactsLock.RLock()
contact, ok := source.Conn.Store.Contacts[puppet.JID]
source.Conn.Store.ContactsLock.RUnlock()
if !ok {
puppet.log.Warnfln("No contact info found through %s in SyncContactIfNecessary", source.MXID)
contact.JID = puppet.JID
// Sync anyway to set a phone number name
} else {
puppet.log.Debugfln("Syncing contact info through %s / %s because puppet has no displayname", source.MXID, source.JID)
contact, err := source.Client.Store.Contacts.GetContact(puppet.JID)
if err != nil {
puppet.log.Warnfln("Failed to get contact info through %s in SyncContact: %v", source.MXID)
} else if !contact.Found {
puppet.log.Warnfln("No contact info found through %s in SyncContact", source.MXID)
}
puppet.Sync(source, contact)
}
func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
func (puppet *Puppet) Sync(source *User, contact types.ContactInfo) {
puppet.syncLock.Lock()
defer puppet.syncLock.Unlock()
err := puppet.DefaultIntent().EnsureRegistered()
@ -314,15 +311,14 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
puppet.log.Errorln("Failed to ensure registered:", err)
}
if contact.JID == source.JID {
contact.Notify = source.pushName
if puppet.JID.User == source.JID.User {
contact.PushName = source.Client.Store.PushName
}
update := false
update = puppet.UpdateName(source, contact) || update
// TODO figure out how to update avatars after being offline
if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync {
update = puppet.UpdateAvatar(source, nil) || update
update = puppet.UpdateAvatar(source) || update
}
if update {
puppet.Update()

1353
user.go

File diff suppressed because it is too large Load diff