Initial switch to go.mau.fi/whatsmeow

This commit is contained in:
Tulir Asokan 2021-10-22 20:14:34 +03:00
parent 923f9c4b21
commit 56850bb698
25 changed files with 2257 additions and 3147 deletions

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.
### 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

@ -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"
@ -94,8 +89,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 = user.JID.String()
pong.RemoteName = fmt.Sprintf("+%s", user.JID.User)
}
pong.Timestamp = time.Now().Unix()
@ -116,32 +111,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 +179,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 +188,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.Client != nil && user.Client.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,12 +20,14 @@ import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/skip2/go-qrcode"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"maunium.net/go/maulogger/v2"
@ -34,8 +36,6 @@ import (
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
)
type CommandHandler struct {
@ -119,8 +119,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":
@ -141,7 +139,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
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
}
@ -149,8 +147,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
switch ce.Command {
case "login-matrix":
handler.CommandLoginMatrix(ce)
case "sync":
handler.CommandSync(ce)
case "list":
handler.CommandList(ce)
case "open":
@ -226,12 +222,13 @@ 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)
// 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 +243,27 @@ 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)
}
// 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 +297,44 @@ 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})
// 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.`
@ -382,11 +381,108 @@ const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
// CommandLogin handles login command
func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
if ce.User.Session != nil {
ce.Reply("You're already logged in")
return
}
qrChan := make(chan *events.QR, 1)
loginChan := make(chan *events.PairSuccess, 1)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go ce.User.loginQrChannel(ctx, ce, qrChan, cancel)
ce.User.qrListener = qrChan
ce.User.loginListener = loginChan
if !ce.User.Connect(true) {
ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
return
}
ce.User.Login(ce)
select {
case success := <-loginChan:
ce.Reply("Successfully logged in as +%s", success.ID.User)
cancel()
case <-ctx.Done():
ce.Reply("Login timed out")
}
}
func (user *User) loginQrChannel(ctx context.Context, ce *CommandEvent, qrChan <-chan *events.QR, cancel func()) {
var qrEvt *events.QR
select {
case qrEvt = <-qrChan:
case <-ctx.Done():
return
}
bot := user.bridge.AS.BotClient()
code := qrEvt.Codes[0]
qrEvt.Codes = qrEvt.Codes[1:]
url, ok := user.uploadQR(ce, code)
if !ok {
return
}
sendResp, err := bot.SendImage(ce.RoomID, code, url)
if err != nil {
user.log.Errorln("Failed to send QR code to user:", err)
return
}
qrEventID := sendResp.EventID
for {
select {
case <-time.After(qrEvt.Timeout):
if len(qrEvt.Codes) == 0 {
cancel()
return
}
code, qrEvt.Codes = qrEvt.Codes[0], qrEvt.Codes[1:]
url, ok = user.uploadQR(ce, code)
if !ok {
continue
}
_, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{
MsgType: event.MsgImage,
Body: code,
URL: url.CUString(),
NewContent: &event.MessageEventContent{
MsgType: event.MsgImage,
Body: code,
URL: url.CUString(),
},
RelatesTo: &event.RelatesTo{
Type: event.RelReplace,
EventID: qrEventID,
},
})
if err != nil {
user.log.Errorln("Failed to send edited QR code to user:", err)
}
case <-ctx.Done():
return
}
}
}
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 - Logout from WhatsApp`
@ -396,7 +492,7 @@ 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 +503,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()
if err != nil {
ce.User.log.Warnln("Error while logging out:", err)
ce.Reply("Unknown error while logging out: %v", err)
return
}
// TODO reimplement
//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.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.")
}
@ -438,21 +533,21 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
return
}
if ce.Args[0] == "presence" || ce.Args[0] == "all" {
customPuppet.EnablePresence = !customPuppet.EnablePresence
var newPresence whatsapp.Presence
if customPuppet.EnablePresence {
newPresence = whatsapp.PresenceAvailable
ce.Reply("Enabled presence bridging")
} else {
newPresence = whatsapp.PresenceUnavailable
ce.Reply("Disabled presence bridging")
}
if ce.User.IsConnected() {
_, err := ce.User.Conn.Presence("", newPresence)
if err != nil {
ce.User.log.Warnln("Failed to set presence:", err)
}
}
//customPuppet.EnablePresence = !customPuppet.EnablePresence
//var newPresence whatsapp.Presence
//if customPuppet.EnablePresence {
// newPresence = whatsapp.PresenceAvailable
// ce.Reply("Enabled presence bridging")
//} else {
// newPresence = whatsapp.PresenceUnavailable
// ce.Reply("Disabled presence bridging")
//}
//if ce.User.IsConnected() {
// _, err := ce.User.Conn.Presence("", newPresence)
// if err != nil {
// ce.User.log.Warnln("Failed to set presence:", err)
// }
//}
}
if ce.Args[0] == "receipts" || ce.Args[0] == "all" {
customPuppet.EnableReceipts = !customPuppet.EnableReceipts
@ -468,108 +563,82 @@ 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.")
// 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.")
}
@ -577,21 +646,11 @@ 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)")
}
}
@ -612,12 +671,10 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
cmdPrefix + cmdDeleteSessionHelp,
cmdPrefix + cmdReconnectHelp,
cmdPrefix + cmdDisconnectHelp,
cmdPrefix + cmdDeleteConnectionHelp,
cmdPrefix + cmdPingHelp,
cmdPrefix + cmdLoginMatrixHelp,
cmdPrefix + cmdLogoutMatrixHelp,
cmdPrefix + cmdToggleHelp,
cmdPrefix + cmdSyncHelp,
cmdPrefix + cmdListHelp,
cmdPrefix + cmdOpenHelp,
cmdPrefix + cmdPMHelp,
@ -630,37 +687,6 @@ 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()
if err != nil {
user.log.Errorln("Error updating contacts:", err)
ce.Reply("Failed to sync contact list (see logs for details)")
return
}
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
}
ce.Reply("Syncing contacts...")
user.syncPuppets(nil)
ce.Reply("Syncing chats...")
user.syncPortals(nil, create)
ce.Reply("Sync complete.")
}
const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.`
func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
@ -670,11 +696,13 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
}
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
}
// TODO reimplement
//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
//}
return
}
ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.")
@ -687,12 +715,13 @@ const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals th
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)
}
}
// TODO reimplement
//for _, portal := range portals {
// users := portal.GetUserIDs()
// if len(users) == 1 && users[0] == ce.User.MXID {
// portalsToDelete = append(portalsToDelete, portal)
// }
//}
leave := func(portal *Portal) {
if len(portal.MXID) > 0 {
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
@ -729,21 +758,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 +803,34 @@ 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"))
// 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.`
@ -811,80 +841,68 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
return
}
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)
_, err = portal.MainIntent().Client.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
err = nil
}
}
if err != nil {
portal.log.Warnfln("Failed to invite %s to portal: %v. Creating new portal", user.MXID, err)
@ -894,7 +912,7 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
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,7 +30,6 @@ 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"`
@ -100,7 +98,6 @@ type BridgeConfig struct {
usernameTemplate *template.Template `yaml:"-"`
displaynameTemplate *template.Template `yaml:"-"`
communityTemplate *template.Template `yaml:"-"`
}
func (bc *BridgeConfig) setDefaults() {
@ -156,13 +153,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
}
@ -170,44 +160,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()
}

View file

@ -100,7 +100,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

@ -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)
}
@ -221,11 +221,12 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
puppet.customUser.log.Debugfln("Ignoring double puppeted read receipt %+v", event.Content.Raw)
// 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)
if err != nil {
puppet.customUser.log.Warnln("Error marking read:", err)
}
// TODO reimplement
//puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
//_, err := puppet.customUser.Client.Read(portal.Key.JID, message.JID)
//if err != nil {
// puppet.customUser.log.Warnln("Error marking read:", err)
//}
}
}
}
@ -240,14 +241,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

@ -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,10 @@ 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 {
@ -52,7 +51,7 @@ func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
return
}
func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.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)
}
@ -90,9 +89,9 @@ type Message struct {
log log.Logger
Chat PortalKey
JID whatsapp.MessageID
JID types.MessageID
MXID id.EventID
Sender whatsapp.JID
Sender types.JID
Timestamp int64
Sent bool
}

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 {
func GroupPortalKey(jid types.JID) PortalKey {
return PortalKey{
JID: jid,
Receiver: jid,
JID: jid.ToNonAD(),
Receiver: jid.ToNonAD(),
}
}
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
}
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 {
func (pq *PortalQuery) GetAllByJID(jid types.JID) []*Portal {
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
}
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@s.whatsapp.net' AND jid LIKE '%@s.whatsapp.net'", receiver)
}
func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) {
@ -170,25 +168,25 @@ func (portal *Portal) Delete() {
}
}
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
}
//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,87 @@
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
}
}
return nil
}}
}

View file

@ -39,7 +39,7 @@ type upgrade struct {
fn upgradeFunc
}
const NumberOfUpgrades = 24
const NumberOfUpgrades = 26
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

@ -73,21 +73,13 @@ 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

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
}

33
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-20211022171408-90a9b647d253
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/mautrix v0.9.29
)
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.6.8 // indirect
github.com/tidwall/match v1.0.3 // indirect
github.com/tidwall/pretty v1.0.2 // indirect
github.com/tidwall/sjson v1.1.5 // indirect
go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
)

34
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=
@ -77,11 +78,11 @@ 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/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.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/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=
@ -138,16 +139,18 @@ 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=
go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488 h1:dIOtV7Fl8bxdOOvBndilSmWFcufBArgq2sZJOqV3Enc=
go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
go.mau.fi/whatsmeow v0.0.0-20211022171408-90a9b647d253 h1:poKOYLU6AFJF5wqq4iuV4zYvl4TUCe2D77ZMv4of36k=
go.mau.fi/whatsmeow v0.0.0-20211022171408-90a9b647d253/go.mod h1:GJl+Pfu5TEvDM+lXG/PnX9/yMf6vEMwD8HC4Nq75Vhg=
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=
@ -179,8 +182,9 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
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-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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=
@ -198,8 +202,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=
@ -220,5 +224,5 @@ maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfk
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/mautrix v0.9.29 h1:qJyTSZQuogkkEFrJd+oZiTuE/6Cq7ca3wxiLYadYUoM=
maunium.net/go/mautrix v0.9.29/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=

51
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"
@ -48,7 +54,7 @@ var (
// This is changed when making a release
Version = "0.1.8"
// This is filled by init()
WAVersion = ""
WAVersion = ""
VersionString = ""
// These are filled at build time with the -X linker flag
Tag = "unknown"
@ -148,16 +154,17 @@ type Bridge struct {
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 +183,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 +266,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 +280,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() {
@ -374,7 +400,7 @@ 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)
@ -401,15 +427,12 @@ 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()
}
}

View file

@ -201,13 +201,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) {
@ -259,7 +256,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
}
@ -300,7 +297,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
}
@ -439,7 +436,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
@ -27,7 +27,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"
@ -58,13 +58,10 @@ type MetricsHandler struct {
unencryptedGroupCount prometheus.Gauge
unencryptedPrivateCount prometheus.Gauge
connected prometheus.Gauge
connectedState map[whatsapp.JID]bool
loggedIn prometheus.Gauge
loggedInState map[whatsapp.JID]bool
syncLocked prometheus.Gauge
syncLockedState map[whatsapp.JID]bool
bufferLength *prometheus.GaugeVec
connected prometheus.Gauge
connectedState map[string]bool
loggedIn prometheus.Gauge
loggedInState map[string]bool
}
func NewMetricsHandler(address string, log log.Logger, db *database.Database) *MetricsHandler {
@ -121,21 +118,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),
}
}
@ -154,7 +142,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
}
@ -165,7 +153,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())
}
}
@ -176,13 +164,13 @@ 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
}
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 {
@ -191,13 +179,13 @@ 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
}
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 {
@ -206,28 +194,6 @@ func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool)
}
}
func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
if !mh.running {
return
}
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

1715
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
@ -21,7 +21,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
@ -29,8 +28,6 @@ import (
"github.com/gorilla/websocket"
"github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
@ -122,7 +119,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,13 +127,13 @@ 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 {
if user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You don't have a WhatsApp connection.",
ErrCode: "not connected",
@ -149,35 +146,20 @@ func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Req
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"})
}
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.",
@ -190,68 +172,69 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
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."
}
jsonResponse(w, http.StatusOK, Response{true, msg})
// TODO reimplement
//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."
//}
//
//jsonResponse(w, http.StatusOK, Response{true, msg})
}
func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
@ -259,32 +242,16 @@ 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,
@ -314,7 +281,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,26 +289,24 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
})
}
} else {
err := user.Conn.Logout()
if err != nil {
user.log.Warnln("Error while logging out:", err)
if !force {
jsonResponse(w, http.StatusInternalServerError, Error{
Error: fmt.Sprintf("Unknown error while logging out: %v", err),
ErrCode: err.Error(),
})
return
}
}
// TODO reimplement
//err := user.Client.Logout()
//if err != nil {
// user.log.Warnln("Error while logging out:", err)
// if !force {
// jsonResponse(w, http.StatusInternalServerError, Error{
// Error: fmt.Sprintf("Unknown error while logging out: %v", err),
// ErrCode: err.Error(),
// })
// return
// }
//}
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."})
}
@ -353,87 +318,88 @@ var upgrader = websocket.Upgrader{
}
func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
user := prov.bridge.GetUserByMXID(id.UserID(userID))
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
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,
})
}
}()
go func() {
// Read everything so SetCloseHandler() works
for {
_, _, err = c.ReadMessage()
if err != nil {
break
}
}
}()
ctx, cancel := context.WithCancel(context.Background())
c.SetCloseHandler(func(code int, text string) error {
user.log.Debugfln("Login websocket closed (%d), cancelling login", code)
cancel()
return nil
})
user.log.Debugln("Starting login via provisioning API")
session, jid, err := user.Conn.Login(qrChan, ctx)
qrChan <- "stop"
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."
} else {
msg = fmt.Sprintf("Unknown error while logging in: %v", err)
}
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()
// TODO reimplement
//userID := r.URL.Query().Get("user_id")
//user := prov.bridge.GetUserByMXID(id.UserID(userID))
//
//c, err := upgrader.Upgrade(w, r, nil)
//if err != nil {
// 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,
// })
// }
//}()
//
//go func() {
// // Read everything so SetCloseHandler() works
// for {
// _, _, err = c.ReadMessage()
// if err != nil {
// break
// }
// }
//}()
//ctx, cancel := context.WithCancel(context.Background())
//c.SetCloseHandler(func(code int, text string) error {
// user.log.Debugfln("Login websocket closed (%d), cancelling login", code)
// cancel()
// return nil
//})
//
//user.log.Debugln("Starting login via provisioning API")
//session, jid, err := user.Conn.Login(qrChan, ctx)
//qrChan <- "stop"
//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."
// } else {
// msg = fmt.Sprintf("Unknown error while logging in: %v", err)
// }
// 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()
}

151
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,13 +17,15 @@
package main
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"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"
@ -34,19 +36,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 +59,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 +130,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)
}
@ -160,13 +164,10 @@ 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) ||
// FIXME
//(portal.backfilling && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling) ||
portal.Key.JID == puppet.JID {
return puppet.DefaultIntent()
}
@ -181,63 +182,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 +290,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 +312,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()

1366
user.go

File diff suppressed because it is too large Load diff