mirror of
https://github.com/tulir/mautrix-whatsapp
synced 2024-11-17 23:43:10 +01:00
commit
b80e0c8db5
33 changed files with 2662 additions and 3551 deletions
|
@ -55,7 +55,7 @@ build docker amd64:
|
|||
DOCKER_ARCH: amd64
|
||||
after_script:
|
||||
- |
|
||||
if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
|
||||
if [[ "$CI_COMMIT_BRANCH" == "legacy" && "$CI_JOB_STATUS" == "success" ]]; then
|
||||
apk add --update curl jq
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# mautrix-whatsapp
|
||||
A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)
|
||||
implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project.
|
||||
A Matrix-WhatsApp puppeting bridge based on [whatsmeow](https://github.com/tulir/whatsmeow).
|
||||
|
||||
### Documentation
|
||||
All setup and usage instructions are located on
|
||||
[docs.mau.fi](https://docs.mau.fi/bridges/go/whatsapp/index.html).
|
||||
Some quick links:
|
||||
All setup and usage instructions are located on [docs.mau.fi]. Some quick links:
|
||||
|
||||
[docs.mau.fi]: https://docs.mau.fi/bridges/go/whatsapp/index.html
|
||||
|
||||
* [Bridge setup](https://docs.mau.fi/bridges/go/whatsapp/setup/index.html)
|
||||
(or [with Docker](https://docs.mau.fi/bridges/go/whatsapp/setup/docker.html))
|
||||
|
|
24
ROADMAP.md
24
ROADMAP.md
|
@ -12,14 +12,14 @@
|
|||
* [x] Read receipts
|
||||
* [ ] Power level
|
||||
* [ ] Membership actions
|
||||
* [x] Invite
|
||||
* [ ] Invite
|
||||
* [ ] Join
|
||||
* [x] Leave
|
||||
* [x] Kick
|
||||
* [ ] Leave
|
||||
* [ ] Kick
|
||||
* [ ] Room metadata changes
|
||||
* [x] Name
|
||||
* [ ] Avatar<sup>[1]</sup>
|
||||
* [x] Topic
|
||||
* [ ] Name
|
||||
* [ ] Avatar
|
||||
* [ ] Topic
|
||||
* [ ] Initial room metadata
|
||||
* WhatsApp → Matrix
|
||||
* [x] Message content
|
||||
|
@ -32,10 +32,10 @@
|
|||
* [ ] Chat types
|
||||
* [x] Private chat
|
||||
* [x] Group chat
|
||||
* [ ] Broadcast list<sup>[2]</sup>
|
||||
* [ ] Broadcast list
|
||||
* [x] Message deletions
|
||||
* [x] Avatars
|
||||
* [x] Presence
|
||||
* [ ] Presence
|
||||
* [x] Typing notifications
|
||||
* [x] Read receipts
|
||||
* [x] Admin/superadmin status
|
||||
|
@ -49,8 +49,8 @@
|
|||
* [x] Avatar
|
||||
* [x] Description
|
||||
* [x] Initial group metadata
|
||||
* [ ] User metadata changes
|
||||
* [ ] Display name<sup>[3]</sup>
|
||||
* [x] User metadata changes
|
||||
* [x] Display name
|
||||
* [x] Avatar
|
||||
* [x] Initial user metadata
|
||||
* [x] Display name
|
||||
|
@ -63,7 +63,3 @@
|
|||
* [x] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
|
||||
* [x] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients
|
||||
* [x] Shared group chat portals
|
||||
|
||||
<sup>[1]</sup> May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp
|
||||
<sup>[2]</sup> May already work
|
||||
<sup>[3]</sup> May not be possible
|
||||
|
|
|
@ -20,16 +20,11 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
@ -38,7 +33,6 @@ import (
|
|||
type BridgeStateEvent string
|
||||
|
||||
const (
|
||||
StateStarting BridgeStateEvent = "STARTING"
|
||||
StateUnconfigured BridgeStateEvent = "UNCONFIGURED"
|
||||
StateRunning BridgeStateEvent = "RUNNING"
|
||||
StateConnecting BridgeStateEvent = "CONNECTING"
|
||||
|
@ -56,20 +50,14 @@ const (
|
|||
WANotLoggedIn BridgeErrorCode = "wa-logged-out"
|
||||
WANotConnected BridgeErrorCode = "wa-not-connected"
|
||||
WAConnecting BridgeErrorCode = "wa-connecting"
|
||||
WATimeout BridgeErrorCode = "wa-timeout"
|
||||
WAServerTimeout BridgeErrorCode = "wa-server-timeout"
|
||||
WAPingFalse BridgeErrorCode = "wa-ping-false"
|
||||
WAPingError BridgeErrorCode = "wa-ping-error"
|
||||
)
|
||||
|
||||
var bridgeHumanErrors = map[BridgeErrorCode]string{
|
||||
WANotLoggedIn: "You're not logged into WhatsApp",
|
||||
WANotConnected: "You're not connected to WhatsApp",
|
||||
WAConnecting: "Trying to reconnect to WhatsApp. Please make sure WhatsApp is running on your phone and connected to the internet.",
|
||||
WATimeout: "WhatsApp on your phone is not responding. Please make sure it is running and connected to the internet.",
|
||||
WAServerTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
|
||||
WAPingFalse: "WhatsApp returned an error, reconnecting. Please make sure WhatsApp is running on your phone and connected to the internet.",
|
||||
WAPingError: "WhatsApp returned an unknown error",
|
||||
}
|
||||
|
||||
type BridgeState struct {
|
||||
|
@ -94,8 +82,8 @@ type GlobalBridgeState struct {
|
|||
func (pong BridgeState) fill(user *User) BridgeState {
|
||||
if user != nil {
|
||||
pong.UserID = user.MXID
|
||||
pong.RemoteID = strings.TrimSuffix(user.JID, whatsapp.NewUserSuffix)
|
||||
pong.RemoteName = fmt.Sprintf("+%s", pong.RemoteID)
|
||||
pong.RemoteID = fmt.Sprintf("%s_a%d_d%d", user.JID.User, user.JID.Agent, user.JID.Device)
|
||||
pong.RemoteName = fmt.Sprintf("+%s", user.JID.User)
|
||||
}
|
||||
|
||||
pong.Timestamp = time.Now().Unix()
|
||||
|
@ -116,32 +104,6 @@ func (pong *BridgeState) shouldDeduplicate(newPong *BridgeState) bool {
|
|||
return pong.Timestamp+int64(pong.TTL/5) > time.Now().Unix()
|
||||
}
|
||||
|
||||
func (user *User) setupAdminTestHooks() {
|
||||
if len(user.bridge.Config.Homeserver.StatusEndpoint) == 0 {
|
||||
return
|
||||
}
|
||||
user.Conn.AdminTestHook = func(err error) {
|
||||
if errors.Is(err, whatsapp.ErrConnectionTimeout) {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
|
||||
} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
|
||||
} else if errors.Is(err, whatsapp.ErrPingFalse) {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingFalse})
|
||||
} else if err == nil {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateConnected})
|
||||
} else {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingError})
|
||||
}
|
||||
}
|
||||
user.Conn.CountTimeoutHook = func(wsKeepaliveErrorCount int) {
|
||||
if wsKeepaliveErrorCount > 0 {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
|
||||
} else {
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) createBridgeStateRequest(ctx context.Context, state *BridgeState) (req *http.Request, err error) {
|
||||
var body bytes.Buffer
|
||||
if err = json.NewEncoder(&body).Encode(&state); err != nil {
|
||||
|
@ -210,8 +172,6 @@ func (user *User) sendBridgeState(state BridgeState) {
|
|||
}
|
||||
}
|
||||
|
||||
var bridgeStatePingID uint32 = 0
|
||||
|
||||
func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
|
||||
if !prov.bridge.AS.CheckServerToken(w, r) {
|
||||
return
|
||||
|
@ -221,37 +181,12 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
|
|||
var global BridgeState
|
||||
global.StateEvent = StateRunning
|
||||
var remote BridgeState
|
||||
if user.Conn != nil {
|
||||
if user.Conn.IsConnected() && user.Conn.IsLoggedIn() {
|
||||
pingID := atomic.AddUint32(&bridgeStatePingID, 1)
|
||||
user.log.Debugfln("Pinging WhatsApp mobile due to bridge status /ping API request (ID %d)", pingID)
|
||||
err := user.Conn.AdminTestWithSuppress(true)
|
||||
if errors.Is(r.Context().Err(), context.Canceled) {
|
||||
user.log.Warnfln("Ping request %d was canceled before we responded (response was %v)", pingID, err)
|
||||
user.prevBridgeStatus = nil
|
||||
return
|
||||
}
|
||||
user.log.Debugfln("Ping %d response: %v", pingID, err)
|
||||
remote.StateEvent = StateTransientDisconnect
|
||||
if err == whatsapp.ErrPingFalse {
|
||||
user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
|
||||
go user.HandleError(err)
|
||||
remote.Error = WAPingFalse
|
||||
} else if errors.Is(err, whatsapp.ErrConnectionTimeout) {
|
||||
remote.Error = WATimeout
|
||||
} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
|
||||
remote.Error = WAServerTimeout
|
||||
} else if err != nil {
|
||||
remote.Error = WAPingError
|
||||
} else {
|
||||
remote.StateEvent = StateConnected
|
||||
}
|
||||
} else if user.Conn.IsLoginInProgress() && user.Session != nil {
|
||||
if user.IsConnected() {
|
||||
if user.Client.IsLoggedIn {
|
||||
remote.StateEvent = StateConnected
|
||||
} else if user.Session != nil {
|
||||
remote.StateEvent = StateConnecting
|
||||
remote.Error = WAConnecting
|
||||
} else if !user.Conn.IsConnected() && user.Session != nil {
|
||||
remote.StateEvent = StateBadCredentials
|
||||
remote.Error = WANotConnected
|
||||
} // else: unconfigured
|
||||
} else if user.Session != nil {
|
||||
remote.StateEvent = StateBadCredentials
|
||||
|
|
726
commands.go
726
commands.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -20,22 +20,21 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/skip2/go-qrcode"
|
||||
|
||||
"maunium.net/go/maulogger/v2"
|
||||
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/database"
|
||||
)
|
||||
|
||||
type CommandHandler struct {
|
||||
|
@ -94,17 +93,11 @@ func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message stri
|
|||
Args: args[1:],
|
||||
}
|
||||
handler.log.Debugfln("%s sent '%s' in %s", user.MXID, message, roomID)
|
||||
if roomID == handler.bridge.Config.Bridge.Relaybot.ManagementRoom {
|
||||
handler.CommandRelaybot(ce)
|
||||
} else {
|
||||
handler.CommandMux(ce)
|
||||
}
|
||||
handler.CommandMux(ce)
|
||||
}
|
||||
|
||||
func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
|
||||
switch ce.Command {
|
||||
case "relaybot":
|
||||
handler.CommandRelaybot(ce)
|
||||
case "login":
|
||||
handler.CommandLogin(ce)
|
||||
case "logout-matrix":
|
||||
|
@ -119,8 +112,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
|
|||
handler.CommandDisconnect(ce)
|
||||
case "ping":
|
||||
handler.CommandPing(ce)
|
||||
case "delete-connection":
|
||||
handler.CommandDeleteConnection(ce)
|
||||
case "delete-session":
|
||||
handler.CommandDeleteSession(ce)
|
||||
case "delete-portal":
|
||||
|
@ -137,20 +128,22 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
|
|||
handler.CommandLogout(ce)
|
||||
case "toggle":
|
||||
handler.CommandToggle(ce)
|
||||
case "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
|
||||
case "set-relay", "unset-relay", "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create":
|
||||
if !ce.User.HasSession() {
|
||||
ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.")
|
||||
return
|
||||
} else if !ce.User.IsConnected() {
|
||||
} else if !ce.User.IsLoggedIn() {
|
||||
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect.")
|
||||
return
|
||||
}
|
||||
|
||||
switch ce.Command {
|
||||
case "set-relay":
|
||||
handler.CommandSetRelay(ce)
|
||||
case "unset-relay":
|
||||
handler.CommandUnsetRelay(ce)
|
||||
case "login-matrix":
|
||||
handler.CommandLoginMatrix(ce)
|
||||
case "sync":
|
||||
handler.CommandSync(ce)
|
||||
case "list":
|
||||
handler.CommandList(ce)
|
||||
case "open":
|
||||
|
@ -180,22 +173,35 @@ func (handler *CommandHandler) CommandDiscardMegolmSession(ce *CommandEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
func (handler *CommandHandler) CommandRelaybot(ce *CommandEvent) {
|
||||
if handler.bridge.Relaybot == nil {
|
||||
ce.Reply("The relaybot is disabled")
|
||||
} else if !ce.User.Admin {
|
||||
ce.Reply("Only admins can manage the relaybot")
|
||||
const cmdSetRelayHelp = `set-relay - Relay messages in this room through your WhatsApp account.`
|
||||
|
||||
func (handler *CommandHandler) CommandSetRelay(ce *CommandEvent) {
|
||||
if !handler.bridge.Config.Bridge.Relay.Enabled {
|
||||
ce.Reply("Relay mode is not enabled on this instance of the bridge")
|
||||
} else if ce.Portal == nil {
|
||||
ce.Reply("This is not a portal room")
|
||||
} else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
|
||||
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
|
||||
} else {
|
||||
if ce.Command == "relaybot" {
|
||||
if len(ce.Args) == 0 {
|
||||
ce.Reply("**Usage:** `relaybot <command>`")
|
||||
return
|
||||
}
|
||||
ce.Command = strings.ToLower(ce.Args[0])
|
||||
ce.Args = ce.Args[1:]
|
||||
}
|
||||
ce.User = handler.bridge.Relaybot
|
||||
handler.CommandMux(ce)
|
||||
ce.Portal.RelayUserID = ce.User.MXID
|
||||
ce.Portal.Update()
|
||||
ce.Reply("Messages from non-logged-in users in this room will now be bridged through your WhatsApp account")
|
||||
}
|
||||
}
|
||||
|
||||
const cmdUnsetRelayHelp = `set-relay - Stop relaying messages in this room.`
|
||||
|
||||
func (handler *CommandHandler) CommandUnsetRelay(ce *CommandEvent) {
|
||||
if !handler.bridge.Config.Bridge.Relay.Enabled {
|
||||
ce.Reply("Relay mode is not enabled on this instance of the bridge")
|
||||
} else if ce.Portal == nil {
|
||||
ce.Reply("This is not a portal room")
|
||||
} else if handler.bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
|
||||
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
|
||||
} else {
|
||||
ce.Portal.RelayUserID = ""
|
||||
ce.Portal.Update()
|
||||
ce.Reply("Messages from non-logged-in users will no longer be bridged in this room")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,12 +232,14 @@ func (handler *CommandHandler) CommandInviteLink(ce *CommandEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get invite link: %v", err)
|
||||
return
|
||||
}
|
||||
ce.Reply("%s%s", inviteLinkPrefix, link)
|
||||
ce.Reply("Not yet implemented")
|
||||
// TODO reimplement
|
||||
//link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
|
||||
//if err != nil {
|
||||
// ce.Reply("Failed to get invite link: %v", err)
|
||||
// return
|
||||
//}
|
||||
//ce.Reply("%s%s", inviteLinkPrefix, link)
|
||||
}
|
||||
|
||||
const cmdJoinHelp = `join <invite link> - Join a group chat with an invite link.`
|
||||
|
@ -246,26 +254,28 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to join group: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
|
||||
portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
|
||||
if len(portal.MXID) > 0 {
|
||||
portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
|
||||
ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
|
||||
} else {
|
||||
err = portal.CreateMatrixRoom(ce.User)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to create portal room: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
|
||||
}
|
||||
ce.Reply("Not yet implemented")
|
||||
// TODO reimplement
|
||||
//jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
|
||||
//if err != nil {
|
||||
// ce.Reply("Failed to join group: %v", err)
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
|
||||
//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
|
||||
//if len(portal.MXID) > 0 {
|
||||
// portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
|
||||
// ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
|
||||
//} else {
|
||||
// err = portal.CreateMatrixRoom(ce.User)
|
||||
// if err != nil {
|
||||
// ce.Reply("Failed to create portal room: %v", err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
|
||||
//}
|
||||
}
|
||||
|
||||
const cmdCreateHelp = `create - Create a group chat.`
|
||||
|
@ -299,43 +309,45 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
participants := []string{ce.User.JID}
|
||||
participants := []types.JID{ce.User.JID.ToNonAD()}
|
||||
for userID := range members.Joined {
|
||||
jid, ok := handler.bridge.ParsePuppetMXID(userID)
|
||||
if ok && jid != ce.User.JID {
|
||||
if ok && jid.User != ce.User.JID.User {
|
||||
participants = append(participants, jid)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to create group: %v", err)
|
||||
return
|
||||
}
|
||||
portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
|
||||
portal.roomCreateLock.Lock()
|
||||
defer portal.roomCreateLock.Unlock()
|
||||
if len(portal.MXID) != 0 {
|
||||
portal.log.Warnln("Detected race condition in room creation")
|
||||
// TODO race condition, clean up the old room
|
||||
}
|
||||
portal.MXID = ce.RoomID
|
||||
portal.Name = roomNameEvent.Name
|
||||
portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
|
||||
if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
|
||||
_, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
|
||||
if err != nil {
|
||||
portal.log.Warnln("Failed to enable e2be:", err)
|
||||
}
|
||||
portal.Encrypted = true
|
||||
}
|
||||
|
||||
portal.Update()
|
||||
portal.UpdateBridgeInfo()
|
||||
|
||||
ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
|
||||
inCommunity := ce.User.addPortalToCommunity(portal)
|
||||
ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
|
||||
ce.Reply("Not yet implemented")
|
||||
// TODO reimplement
|
||||
//resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
|
||||
//if err != nil {
|
||||
// ce.Reply("Failed to create group: %v", err)
|
||||
// return
|
||||
//}
|
||||
//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
|
||||
//portal.roomCreateLock.Lock()
|
||||
//defer portal.roomCreateLock.Unlock()
|
||||
//if len(portal.MXID) != 0 {
|
||||
// portal.log.Warnln("Detected race condition in room creation")
|
||||
// // TODO race condition, clean up the old room
|
||||
//}
|
||||
//portal.MXID = ce.RoomID
|
||||
//portal.Name = roomNameEvent.Name
|
||||
//portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
|
||||
//if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
|
||||
// _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
|
||||
// if err != nil {
|
||||
// portal.log.Warnln("Failed to enable e2be:", err)
|
||||
// }
|
||||
// portal.Encrypted = true
|
||||
//}
|
||||
//
|
||||
//portal.Update()
|
||||
//portal.UpdateBridgeInfo()
|
||||
//
|
||||
//ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
|
||||
//inCommunity := ce.User.addPortalToCommunity(portal)
|
||||
//ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
|
||||
}
|
||||
|
||||
const cmdSetPowerLevelHelp = `set-pl [user ID] <power level> - Change the power level in a portal room. Only for bridge admins.`
|
||||
|
@ -378,25 +390,94 @@ func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
|
||||
const cmdLoginHelp = `login - Link the bridge to your WhatsApp account as a web client`
|
||||
|
||||
// CommandLogin handles login command
|
||||
func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
|
||||
if !ce.User.Connect(true) {
|
||||
ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
|
||||
if ce.User.Session != nil {
|
||||
if ce.User.IsConnected() {
|
||||
ce.Reply("You're already logged in")
|
||||
} else {
|
||||
ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
|
||||
}
|
||||
return
|
||||
}
|
||||
ce.User.Login(ce)
|
||||
|
||||
qrChan, err := ce.User.Login(context.Background())
|
||||
if err != nil {
|
||||
ce.User.log.Errorf("Failed to log in:", err)
|
||||
ce.Reply("Failed to log in: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var qrEventID id.EventID
|
||||
for item := range qrChan {
|
||||
switch item {
|
||||
case whatsmeow.QRChannelSuccess:
|
||||
jid := ce.User.Client.Store.ID
|
||||
ce.Reply("Successfully logged in as +%s (device #%d)", jid.User, jid.Device)
|
||||
case whatsmeow.QRChannelTimeout:
|
||||
ce.Reply("QR code timed out. Please restart the login.")
|
||||
case whatsmeow.QRChannelErrUnexpectedEvent:
|
||||
ce.Reply("Failed to log in: unexpected connection event from server")
|
||||
case whatsmeow.QRChannelScannedWithoutMultidevice:
|
||||
ce.Reply("Please enable the WhatsApp multidevice beta and scan the QR code again.")
|
||||
default:
|
||||
qrEventID = ce.User.sendQR(ce, string(item), qrEventID)
|
||||
}
|
||||
}
|
||||
_, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
|
||||
}
|
||||
|
||||
const cmdLogoutHelp = `logout - Logout from WhatsApp`
|
||||
func (user *User) sendQR(ce *CommandEvent, code string, prevEvent id.EventID) id.EventID {
|
||||
url, ok := user.uploadQR(ce, code)
|
||||
if !ok {
|
||||
return prevEvent
|
||||
}
|
||||
content := event.MessageEventContent{
|
||||
MsgType: event.MsgImage,
|
||||
Body: code,
|
||||
URL: url.CUString(),
|
||||
}
|
||||
if len(prevEvent) != 0 {
|
||||
content.SetEdit(prevEvent)
|
||||
}
|
||||
resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)
|
||||
if err != nil {
|
||||
user.log.Errorln("Failed to send edited QR code to user:", err)
|
||||
} else if len(prevEvent) == 0 {
|
||||
prevEvent = resp.EventID
|
||||
}
|
||||
return prevEvent
|
||||
}
|
||||
|
||||
func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool) {
|
||||
qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
|
||||
if err != nil {
|
||||
user.log.Errorln("Failed to encode QR code:", err)
|
||||
ce.Reply("Failed to encode QR code: %v", err)
|
||||
return id.ContentURI{}, false
|
||||
}
|
||||
|
||||
bot := user.bridge.AS.BotClient()
|
||||
|
||||
resp, err := bot.UploadBytes(qrCode, "image/png")
|
||||
if err != nil {
|
||||
user.log.Errorln("Failed to upload QR code:", err)
|
||||
ce.Reply("Failed to upload QR code: %v", err)
|
||||
return id.ContentURI{}, false
|
||||
}
|
||||
return resp.ContentURI, true
|
||||
}
|
||||
|
||||
const cmdLogoutHelp = `logout - Unlink the bridge from your WhatsApp account`
|
||||
|
||||
// CommandLogout handles !logout command
|
||||
func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
|
||||
if ce.User.Session == nil {
|
||||
ce.Reply("You're not logged in.")
|
||||
return
|
||||
} else if !ce.User.IsConnected() {
|
||||
} else if !ce.User.IsLoggedIn() {
|
||||
ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.")
|
||||
return
|
||||
}
|
||||
|
@ -407,17 +488,16 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
|
|||
ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
|
||||
}
|
||||
}
|
||||
err := ce.User.Conn.Logout()
|
||||
err := ce.User.Client.Logout()
|
||||
if err != nil {
|
||||
ce.User.log.Warnln("Error while logging out:", err)
|
||||
ce.Reply("Unknown error while logging out: %v", err)
|
||||
return
|
||||
}
|
||||
ce.User.Session = nil
|
||||
ce.User.removeFromJIDMap(StateLoggedOut)
|
||||
// TODO this causes a foreign key violation, which should be fixed
|
||||
//ce.User.JID = ""
|
||||
ce.User.SetSession(nil)
|
||||
ce.User.DeleteConnection()
|
||||
ce.User.DeleteSession()
|
||||
ce.Reply("Logged out successfully.")
|
||||
}
|
||||
|
||||
|
@ -439,16 +519,16 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
|
|||
}
|
||||
if ce.Args[0] == "presence" || ce.Args[0] == "all" {
|
||||
customPuppet.EnablePresence = !customPuppet.EnablePresence
|
||||
var newPresence whatsapp.Presence
|
||||
var newPresence types.Presence
|
||||
if customPuppet.EnablePresence {
|
||||
newPresence = whatsapp.PresenceAvailable
|
||||
newPresence = types.PresenceAvailable
|
||||
ce.Reply("Enabled presence bridging")
|
||||
} else {
|
||||
newPresence = whatsapp.PresenceUnavailable
|
||||
newPresence = types.PresenceUnavailable
|
||||
ce.Reply("Disabled presence bridging")
|
||||
}
|
||||
if ce.User.IsConnected() {
|
||||
_, err := ce.User.Conn.Presence("", newPresence)
|
||||
if ce.User.IsLoggedIn() {
|
||||
err := ce.User.Client.SendPresence(newPresence)
|
||||
if err != nil {
|
||||
ce.User.log.Warnln("Failed to set presence:", err)
|
||||
}
|
||||
|
@ -468,130 +548,96 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
|
|||
const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request`
|
||||
|
||||
func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) {
|
||||
if ce.User.Session == nil && ce.User.Conn == nil {
|
||||
if ce.User.Session == nil && ce.User.Client == nil {
|
||||
ce.Reply("Nothing to purge: no session information stored and no active connection.")
|
||||
return
|
||||
}
|
||||
//ce.User.JID = ""
|
||||
ce.User.removeFromJIDMap(StateLoggedOut)
|
||||
ce.User.SetSession(nil)
|
||||
ce.User.DeleteConnection()
|
||||
ce.User.DeleteSession()
|
||||
ce.Reply("Session information purged")
|
||||
}
|
||||
|
||||
const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp`
|
||||
|
||||
func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) {
|
||||
if ce.User.Conn == nil {
|
||||
if ce.User.Session == nil {
|
||||
ce.Reply("No existing connection and no session. Did you mean `login`?")
|
||||
} else {
|
||||
ce.Reply("No existing connection, creating one...")
|
||||
ce.User.Connect(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wasConnected := true
|
||||
err := ce.User.Conn.Disconnect()
|
||||
if err == whatsapp.ErrNotConnected {
|
||||
wasConnected = false
|
||||
} else if err != nil {
|
||||
ce.User.log.Warnln("Error while disconnecting:", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
err = ce.User.Conn.Restore(true, ctx)
|
||||
if err == whatsapp.ErrInvalidSession {
|
||||
if ce.User.Session != nil {
|
||||
ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
|
||||
ce.User.Conn.SetSession(*ce.User.Session)
|
||||
err = ce.User.Conn.Restore(true, ctx)
|
||||
} else {
|
||||
ce.Reply("You are not logged in.")
|
||||
return
|
||||
}
|
||||
} else if err == whatsapp.ErrLoginInProgress {
|
||||
ce.Reply("A login or reconnection is already in progress.")
|
||||
return
|
||||
} else if err == whatsapp.ErrAlreadyLoggedIn {
|
||||
ce.Reply("You were already connected.")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ce.User.log.Warnln("Error while reconnecting:", err)
|
||||
ce.Reply("Unknown error while reconnecting: %v", err)
|
||||
ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
|
||||
err = ce.User.Conn.Disconnect()
|
||||
if err != nil {
|
||||
ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ce.User.ConnectionErrors = 0
|
||||
|
||||
var msg string
|
||||
if wasConnected {
|
||||
msg = "Reconnected successfully."
|
||||
} else {
|
||||
msg = "Connected successfully."
|
||||
}
|
||||
ce.Reply(msg)
|
||||
ce.User.PostLogin()
|
||||
}
|
||||
|
||||
const cmdDeleteConnectionHelp = `delete-connection - Disconnect ignoring errors and delete internal connection state.`
|
||||
|
||||
func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) {
|
||||
if ce.User.Conn == nil {
|
||||
ce.Reply("You don't have a WhatsApp connection.")
|
||||
return
|
||||
}
|
||||
ce.User.DeleteConnection()
|
||||
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
|
||||
ce.Reply("Not yet implemented")
|
||||
// TODO reimplement
|
||||
//if ce.User.Client == nil {
|
||||
// if ce.User.Session == nil {
|
||||
// ce.Reply("No existing connection and no session. Did you mean `login`?")
|
||||
// } else {
|
||||
// ce.Reply("No existing connection, creating one...")
|
||||
// ce.User.Connect(false)
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//wasConnected := true
|
||||
//ce.User.Client.Disconnect()
|
||||
//ctx := context.Background()
|
||||
//connected := ce.User.Connect(false)
|
||||
//
|
||||
//err = ce.User.Conn.Restore(true, ctx)
|
||||
//if err == whatsapp.ErrInvalidSession {
|
||||
// if ce.User.Session != nil {
|
||||
// ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
|
||||
// ce.User.Conn.SetSession(*ce.User.Session)
|
||||
// err = ce.User.Conn.Restore(true, ctx)
|
||||
// } else {
|
||||
// ce.Reply("You are not logged in.")
|
||||
// return
|
||||
// }
|
||||
//} else if err == whatsapp.ErrLoginInProgress {
|
||||
// ce.Reply("A login or reconnection is already in progress.")
|
||||
// return
|
||||
//} else if err == whatsapp.ErrAlreadyLoggedIn {
|
||||
// ce.Reply("You were already connected.")
|
||||
// return
|
||||
//}
|
||||
//if err != nil {
|
||||
// ce.User.log.Warnln("Error while reconnecting:", err)
|
||||
// ce.Reply("Unknown error while reconnecting: %v", err)
|
||||
// ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
|
||||
// err = ce.User.Conn.Disconnect()
|
||||
// if err != nil {
|
||||
// ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
//ce.User.ConnectionErrors = 0
|
||||
//
|
||||
//var msg string
|
||||
//if wasConnected {
|
||||
// msg = "Reconnected successfully."
|
||||
//} else {
|
||||
// msg = "Connected successfully."
|
||||
//}
|
||||
//ce.Reply(msg)
|
||||
//ce.User.PostLogin()
|
||||
}
|
||||
|
||||
const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without logging out)`
|
||||
|
||||
func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) {
|
||||
if ce.User.Conn == nil {
|
||||
if ce.User.Client == nil {
|
||||
ce.Reply("You don't have a WhatsApp connection.")
|
||||
return
|
||||
}
|
||||
err := ce.User.Conn.Disconnect()
|
||||
if err == whatsapp.ErrNotConnected {
|
||||
ce.Reply("You were not connected.")
|
||||
return
|
||||
} else if err != nil {
|
||||
ce.User.log.Warnln("Error while disconnecting:", err)
|
||||
ce.Reply("Unknown error while disconnecting: %v", err)
|
||||
return
|
||||
}
|
||||
ce.User.bridge.Metrics.TrackConnectionState(ce.User.JID, false)
|
||||
ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
|
||||
ce.User.DeleteConnection()
|
||||
ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
|
||||
ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
|
||||
}
|
||||
|
||||
const cmdPingHelp = `ping - Check your connection to WhatsApp.`
|
||||
|
||||
func (handler *CommandHandler) CommandPing(ce *CommandEvent) {
|
||||
if ce.User.Session == nil {
|
||||
if ce.User.IsLoginInProgress() {
|
||||
ce.Reply("You're not logged into WhatsApp, but there's a login in progress.")
|
||||
} else {
|
||||
ce.Reply("You're not logged into WhatsApp.")
|
||||
}
|
||||
} else if ce.User.Conn == nil {
|
||||
ce.Reply("You're not logged into WhatsApp.")
|
||||
} else if ce.User.Client == nil || !ce.User.Client.IsConnected() {
|
||||
ce.Reply("You don't have a WhatsApp connection.")
|
||||
} else if err := ce.User.Conn.AdminTest(); err != nil {
|
||||
if ce.User.IsLoginInProgress() {
|
||||
ce.Reply("Connection not OK: %v, but login in progress", err)
|
||||
} else {
|
||||
ce.Reply("Connection not OK: %v", err)
|
||||
}
|
||||
} else {
|
||||
ce.Reply("Connection to WhatsApp OK")
|
||||
ce.Reply("Connection to WhatsApp OK (probably)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,7 +646,7 @@ const cmdHelpHelp = `help - Prints this help`
|
|||
// CommandHelp handles help command
|
||||
func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
|
||||
cmdPrefix := ""
|
||||
if ce.User.ManagementRoom != ce.RoomID || ce.User.IsRelaybot {
|
||||
if ce.User.ManagementRoom != ce.RoomID {
|
||||
cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " "
|
||||
}
|
||||
|
||||
|
@ -612,12 +658,12 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
|
|||
cmdPrefix + cmdDeleteSessionHelp,
|
||||
cmdPrefix + cmdReconnectHelp,
|
||||
cmdPrefix + cmdDisconnectHelp,
|
||||
cmdPrefix + cmdDeleteConnectionHelp,
|
||||
cmdPrefix + cmdPingHelp,
|
||||
cmdPrefix + cmdSetRelayHelp,
|
||||
cmdPrefix + cmdUnsetRelayHelp,
|
||||
cmdPrefix + cmdLoginMatrixHelp,
|
||||
cmdPrefix + cmdLogoutMatrixHelp,
|
||||
cmdPrefix + cmdToggleHelp,
|
||||
cmdPrefix + cmdSyncHelp,
|
||||
cmdPrefix + cmdListHelp,
|
||||
cmdPrefix + cmdOpenHelp,
|
||||
cmdPrefix + cmdPMHelp,
|
||||
|
@ -630,35 +676,23 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
|
|||
}, "\n* "))
|
||||
}
|
||||
|
||||
const cmdSyncHelp = `sync [--create-all] - Synchronize contacts from phone and optionally create portals for group chats.`
|
||||
|
||||
// CommandSync handles sync command
|
||||
func (handler *CommandHandler) CommandSync(ce *CommandEvent) {
|
||||
user := ce.User
|
||||
create := len(ce.Args) > 0 && ce.Args[0] == "--create-all"
|
||||
|
||||
ce.Reply("Updating contact and chat list...")
|
||||
handler.log.Debugln("Importing contacts of", user.MXID)
|
||||
_, err := user.Conn.Contacts()
|
||||
func canDeletePortal(portal *Portal, userID id.UserID) bool {
|
||||
members, err := portal.MainIntent().JoinedMembers(portal.MXID)
|
||||
if err != nil {
|
||||
user.log.Errorln("Error updating contacts:", err)
|
||||
ce.Reply("Failed to sync contact list (see logs for details)")
|
||||
return
|
||||
portal.log.Errorfln("Failed to get joined members to check if portal can be deleted by %s: %v", userID, err)
|
||||
return false
|
||||
}
|
||||
handler.log.Debugln("Importing chats of", user.MXID)
|
||||
_, err = user.Conn.Chats()
|
||||
if err != nil {
|
||||
user.log.Errorln("Error updating chats:", err)
|
||||
ce.Reply("Failed to sync chat list (see logs for details)")
|
||||
return
|
||||
for otherUser := range members.Joined {
|
||||
_, isPuppet := portal.bridge.ParsePuppetMXID(otherUser)
|
||||
if isPuppet || otherUser == portal.bridge.Bot.UserID || otherUser == userID {
|
||||
continue
|
||||
}
|
||||
user := portal.bridge.GetUserByMXID(otherUser)
|
||||
if user != nil && user.Session != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ce.Reply("Syncing contacts...")
|
||||
user.syncPuppets(nil)
|
||||
ce.Reply("Syncing chats...")
|
||||
user.syncPortals(nil, create)
|
||||
|
||||
ce.Reply("Sync complete.")
|
||||
return true
|
||||
}
|
||||
|
||||
const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.`
|
||||
|
@ -669,12 +703,9 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ce.User.Admin {
|
||||
users := ce.Portal.GetUserIDs()
|
||||
if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) {
|
||||
ce.Reply("Only bridge admins can delete portals with other Matrix users")
|
||||
return
|
||||
}
|
||||
if !ce.User.Admin && !canDeletePortal(ce.Portal, ce.User.MXID) {
|
||||
ce.Reply("Only bridge admins can delete portals with other Matrix users")
|
||||
return
|
||||
}
|
||||
|
||||
ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.")
|
||||
|
@ -682,17 +713,23 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
|
|||
ce.Portal.Cleanup(false)
|
||||
}
|
||||
|
||||
const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals that aren't used by any other user.'`
|
||||
const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all portals.`
|
||||
|
||||
func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
|
||||
portals := ce.User.GetPortals()
|
||||
portalsToDelete := make([]*Portal, 0, len(portals))
|
||||
for _, portal := range portals {
|
||||
users := portal.GetUserIDs()
|
||||
if len(users) == 1 && users[0] == ce.User.MXID {
|
||||
portalsToDelete = append(portalsToDelete, portal)
|
||||
portals := handler.bridge.GetAllPortals()
|
||||
var portalsToDelete []*Portal
|
||||
|
||||
if ce.User.Admin {
|
||||
portals = portalsToDelete
|
||||
} else {
|
||||
portalsToDelete = portals[:0]
|
||||
for _, portal := range portals {
|
||||
if canDeletePortal(portal, ce.User.MXID) {
|
||||
portalsToDelete = append(portalsToDelete, portal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leave := func(portal *Portal) {
|
||||
if len(portal.MXID) > 0 {
|
||||
_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
|
||||
|
@ -711,13 +748,12 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
|
|||
}
|
||||
}
|
||||
}
|
||||
ce.Reply("Found %d portals with no other users, deleting...", len(portalsToDelete))
|
||||
ce.Reply("Found %d portals, deleting...", len(portalsToDelete))
|
||||
for _, portal := range portalsToDelete {
|
||||
portal.Delete()
|
||||
leave(portal)
|
||||
}
|
||||
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background. " +
|
||||
"You may already continue using the bridge. Use `sync` to recreate portals.")
|
||||
ce.Reply("Finished deleting portal info. Now cleaning up rooms in background.")
|
||||
|
||||
go func() {
|
||||
for _, portal := range portalsToDelete {
|
||||
|
@ -729,21 +765,21 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
|
|||
|
||||
const cmdListHelp = `list <contacts|groups> [page] [items per page] - Get a list of all contacts and groups.`
|
||||
|
||||
func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
|
||||
for jid, contact := range input {
|
||||
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
|
||||
continue
|
||||
}
|
||||
|
||||
if contacts {
|
||||
result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
|
||||
} else {
|
||||
result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.StringSlice(result))
|
||||
return
|
||||
}
|
||||
//func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
|
||||
// for jid, contact := range input {
|
||||
// if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// if contacts {
|
||||
// result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
|
||||
// } else {
|
||||
// result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
|
||||
// }
|
||||
// }
|
||||
// sort.Sort(sort.StringSlice(result))
|
||||
// return
|
||||
//}
|
||||
|
||||
func (handler *CommandHandler) CommandList(ce *CommandEvent) {
|
||||
if len(ce.Args) == 0 {
|
||||
|
@ -774,33 +810,35 @@ func (handler *CommandHandler) CommandList(ce *CommandEvent) {
|
|||
ce.Reply("Warning: a high number of items per page may fail to send a reply")
|
||||
}
|
||||
}
|
||||
contacts := mode[0] == 'c'
|
||||
typeName := "Groups"
|
||||
if contacts {
|
||||
typeName = "Contacts"
|
||||
}
|
||||
ce.User.Conn.Store.ContactsLock.RLock()
|
||||
result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
|
||||
ce.User.Conn.Store.ContactsLock.RUnlock()
|
||||
if len(result) == 0 {
|
||||
ce.Reply("No %s found", strings.ToLower(typeName))
|
||||
return
|
||||
}
|
||||
pages := int(math.Ceil(float64(len(result)) / float64(max)))
|
||||
if (page-1)*max >= len(result) {
|
||||
if pages == 1 {
|
||||
ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
|
||||
} else {
|
||||
ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
|
||||
}
|
||||
return
|
||||
}
|
||||
lastIndex := page * max
|
||||
if lastIndex > len(result) {
|
||||
lastIndex = len(result)
|
||||
}
|
||||
result = result[(page-1)*max : lastIndex]
|
||||
ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
|
||||
ce.Reply("Not yet implemented")
|
||||
// TODO reimplement
|
||||
//contacts := mode[0] == 'c'
|
||||
//typeName := "Groups"
|
||||
//if contacts {
|
||||
// typeName = "Contacts"
|
||||
//}
|
||||
//ce.User.Conn.Store.ContactsLock.RLock()
|
||||
//result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
|
||||
//ce.User.Conn.Store.ContactsLock.RUnlock()
|
||||
//if len(result) == 0 {
|
||||
// ce.Reply("No %s found", strings.ToLower(typeName))
|
||||
// return
|
||||
//}
|
||||
//pages := int(math.Ceil(float64(len(result)) / float64(max)))
|
||||
//if (page-1)*max >= len(result) {
|
||||
// if pages == 1 {
|
||||
// ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
|
||||
// } else {
|
||||
// ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
//lastIndex := page * max
|
||||
//if lastIndex > len(result) {
|
||||
// lastIndex = len(result)
|
||||
//}
|
||||
//result = result[(page-1)*max : lastIndex]
|
||||
//ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
|
||||
}
|
||||
|
||||
const cmdOpenHelp = `open <_group JID_> - Open a group chat portal.`
|
||||
|
@ -810,91 +848,75 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
|
|||
ce.Reply("**Usage:** `open <group JID>`")
|
||||
return
|
||||
}
|
||||
ce.Reply("Not yet implemented")
|
||||
|
||||
user := ce.User
|
||||
jid := ce.Args[0]
|
||||
|
||||
if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
|
||||
ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
|
||||
return
|
||||
}
|
||||
|
||||
user.Conn.Store.ContactsLock.RLock()
|
||||
contact, ok := user.Conn.Store.Contacts[jid]
|
||||
user.Conn.Store.ContactsLock.RUnlock()
|
||||
if !ok {
|
||||
ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
|
||||
return
|
||||
}
|
||||
handler.log.Debugln("Importing", jid, "for", user)
|
||||
portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
|
||||
if len(portal.MXID) > 0 {
|
||||
portal.Sync(user, contact)
|
||||
ce.Reply("Portal room synced.")
|
||||
} else {
|
||||
portal.Sync(user, contact)
|
||||
ce.Reply("Portal room created.")
|
||||
}
|
||||
_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
|
||||
// TODO reimplement
|
||||
//user := ce.User
|
||||
//jid := ce.Args[0]
|
||||
//if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
|
||||
// ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
|
||||
// return
|
||||
//}
|
||||
//
|
||||
//user.Conn.Store.ContactsLock.RLock()
|
||||
//contact, ok := user.Conn.Store.Contacts[jid]
|
||||
//user.Conn.Store.ContactsLock.RUnlock()
|
||||
//if !ok {
|
||||
// ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
|
||||
// return
|
||||
//}
|
||||
//handler.log.Debugln("Importing", jid, "for", user)
|
||||
//portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
|
||||
//if len(portal.MXID) > 0 {
|
||||
// portal.Sync(user, contact)
|
||||
// ce.Reply("Portal room synced.")
|
||||
//} else {
|
||||
// portal.Sync(user, contact)
|
||||
// ce.Reply("Portal room created.")
|
||||
//}
|
||||
//_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
|
||||
}
|
||||
|
||||
const cmdPMHelp = `pm [--force] <_international phone number_> - Open a private chat with the given phone number.`
|
||||
const cmdPMHelp = `pm <_international phone number_> - Open a private chat with the given phone number.`
|
||||
|
||||
func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
|
||||
if len(ce.Args) == 0 {
|
||||
ce.Reply("**Usage:** `pm [--force] <international phone number>`")
|
||||
ce.Reply("**Usage:** `pm <international phone number>`")
|
||||
return
|
||||
}
|
||||
|
||||
force := ce.Args[0] == "--force"
|
||||
if force {
|
||||
ce.Args = ce.Args[1:]
|
||||
}
|
||||
|
||||
user := ce.User
|
||||
|
||||
number := strings.Join(ce.Args, "")
|
||||
if number[0] == '+' {
|
||||
number = number[1:]
|
||||
resp, err := ce.User.Client.IsOnWhatsApp([]string{number})
|
||||
if err != nil {
|
||||
ce.Reply("Failed to check if user is on WhatsApp: %v", err)
|
||||
return
|
||||
} else if len(resp) == 0 {
|
||||
ce.Reply("Didn't get a response to checking if the user is on WhatsApp")
|
||||
return
|
||||
}
|
||||
for _, char := range number {
|
||||
if char < '0' || char > '9' {
|
||||
ce.Reply("Invalid phone number.")
|
||||
return
|
||||
}
|
||||
targetUser := resp[0]
|
||||
if !targetUser.IsIn {
|
||||
ce.Reply("The server said +%s is not on WhatsApp", targetUser.JID.User)
|
||||
return
|
||||
}
|
||||
jid := number + whatsapp.NewUserSuffix
|
||||
|
||||
handler.log.Debugln("Importing", jid, "for", user)
|
||||
|
||||
user.Conn.Store.ContactsLock.RLock()
|
||||
contact, ok := user.Conn.Store.Contacts[jid]
|
||||
user.Conn.Store.ContactsLock.RUnlock()
|
||||
if !ok {
|
||||
if !force {
|
||||
ce.Reply("Phone number not found in contacts. Try syncing contacts with `sync` first. " +
|
||||
"To create a portal anyway, use `pm --force <number>`.")
|
||||
return
|
||||
}
|
||||
contact = whatsapp.Contact{JID: jid}
|
||||
}
|
||||
puppet := user.bridge.GetPuppetByJID(contact.JID)
|
||||
puppet.Sync(user, contact)
|
||||
portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.JID, user.JID))
|
||||
handler.log.Debugln("Importing", targetUser.JID, "for", user)
|
||||
puppet := user.bridge.GetPuppetByJID(targetUser.JID)
|
||||
puppet.SyncContact(user, true)
|
||||
portal := user.GetPortalByJID(puppet.JID)
|
||||
if len(portal.MXID) > 0 {
|
||||
var err error
|
||||
if !user.IsRelaybot {
|
||||
err = portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
|
||||
}
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to invite %s to portal: %v. Creating new portal", user.MXID, err)
|
||||
ok := portal.ensureUserInvited(user)
|
||||
if !ok {
|
||||
portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", user.MXID)
|
||||
portal.MXID = ""
|
||||
} else {
|
||||
ce.Reply("You already have a private chat portal with that user at [%s](https://matrix.to/#/%s)", puppet.Displayname, portal.MXID)
|
||||
return
|
||||
}
|
||||
}
|
||||
err := portal.CreateMatrixRoom(user)
|
||||
err = portal.CreateMatrixRoom(user)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to create portal room: %v", err)
|
||||
return
|
||||
|
|
132
community.go
132
community.go
|
@ -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
|
||||
}
|
162
config/bridge.go
162
config/bridge.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -17,12 +17,11 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
@ -31,18 +30,8 @@ import (
|
|||
type BridgeConfig struct {
|
||||
UsernameTemplate string `yaml:"username_template"`
|
||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||
CommunityTemplate string `yaml:"community_template"`
|
||||
|
||||
ConnectionTimeout int `yaml:"connection_timeout"`
|
||||
FetchMessageOnTimeout bool `yaml:"fetch_message_on_timeout"`
|
||||
DeliveryReceipts bool `yaml:"delivery_receipts"`
|
||||
MaxConnectionAttempts int `yaml:"max_connection_attempts"`
|
||||
ConnectionRetryDelay int `yaml:"connection_retry_delay"`
|
||||
ReportConnectionRetry bool `yaml:"report_connection_retry"`
|
||||
AggressiveReconnect bool `yaml:"aggressive_reconnect"`
|
||||
ChatListWait int `yaml:"chat_list_wait"`
|
||||
PortalSyncWait int `yaml:"portal_sync_wait"`
|
||||
UserMessageBuffer int `yaml:"user_message_buffer"`
|
||||
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
||||
|
||||
CallNotices struct {
|
||||
|
@ -50,15 +39,13 @@ type BridgeConfig struct {
|
|||
End bool `yaml:"end"`
|
||||
} `yaml:"call_notices"`
|
||||
|
||||
InitialChatSync int `yaml:"initial_chat_sync_count"`
|
||||
InitialHistoryFill int `yaml:"initial_history_fill_count"`
|
||||
HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"`
|
||||
RecoverChatSync int `yaml:"recovery_chat_sync_count"`
|
||||
RecoverHistory bool `yaml:"recovery_history_backfill"`
|
||||
ChatMetaSync bool `yaml:"chat_meta_sync"`
|
||||
UserAvatarSync bool `yaml:"user_avatar_sync"`
|
||||
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
|
||||
SyncChatMaxAge int64 `yaml:"sync_max_chat_age"`
|
||||
HistorySync struct {
|
||||
CreatePortals bool `yaml:"create_portals"`
|
||||
Backfill bool `yaml:"backfill"`
|
||||
DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
|
||||
} `yaml:"history_sync"`
|
||||
UserAvatarSync bool `yaml:"user_avatar_sync"`
|
||||
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
|
||||
|
||||
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
|
||||
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
|
||||
|
@ -66,16 +53,15 @@ type BridgeConfig struct {
|
|||
DefaultBridgePresence bool `yaml:"default_bridge_presence"`
|
||||
LoginSharedSecret string `yaml:"login_shared_secret"`
|
||||
|
||||
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
|
||||
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
|
||||
BridgeNotices bool `yaml:"bridge_notices"`
|
||||
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
|
||||
MuteBridging bool `yaml:"mute_bridging"`
|
||||
ArchiveTag string `yaml:"archive_tag"`
|
||||
PinnedTag string `yaml:"pinned_tag"`
|
||||
TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
|
||||
MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
|
||||
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
|
||||
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
|
||||
BridgeNotices bool `yaml:"bridge_notices"`
|
||||
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
|
||||
MuteBridging bool `yaml:"mute_bridging"`
|
||||
ArchiveTag string `yaml:"archive_tag"`
|
||||
PinnedTag string `yaml:"pinned_tag"`
|
||||
TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
|
||||
MarkReadOnlyOnCreate bool `yaml:"mark_read_only_on_create"`
|
||||
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
|
||||
|
||||
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
|
||||
|
||||
|
@ -103,44 +89,26 @@ type BridgeConfig struct {
|
|||
|
||||
Permissions PermissionConfig `yaml:"permissions"`
|
||||
|
||||
Relaybot RelaybotConfig `yaml:"relaybot"`
|
||||
Relay RelaybotConfig `yaml:"relay"`
|
||||
|
||||
usernameTemplate *template.Template `yaml:"-"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
communityTemplate *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
func (bc *BridgeConfig) setDefaults() {
|
||||
bc.ConnectionTimeout = 20
|
||||
bc.FetchMessageOnTimeout = false
|
||||
bc.DeliveryReceipts = false
|
||||
bc.MaxConnectionAttempts = 3
|
||||
bc.ConnectionRetryDelay = -1
|
||||
bc.ReportConnectionRetry = true
|
||||
bc.ChatListWait = 30
|
||||
bc.PortalSyncWait = 600
|
||||
bc.UserMessageBuffer = 1024
|
||||
bc.PortalMessageBuffer = 128
|
||||
|
||||
bc.CallNotices.Start = true
|
||||
bc.CallNotices.End = true
|
||||
|
||||
bc.InitialChatSync = 10
|
||||
bc.InitialHistoryFill = 20
|
||||
bc.RecoverChatSync = -1
|
||||
bc.RecoverHistory = true
|
||||
bc.ChatMetaSync = true
|
||||
bc.HistorySync.CreatePortals = true
|
||||
bc.UserAvatarSync = true
|
||||
bc.BridgeMatrixLeave = true
|
||||
bc.SyncChatMaxAge = 259200
|
||||
|
||||
bc.SyncWithCustomPuppets = true
|
||||
bc.DefaultBridgePresence = true
|
||||
bc.DefaultBridgeReceipts = true
|
||||
bc.LoginSharedSecret = ""
|
||||
|
||||
bc.InviteOwnPuppetForBackfilling = true
|
||||
bc.PrivateChatPortalMeta = false
|
||||
bc.BridgeNotices = true
|
||||
bc.EnableStatusBroadcast = true
|
||||
|
||||
|
@ -167,13 +135,6 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if len(bc.CommunityTemplate) > 0 {
|
||||
bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -181,44 +142,43 @@ type UsernameTemplateArgs struct {
|
|||
UserID id.UserID
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
|
||||
var buf bytes.Buffer
|
||||
if index := strings.IndexRune(contact.JID, '@'); index > 0 {
|
||||
contact.JID = "+" + contact.JID[:index]
|
||||
}
|
||||
bc.displaynameTemplate.Execute(&buf, contact)
|
||||
type legacyContactInfo struct {
|
||||
types.ContactInfo
|
||||
Phone string
|
||||
|
||||
Notify string
|
||||
VName string
|
||||
Name string
|
||||
Short string
|
||||
JID string
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatDisplayname(jid types.JID, contact types.ContactInfo) (string, int8) {
|
||||
var buf strings.Builder
|
||||
_ = bc.displaynameTemplate.Execute(&buf, legacyContactInfo{
|
||||
ContactInfo: contact,
|
||||
Notify: contact.PushName,
|
||||
VName: contact.BusinessName,
|
||||
Name: contact.FullName,
|
||||
Short: contact.FirstName,
|
||||
Phone: "+" + jid.User,
|
||||
JID: "+" + jid.User,
|
||||
})
|
||||
var quality int8
|
||||
switch {
|
||||
case len(contact.Notify) > 0 || len(contact.VName) > 0:
|
||||
case len(contact.PushName) > 0 || len(contact.BusinessName) > 0:
|
||||
quality = 3
|
||||
case len(contact.Name) > 0 || len(contact.Short) > 0:
|
||||
case len(contact.FullName) > 0 || len(contact.FirstName) > 0:
|
||||
quality = 2
|
||||
case len(contact.JID) > 0:
|
||||
quality = 1
|
||||
default:
|
||||
quality = 0
|
||||
quality = 1
|
||||
}
|
||||
return buf.String(), quality
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatUsername(userID whatsapp.JID) string {
|
||||
var buf bytes.Buffer
|
||||
bc.usernameTemplate.Execute(&buf, userID)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type CommunityTemplateArgs struct {
|
||||
Localpart string
|
||||
Server string
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) EnableCommunities() bool {
|
||||
return bc.communityTemplate != nil
|
||||
}
|
||||
|
||||
func (bc BridgeConfig) FormatCommunity(localpart, server string) string {
|
||||
var buf bytes.Buffer
|
||||
bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server})
|
||||
func (bc BridgeConfig) FormatUsername(username string) string {
|
||||
var buf strings.Builder
|
||||
_ = bc.usernameTemplate.Execute(&buf, username)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
|
@ -227,10 +187,10 @@ type PermissionConfig map[string]PermissionLevel
|
|||
type PermissionLevel int
|
||||
|
||||
const (
|
||||
PermissionLevelDefault PermissionLevel = 0
|
||||
PermissionLevelRelaybot PermissionLevel = 5
|
||||
PermissionLevelUser PermissionLevel = 10
|
||||
PermissionLevelAdmin PermissionLevel = 100
|
||||
PermissionLevelDefault PermissionLevel = 0
|
||||
PermissionLevelRelay PermissionLevel = 5
|
||||
PermissionLevelUser PermissionLevel = 10
|
||||
PermissionLevelAdmin PermissionLevel = 100
|
||||
)
|
||||
|
||||
func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
|
@ -245,8 +205,8 @@ func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
|
|||
}
|
||||
for key, value := range rawPC {
|
||||
switch strings.ToLower(value) {
|
||||
case "relaybot":
|
||||
(*pc)[key] = PermissionLevelRelaybot
|
||||
case "relaybot", "relay":
|
||||
(*pc)[key] = PermissionLevelRelay
|
||||
case "user":
|
||||
(*pc)[key] = PermissionLevelUser
|
||||
case "admin":
|
||||
|
@ -270,8 +230,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
|
|||
rawPC := make(map[string]string)
|
||||
for key, value := range *pc {
|
||||
switch value {
|
||||
case PermissionLevelRelaybot:
|
||||
rawPC[key] = "relaybot"
|
||||
case PermissionLevelRelay:
|
||||
rawPC[key] = "relay"
|
||||
case PermissionLevelUser:
|
||||
rawPC[key] = "user"
|
||||
case PermissionLevelAdmin:
|
||||
|
@ -283,8 +243,8 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
|
|||
return rawPC, nil
|
||||
}
|
||||
|
||||
func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool {
|
||||
return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot
|
||||
func (pc PermissionConfig) IsRelayWhitelisted(userID id.UserID) bool {
|
||||
return pc.GetPermissionLevel(userID) >= PermissionLevelRelay
|
||||
}
|
||||
|
||||
func (pc PermissionConfig) IsWhitelisted(userID id.UserID) bool {
|
||||
|
@ -316,10 +276,8 @@ func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel
|
|||
}
|
||||
|
||||
type RelaybotConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
ManagementRoom id.RoomID `yaml:"management"`
|
||||
InviteUsers []id.UserID `yaml:"invites"`
|
||||
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AdminOnly bool `yaml:"admin_only"`
|
||||
MessageFormats map[event.MessageType]string `yaml:"message_formats"`
|
||||
messageTemplates *template.Template `yaml:"-"`
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build cgo && !nocrypto
|
||||
// +build cgo,!nocrypto
|
||||
|
||||
package main
|
||||
|
@ -100,7 +101,8 @@ func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info ev
|
|||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"}
|
||||
}
|
||||
user := helper.bridge.GetUserByMXID(device.UserID)
|
||||
if !user.Admin && !user.IsInPortal(portal.Key) {
|
||||
// FIXME reimplement IsInPortal
|
||||
if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ {
|
||||
helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID)
|
||||
return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -23,7 +23,7 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
|
@ -160,7 +160,7 @@ func (puppet *Puppet) stopSyncing() {
|
|||
}
|
||||
|
||||
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
|
||||
if !puppet.customUser.IsConnected() {
|
||||
if !puppet.customUser.IsLoggedIn() {
|
||||
puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
|
||||
return nil
|
||||
}
|
||||
|
@ -200,14 +200,14 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
|
|||
}
|
||||
|
||||
func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
|
||||
presence := whatsapp.PresenceAvailable
|
||||
presence := types.PresenceAvailable
|
||||
if event.Content.Raw["presence"].(string) != "online" {
|
||||
presence = whatsapp.PresenceUnavailable
|
||||
presence = types.PresenceUnavailable
|
||||
puppet.customUser.log.Debugln("Marking offline")
|
||||
} else {
|
||||
puppet.customUser.log.Debugln("Marking online")
|
||||
}
|
||||
_, err := puppet.customUser.Conn.Presence("", presence)
|
||||
err := puppet.customUser.Client.SendPresence(presence)
|
||||
if err != nil {
|
||||
puppet.customUser.log.Warnln("Failed to set presence:", err)
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
|
|||
// Ignore double puppeted read receipts.
|
||||
} else if message := puppet.bridge.DB.Message.GetByMXID(eventID); message != nil {
|
||||
puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
|
||||
_, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
|
||||
err := puppet.customUser.Client.MarkRead([]types.MessageID{message.JID}, time.UnixMilli(receipt.Timestamp), portal.Key.JID, message.Sender)
|
||||
if err != nil {
|
||||
puppet.customUser.log.Warnln("Error marking read:", err)
|
||||
}
|
||||
|
@ -240,14 +240,14 @@ func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
|
|||
}
|
||||
if puppet.customTypingIn[evt.RoomID] != isTyping {
|
||||
puppet.customTypingIn[evt.RoomID] = isTyping
|
||||
presence := whatsapp.PresenceComposing
|
||||
presence := types.ChatPresenceComposing
|
||||
if !isTyping {
|
||||
puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
|
||||
presence = whatsapp.PresencePaused
|
||||
presence = types.ChatPresencePaused
|
||||
} else {
|
||||
puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
|
||||
}
|
||||
_, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
|
||||
err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID)
|
||||
if err != nil {
|
||||
puppet.customUser.log.Warnln("Error setting typing:", err)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build cgo && !nocrypto
|
||||
// +build cgo,!nocrypto
|
||||
|
||||
package database
|
||||
|
|
|
@ -19,14 +19,19 @@ package database
|
|||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
"maunium.net/go/mautrix-whatsapp/database/upgrades"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sqlstore.PostgresArrayWrapper = pq.Array
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
*sql.DB
|
||||
log log.Logger
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -21,11 +21,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
type MessageQuery struct {
|
||||
|
@ -40,45 +40,66 @@ func (mq *MessageQuery) New() *Message {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
getAllMessagesQuery = `
|
||||
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
|
||||
WHERE chat_jid=$1 AND chat_receiver=$2
|
||||
`
|
||||
getMessageByJIDQuery = `
|
||||
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
|
||||
WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3
|
||||
`
|
||||
getMessageByMXIDQuery = `
|
||||
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
|
||||
WHERE mxid=$1
|
||||
`
|
||||
getLastMessageInChatQuery = `
|
||||
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
|
||||
WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1
|
||||
`
|
||||
getFirstMessageInChatQuery = `
|
||||
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error FROM message
|
||||
WHERE chat_jid=$1 AND chat_receiver=$2 AND sent=true ORDER BY timestamp ASC LIMIT 1
|
||||
`
|
||||
)
|
||||
|
||||
func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
|
||||
rows, err := mq.db.Query("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent FROM message WHERE chat_jid=$1 AND chat_receiver=$2", chat.JID, chat.Receiver)
|
||||
rows, err := mq.db.Query(getAllMessagesQuery, chat.JID, chat.Receiver)
|
||||
if err != nil || rows == nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
messages = append(messages, mq.New().Scan(rows))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
|
||||
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
|
||||
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
|
||||
func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.MessageID) *Message {
|
||||
return mq.maybeScan(mq.db.QueryRow(getMessageByJIDQuery, chat.JID, chat.Receiver, jid))
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message {
|
||||
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
|
||||
"FROM message WHERE mxid=$1", mxid)
|
||||
return mq.maybeScan(mq.db.QueryRow(getMessageByMXIDQuery, mxid))
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message {
|
||||
return mq.GetLastInChatBefore(chat, time.Now().Unix()+60)
|
||||
return mq.GetLastInChatBefore(chat, time.Now().Add(60*time.Second))
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp int64) *Message {
|
||||
msg := mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
|
||||
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1",
|
||||
chat.JID, chat.Receiver, maxTimestamp)
|
||||
if msg == nil || msg.Timestamp == 0 {
|
||||
func (mq *MessageQuery) GetLastInChatBefore(chat PortalKey, maxTimestamp time.Time) *Message {
|
||||
msg := mq.maybeScan(mq.db.QueryRow(getLastMessageInChatQuery, chat.JID, chat.Receiver, maxTimestamp.Unix()))
|
||||
if msg == nil || msg.Timestamp.IsZero() {
|
||||
// Old db, we don't know what the last message is.
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) get(query string, args ...interface{}) *Message {
|
||||
row := mq.db.QueryRow(query, args...)
|
||||
func (mq *MessageQuery) GetFirstInChat(chat PortalKey) *Message {
|
||||
return mq.maybeScan(mq.db.QueryRow(getFirstMessageInChatQuery, chat.JID, chat.Receiver))
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) maybeScan(row *sql.Row) *Message {
|
||||
if row == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -90,11 +111,13 @@ type Message struct {
|
|||
log log.Logger
|
||||
|
||||
Chat PortalKey
|
||||
JID whatsapp.MessageID
|
||||
JID types.MessageID
|
||||
MXID id.EventID
|
||||
Sender whatsapp.JID
|
||||
Timestamp int64
|
||||
Sender types.JID
|
||||
Timestamp time.Time
|
||||
Sent bool
|
||||
|
||||
DecryptionError bool
|
||||
}
|
||||
|
||||
func (msg *Message) IsFakeMXID() bool {
|
||||
|
@ -102,22 +125,30 @@ func (msg *Message) IsFakeMXID() bool {
|
|||
}
|
||||
|
||||
func (msg *Message) Scan(row Scannable) *Message {
|
||||
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &msg.Sent)
|
||||
var ts int64
|
||||
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &ts, &msg.Sent, &msg.DecryptionError)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
msg.log.Errorln("Database scan failed:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if ts != 0 {
|
||||
msg.Timestamp = time.Unix(ts, 0)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *Message) Insert() {
|
||||
var sender interface{} = msg.Sender
|
||||
// Slightly hacky hack to allow inserting empty senders (used for post-backfill dummy events)
|
||||
if msg.Sender.IsEmpty() {
|
||||
sender = ""
|
||||
}
|
||||
_, err := msg.db.Exec(`INSERT INTO message
|
||||
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.Timestamp, msg.Sent)
|
||||
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, decryption_error)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.Timestamp.Unix(), msg.Sent, msg.DecryptionError)
|
||||
if err != nil {
|
||||
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
|
||||
}
|
||||
|
@ -131,6 +162,15 @@ func (msg *Message) MarkSent() {
|
|||
}
|
||||
}
|
||||
|
||||
func (msg *Message) UpdateMXID(mxid id.EventID, stillDecryptionError bool) {
|
||||
msg.MXID = mxid
|
||||
msg.DecryptionError = stillDecryptionError
|
||||
_, err := msg.db.Exec("UPDATE message SET mxid=$4, decryption_error=$5 WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID, mxid, stillDecryptionError)
|
||||
if err != nil {
|
||||
msg.log.Warnfln("Failed to update %s@%s: %v", msg.Chat, msg.JID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *Message) Delete() {
|
||||
_, err := msg.db.Exec("DELETE FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -18,42 +18,40 @@ package database
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
type PortalKey struct {
|
||||
JID whatsapp.JID
|
||||
Receiver whatsapp.JID
|
||||
JID types.JID
|
||||
Receiver types.JID
|
||||
}
|
||||
|
||||
func GroupPortalKey(jid whatsapp.JID) PortalKey {
|
||||
return PortalKey{
|
||||
JID: jid,
|
||||
Receiver: jid,
|
||||
}
|
||||
func GroupPortalKey(jid types.JID) PortalKey {
|
||||
return NewPortalKey(jid, jid)
|
||||
}
|
||||
|
||||
func NewPortalKey(jid, receiver whatsapp.JID) PortalKey {
|
||||
if strings.HasSuffix(jid, whatsapp.GroupSuffix) {
|
||||
func NewPortalKey(jid, receiver types.JID) PortalKey {
|
||||
if jid.Server == types.GroupServer {
|
||||
receiver = jid
|
||||
} else if jid.Server == types.LegacyUserServer {
|
||||
jid.Server = types.DefaultUserServer
|
||||
}
|
||||
return PortalKey{
|
||||
JID: jid,
|
||||
Receiver: receiver,
|
||||
JID: jid.ToNonAD(),
|
||||
Receiver: receiver.ToNonAD(),
|
||||
}
|
||||
}
|
||||
|
||||
func (key PortalKey) String() string {
|
||||
if key.Receiver == key.JID {
|
||||
return key.JID
|
||||
return key.JID.String()
|
||||
}
|
||||
return key.JID + "-" + key.Receiver
|
||||
return key.JID.String() + "-" + key.Receiver.String()
|
||||
}
|
||||
|
||||
type PortalQuery struct {
|
||||
|
@ -80,12 +78,12 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
|
|||
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) GetAllByJID(jid whatsapp.JID) []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
|
||||
func (pq *PortalQuery) GetAllByJID(jid types.JID) []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid.ToNonAD())
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) FindPrivateChats(receiver whatsapp.JID) []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver)
|
||||
func (pq *PortalQuery) FindPrivateChats(receiver types.JID) []*Portal {
|
||||
return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver.ToNonAD())
|
||||
}
|
||||
|
||||
func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) {
|
||||
|
@ -120,11 +118,16 @@ type Portal struct {
|
|||
Avatar string
|
||||
AvatarURL id.ContentURI
|
||||
Encrypted bool
|
||||
|
||||
FirstEventID id.EventID
|
||||
NextBatchID id.BatchID
|
||||
|
||||
RelayUserID id.UserID
|
||||
}
|
||||
|
||||
func (portal *Portal) Scan(row Scannable) *Portal {
|
||||
var mxid, avatarURL sql.NullString
|
||||
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted)
|
||||
var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString
|
||||
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
portal.log.Errorln("Database scan failed:", err)
|
||||
|
@ -133,6 +136,9 @@ func (portal *Portal) Scan(row Scannable) *Portal {
|
|||
}
|
||||
portal.MXID = id.RoomID(mxid.String)
|
||||
portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
|
||||
portal.FirstEventID = id.EventID(firstEventID.String)
|
||||
portal.NextBatchID = id.BatchID(nextBatchID.String)
|
||||
portal.RelayUserID = id.UserID(relayUserID.String)
|
||||
return portal
|
||||
}
|
||||
|
||||
|
@ -143,21 +149,24 @@ func (portal *Portal) mxidPtr() *id.RoomID {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (portal *Portal) relayUserPtr() *id.UserID {
|
||||
if len(portal.RelayUserID) > 0 {
|
||||
return &portal.RelayUserID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (portal *Portal) Insert() {
|
||||
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted)
|
||||
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) Update() {
|
||||
var mxid *id.RoomID
|
||||
if len(portal.MXID) > 0 {
|
||||
mxid = &portal.MXID
|
||||
}
|
||||
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6 WHERE jid=$7 AND receiver=$8",
|
||||
mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.Key.JID, portal.Key.Receiver)
|
||||
_, err := portal.db.Exec("UPDATE portal SET mxid=$3, name=$4, topic=$5, avatar=$6, avatar_url=$7, encrypted=$8, first_event_id=$9, next_batch_id=$10, relay_user_id=$11 WHERE jid=$1 AND receiver=$2",
|
||||
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
|
||||
if err != nil {
|
||||
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
|
||||
}
|
||||
|
@ -169,26 +178,3 @@ func (portal *Portal) Delete() {
|
|||
portal.log.Warnfln("Failed to delete %s: %v", portal.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (portal *Portal) GetUserIDs() []id.UserID {
|
||||
rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
|
||||
WHERE "user".jid=user_portal.user_jid
|
||||
AND user_portal.portal_jid=$1
|
||||
AND user_portal.portal_receiver=$2`,
|
||||
portal.Key.JID, portal.Key.Receiver)
|
||||
if err != nil {
|
||||
portal.log.Debugln("Failed to get portal user ids:", err)
|
||||
return nil
|
||||
}
|
||||
var userIDs []id.UserID
|
||||
for rows.Next() {
|
||||
var userID id.UserID
|
||||
err = rows.Scan(&userID)
|
||||
if err != nil {
|
||||
portal.log.Warnln("Failed to scan row:", err)
|
||||
continue
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
13
database/upgrades/2021-10-21-add-whatsmeow-store.go
Normal file
13
database/upgrades/2021-10-21-add-whatsmeow-store.go
Normal 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))
|
||||
}}
|
||||
}
|
93
database/upgrades/2021-10-21-multidevice-updates.go
Normal file
93
database/upgrades/2021-10-21-multidevice-updates.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package upgrades
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
upgrades[25] = upgrade{"Update things for multidevice", func(tx *sql.Tx, ctx context) error {
|
||||
// This is probably not necessary
|
||||
_, err := tx.Exec("DROP TABLE user_portal")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove invalid puppet rows
|
||||
_, err = tx.Exec("DELETE FROM puppet WHERE jid LIKE '%@g.us' OR jid LIKE '%@broadcast'")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove the suffix from puppets since they'll all have the same suffix
|
||||
_, err = tx.Exec("UPDATE puppet SET jid=REPLACE(jid, '@s.whatsapp.net', '')")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Rename column to correctly represent the new content
|
||||
_, err = tx.Exec("ALTER TABLE puppet RENAME COLUMN jid TO username")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.dialect == SQLite {
|
||||
// Message content was removed from the main message table earlier, but the backup table still exists for SQLite
|
||||
_, err = tx.Exec("DROP TABLE IF EXISTS old_message")
|
||||
|
||||
_, err = tx.Exec(`ALTER TABLE "user" RENAME TO old_user`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`CREATE TABLE "user" (
|
||||
mxid TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE,
|
||||
agent SMALLINT,
|
||||
device SMALLINT,
|
||||
management_room TEXT
|
||||
)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No need to copy auth data, users need to relogin anyway
|
||||
_, err = tx.Exec(`INSERT INTO "user" (mxid, management_room, last_connection) SELECT mxid, management_room, last_connection FROM old_user`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DROP TABLE old_user")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// The jid column never actually contained the full JID, so let's rename it.
|
||||
_, err = tx.Exec(`ALTER TABLE "user" RENAME COLUMN jid TO username`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The auth data is now in the whatsmeow_device table.
|
||||
for _, column := range []string{"last_connection", "client_id", "client_token", "server_token", "enc_key", "mac_key"} {
|
||||
_, err = tx.Exec(`ALTER TABLE "user" DROP COLUMN ` + column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// The whatsmeow_device table is keyed by the full JID, so we need to store the other parts of the JID here too.
|
||||
_, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN agent SMALLINT`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`ALTER TABLE "user" ADD COLUMN device SMALLINT`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear all usernames, the users need to relogin anyway.
|
||||
_, err = tx.Exec(`UPDATE "user" SET username=null`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
}
|
19
database/upgrades/2021-10-26-portal-origin-event-id.go
Normal file
19
database/upgrades/2021-10-26-portal-origin-event-id.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package upgrades
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
upgrades[26] = upgrade{"Add columns to store infinite backfill pointers for portals", func(tx *sql.Tx, ctx context) error {
|
||||
_, err := tx.Exec(`ALTER TABLE portal ADD COLUMN first_event_id TEXT NOT NULL DEFAULT ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`ALTER TABLE portal ADD COLUMN next_batch_id TEXT NOT NULL DEFAULT ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
}
|
12
database/upgrades/2021-10-27-message-decryption-errors.go
Normal file
12
database/upgrades/2021-10-27-message-decryption-errors.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package upgrades
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
upgrades[27] = upgrade{"Add marker for WhatsApp decryption errors in message table", func(tx *sql.Tx, ctx context) error {
|
||||
_, err := tx.Exec(`ALTER TABLE message ADD COLUMN decryption_error BOOLEAN NOT NULL DEFAULT false`)
|
||||
return err
|
||||
}}
|
||||
}
|
12
database/upgrades/2021-10-28-portal-relay-user.go
Normal file
12
database/upgrades/2021-10-28-portal-relay-user.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package upgrades
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
upgrades[28] = upgrade{"Add relay user field to portal table", func(tx *sql.Tx, ctx context) error {
|
||||
_, err := tx.Exec(`ALTER TABLE portal ADD COLUMN relay_user_id TEXT`)
|
||||
return err
|
||||
}}
|
||||
}
|
|
@ -39,7 +39,7 @@ type upgrade struct {
|
|||
fn upgradeFunc
|
||||
}
|
||||
|
||||
const NumberOfUpgrades = 24
|
||||
const NumberOfUpgrades = 29
|
||||
|
||||
var upgrades [NumberOfUpgrades]upgrade
|
||||
|
||||
|
|
282
database/user.go
282
database/user.go
|
@ -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)
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -27,6 +27,7 @@ appservice:
|
|||
# The database URI.
|
||||
# SQLite: File name is enough. https://github.com/mattn/go-sqlite3#connection-string
|
||||
# Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
|
||||
# To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
|
||||
uri: mautrix-whatsapp.db
|
||||
# Maximum number of connections. Mostly relevant for Postgres.
|
||||
max_open_conns: 20
|
||||
|
@ -63,9 +64,10 @@ metrics:
|
|||
whatsapp:
|
||||
# Device name that's shown in the "WhatsApp Web" section in the mobile app.
|
||||
os_name: Mautrix-WhatsApp bridge
|
||||
# Browser name that determines the logo shown in the mobile app. If the name is unrecognized, a generic icon is shown.
|
||||
# Use the name of an actual browser (Chrome, Firefox, Safari, IE, Edge, Opera) if you want a specific icon.
|
||||
browser_name: mx-wa
|
||||
# Browser name that determines the logo shown in the mobile app.
|
||||
# Must be "unknown" for a generic icon or a valid browser name if you want a specific icon.
|
||||
# List of valid browser names: https://github.com/tulir/whatsmeow/blob/2a72655ef600a7fd7a2e98d53ec6da029759c4b8/binary/proto/def.proto#L1582-L1594
|
||||
browser_name: unknown
|
||||
|
||||
# Bridge config
|
||||
bridge:
|
||||
|
@ -73,49 +75,19 @@ bridge:
|
|||
# {{.}} is replaced with the phone number of the WhatsApp user.
|
||||
username_template: whatsapp_{{.}}
|
||||
# Displayname template for WhatsApp users.
|
||||
# {{.Notify}} - nickname set by the WhatsApp user
|
||||
# {{.VName}} - validated WhatsApp business name
|
||||
# {{.JID}} - phone number (international format)
|
||||
# {{.PushName}} - nickname set by the WhatsApp user
|
||||
# {{.BusinessName}} - validated WhatsApp business name
|
||||
# {{.Phone}} - phone number (international format)
|
||||
# The following variables are also available, but will cause problems on multi-user instances:
|
||||
# {{.Name}} - display name from contact list
|
||||
# {{.Short}} - short display name from contact list
|
||||
displayname_template: "{{if .Notify}}{{.Notify}}{{else if .VName}}{{.VName}}{{else}}{{.JID}}{{end}} (WA)"
|
||||
# Localpart template for per-user room grouping community IDs.
|
||||
# On startup, the bridge will try to create these communities, add all of the specific user's
|
||||
# portals to the community, and invite the Matrix user to it.
|
||||
# (Note that, by default, non-admins might not have your homeserver's permission to create
|
||||
# communities.)
|
||||
# {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user.
|
||||
# whatsapp_{{.Localpart}}={{.Server}} is a good value that should work for any user.
|
||||
community_template: null
|
||||
# {{.FullName}} - full name from contact list
|
||||
# {{.FirstName}} - first name from contact list
|
||||
displayname_template: "{{if .PushName}}{{.PushName}}{{else if .BusinessName}}{{.BusinessName}}{{else}}{{.JID}}{{end}} (WA)"
|
||||
|
||||
# WhatsApp connection timeout in seconds.
|
||||
connection_timeout: 20
|
||||
# If WhatsApp doesn't respond within connection_timeout, should the bridge try to fetch the message
|
||||
# to see if it was actually bridged? Use this if you have problems with sends timing out but actually
|
||||
# succeeding.
|
||||
fetch_message_on_timeout: false
|
||||
# Whether or not the bridge should send a read receipt from the bridge bot when a message has been
|
||||
# sent to WhatsApp. If fetch_message_on_timeout is enabled, a successful post-timeout fetch will
|
||||
# trigger a read receipt too.
|
||||
delivery_receipts: false
|
||||
# Maximum number of times to retry connecting on connection error.
|
||||
max_connection_attempts: 3
|
||||
# Number of seconds to wait between connection attempts.
|
||||
# Negative numbers are exponential backoff: -connection_retry_delay + 1 + 2^attempts
|
||||
connection_retry_delay: -1
|
||||
# Whether or not the bridge should send a notice to the user's management room when it retries connecting.
|
||||
# If false, it will only report when it stops retrying.
|
||||
report_connection_retry: true
|
||||
# Whether or not the bridge should reconnect even if WhatsApp says another web client connected.
|
||||
aggressive_reconnect: false
|
||||
# Maximum number of seconds to wait for chats to be sent at startup.
|
||||
# If this is too low and you have lots of chats, it could cause backfilling to fail.
|
||||
chat_list_wait: 30
|
||||
# Maximum number of seconds to wait to sync portals before force unlocking message processing.
|
||||
# If this is too low and you have lots of chats, it could cause backfilling to fail.
|
||||
portal_sync_wait: 600
|
||||
user_message_buffer: 1024
|
||||
|
||||
portal_message_buffer: 128
|
||||
|
||||
# Whether or not to send call start/end notices to Matrix.
|
||||
|
@ -123,32 +95,20 @@ bridge:
|
|||
start: true
|
||||
end: true
|
||||
|
||||
# Number of chats to sync for new users.
|
||||
initial_chat_sync_count: 10
|
||||
# Number of old messages to fill when creating new portal rooms.
|
||||
initial_history_fill_count: 20
|
||||
# Whether or not notifications should be turned off while filling initial history.
|
||||
# Only applicable when using double puppeting.
|
||||
initial_history_disable_notifications: false
|
||||
# Maximum number of chats to sync when recovering from downtime.
|
||||
# Set to -1 to sync all new chats during downtime.
|
||||
recovery_chat_sync_limit: -1
|
||||
# Whether or not to sync history when recovering from downtime.
|
||||
recovery_history_backfill: true
|
||||
# Whether or not portal info should be fetched from the server when syncing,
|
||||
# instead of relying on finding any changes in the message history.
|
||||
# If you get 599 errors often, you should try disabling this.
|
||||
chat_meta_sync: true
|
||||
history_sync:
|
||||
# Whether to create portals from history sync payloads from WhatsApp.
|
||||
create_portals: true
|
||||
# Whether to enable backfilling history sync payloads from WhatsApp using batch sending
|
||||
# This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
|
||||
# It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml.
|
||||
backfill: false
|
||||
# Whether to use custom puppet for backfilling.
|
||||
# In order to use this, the custom puppets must be in the appservice's user ID namespace.
|
||||
double_puppet_backfill: false
|
||||
# Whether or not puppet avatars should be fetched from the server even if an avatar is already set.
|
||||
# If you get 599 errors often, you should try disabling this.
|
||||
user_avatar_sync: true
|
||||
# Whether or not Matrix users leaving groups should be bridged to WhatsApp
|
||||
bridge_matrix_leave: true
|
||||
# Maximum number of seconds since last message in chat to skip
|
||||
# syncing the chat in any case. This setting will take priority
|
||||
# over both recovery_chat_sync_limit and initial_chat_sync_count.
|
||||
# Default is 3 days = 259200 seconds
|
||||
sync_max_chat_age: 259200
|
||||
|
||||
# Whether or not to sync with custom puppets to receive EDUs that
|
||||
# are not normally sent to appservices.
|
||||
|
@ -169,20 +129,12 @@ bridge:
|
|||
# manually.
|
||||
login_shared_secret: null
|
||||
|
||||
# Whether or not to invite own WhatsApp user's Matrix puppet into private
|
||||
# chat portals when backfilling if needed.
|
||||
# This always uses the default puppet instead of custom puppets due to
|
||||
# rate limits and timestamp massaging.
|
||||
invite_own_puppet_for_backfilling: true
|
||||
# Whether or not to explicitly set the avatar and room name for private
|
||||
# chat portal rooms. This can be useful if the previous field works fine,
|
||||
# but causes room avatar/name bugs.
|
||||
# Whether to explicitly set the avatar and room name for private chat portal rooms.
|
||||
private_chat_portal_meta: false
|
||||
# Whether or not Matrix m.notice-type messages should be bridged.
|
||||
# Whether Matrix m.notice-type messages should be bridged.
|
||||
bridge_notices: true
|
||||
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
|
||||
# This field will automatically be changed back to false after it,
|
||||
# except if the config file is not writable.
|
||||
# This field will automatically be changed back to false after it, except if the config file is not writable.
|
||||
resend_bridge_info: false
|
||||
# When using double puppeting, should muted chats be muted in Matrix?
|
||||
mute_bridging: false
|
||||
|
@ -246,7 +198,7 @@ bridge:
|
|||
|
||||
# Permissions for using the bridge.
|
||||
# Permitted values:
|
||||
# relaybot - Talk through the relaybot (if enabled), no access otherwise
|
||||
# relay - Talk through the relaybot (if enabled), no access otherwise
|
||||
# user - Access to use the bridge to chat with a WhatsApp account.
|
||||
# admin - User level and some additional administration tools
|
||||
# Permitted keys:
|
||||
|
@ -254,19 +206,16 @@ bridge:
|
|||
# domain - All users on that homeserver
|
||||
# mxid - Specific user
|
||||
permissions:
|
||||
"*": relaybot
|
||||
"*": relay
|
||||
"example.com": user
|
||||
"@admin:example.com": admin
|
||||
|
||||
relaybot:
|
||||
# Whether or not relaybot support is enabled.
|
||||
relay:
|
||||
# Whether relay mode should be allowed. If allowed, `!signal set-relay` can be used to turn any
|
||||
# authenticated user into a relaybot for that chat.
|
||||
enabled: false
|
||||
# The management room for the bot. This is where all status notifications are posted and
|
||||
# in this room, you can use `!wa <command>` instead of `!wa relaybot <command>`. Omitting
|
||||
# the command prefix completely like in user management rooms is not possible.
|
||||
management: "!foo:example.com"
|
||||
# List of users to invite to all created rooms that include the relaybot.
|
||||
invites: []
|
||||
# Should only admins be allowed to set themselves as relay users?
|
||||
admin_only: true
|
||||
# The formats to use when sending messages to WhatsApp via the relaybot.
|
||||
message_formats:
|
||||
m.text: "<b>{{ .Sender.Displayname }}</b>: {{ .Message }}"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -22,7 +22,7 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/format"
|
||||
|
@ -57,32 +57,22 @@ func NewFormatter(bridge *Bridge) *Formatter {
|
|||
if mxid[0] == '@' {
|
||||
puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
|
||||
if puppet != nil {
|
||||
jids, ok := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
|
||||
jids, ok := ctx[mentionedJIDsContextKey].([]string)
|
||||
if !ok {
|
||||
ctx[mentionedJIDsContextKey] = []whatsapp.JID{puppet.JID}
|
||||
ctx[mentionedJIDsContextKey] = []string{puppet.JID.String()}
|
||||
} else {
|
||||
ctx[mentionedJIDsContextKey] = append(jids, puppet.JID)
|
||||
ctx[mentionedJIDsContextKey] = append(jids, puppet.JID.String())
|
||||
}
|
||||
return "@" + puppet.PhoneNumber()
|
||||
return "@" + puppet.JID.User
|
||||
}
|
||||
}
|
||||
return mxid
|
||||
},
|
||||
BoldConverter: func(text string, _ format.Context) string {
|
||||
return fmt.Sprintf("*%s*", text)
|
||||
},
|
||||
ItalicConverter: func(text string, _ format.Context) string {
|
||||
return fmt.Sprintf("_%s_", text)
|
||||
},
|
||||
StrikethroughConverter: func(text string, _ format.Context) string {
|
||||
return fmt.Sprintf("~%s~", text)
|
||||
},
|
||||
MonospaceConverter: func(text string, _ format.Context) string {
|
||||
return fmt.Sprintf("```%s```", text)
|
||||
},
|
||||
MonospaceBlockConverter: func(text, language string, _ format.Context) string {
|
||||
return fmt.Sprintf("```%s```", text)
|
||||
},
|
||||
BoldConverter: func(text string, _ format.Context) string { return fmt.Sprintf("*%s*", text) },
|
||||
ItalicConverter: func(text string, _ format.Context) string { return fmt.Sprintf("_%s_", text) },
|
||||
StrikethroughConverter: func(text string, _ format.Context) string { return fmt.Sprintf("~%s~", text) },
|
||||
MonospaceConverter: func(text string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
|
||||
MonospaceBlockConverter: func(text, language string, _ format.Context) string { return fmt.Sprintf("```%s```", text) },
|
||||
},
|
||||
waReplString: map[*regexp.Regexp]string{
|
||||
italicRegex: "$1<em>$2</em>$3",
|
||||
|
@ -99,12 +89,11 @@ func NewFormatter(bridge *Bridge) *Formatter {
|
|||
return fmt.Sprintf("<code>%s</code>", str)
|
||||
},
|
||||
}
|
||||
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{
|
||||
}
|
||||
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{}
|
||||
return formatter
|
||||
}
|
||||
|
||||
func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID, displayname string) {
|
||||
func (formatter *Formatter) getMatrixInfoByJID(jid types.JID) (mxid id.UserID, displayname string) {
|
||||
if user := formatter.bridge.GetUserByJID(jid); user != nil {
|
||||
mxid = user.MXID
|
||||
displayname = string(user.MXID)
|
||||
|
@ -115,7 +104,7 @@ func (formatter *Formatter) getMatrixInfoByJID(jid whatsapp.JID) (mxid id.UserID
|
|||
return
|
||||
}
|
||||
|
||||
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []whatsapp.JID) {
|
||||
func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []string) {
|
||||
output := html.EscapeString(content.Body)
|
||||
for regex, replacement := range formatter.waReplString {
|
||||
output = regex.ReplaceAllString(output, replacement)
|
||||
|
@ -123,14 +112,20 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
|
|||
for regex, replacer := range formatter.waReplFunc {
|
||||
output = regex.ReplaceAllStringFunc(output, replacer)
|
||||
}
|
||||
for _, jid := range mentionedJIDs {
|
||||
for _, rawJID := range mentionedJIDs {
|
||||
jid, err := types.ParseJID(rawJID)
|
||||
if err != nil {
|
||||
continue
|
||||
} else if jid.Server == types.LegacyUserServer {
|
||||
jid.Server = types.DefaultUserServer
|
||||
}
|
||||
mxid, displayname := formatter.getMatrixInfoByJID(jid)
|
||||
number := "@" + strings.Replace(jid, whatsapp.NewUserSuffix, "", 1)
|
||||
output = strings.Replace(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname), -1)
|
||||
content.Body = strings.Replace(content.Body, number, displayname, -1)
|
||||
number := "@" + jid.User
|
||||
output = strings.ReplaceAll(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname))
|
||||
content.Body = strings.ReplaceAll(content.Body, number, displayname)
|
||||
}
|
||||
if output != content.Body {
|
||||
output = strings.Replace(output, "\n", "<br/>", -1)
|
||||
output = strings.ReplaceAll(output, "\n", "<br/>")
|
||||
content.FormattedBody = output
|
||||
content.Format = event.FormatHTML
|
||||
for regex, replacer := range formatter.waReplFuncText {
|
||||
|
@ -139,9 +134,9 @@ func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, me
|
|||
}
|
||||
}
|
||||
|
||||
func (formatter *Formatter) ParseMatrix(html string) (string, []whatsapp.JID) {
|
||||
func (formatter *Formatter) ParseMatrix(html string) (string, []string) {
|
||||
ctx := make(format.Context)
|
||||
result := formatter.matrixHTMLParser.Parse(html, ctx)
|
||||
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]whatsapp.JID)
|
||||
mentionedJIDs, _ := ctx[mentionedJIDsContextKey].([]string)
|
||||
return result, mentionedJIDs
|
||||
}
|
||||
|
|
35
go.mod
35
go.mod
|
@ -1,19 +1,40 @@
|
|||
module maunium.net/go/mautrix-whatsapp
|
||||
|
||||
go 1.14
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/Rhymen/go-whatsapp v0.1.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/lib/pq v1.10.2
|
||||
github.com/mattn/go-sqlite3 v1.14.8
|
||||
github.com/lib/pq v1.10.3
|
||||
github.com/mattn/go-sqlite3 v1.14.9
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
maunium.net/go/mauflag v1.0.0
|
||||
maunium.net/go/maulogger/v2 v2.3.0
|
||||
maunium.net/go/mautrix v0.9.27
|
||||
maunium.net/go/maulogger/v2 v2.3.1
|
||||
maunium.net/go/mautrix v0.9.31
|
||||
)
|
||||
|
||||
replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.5.12
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/btcsuite/btcutil v1.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/golang/protobuf v1.5.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/gjson v1.10.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/sjson v1.2.3 // indirect
|
||||
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
|
||||
)
|
||||
|
|
69
go.sum
69
go.sum
|
@ -1,4 +1,6 @@
|
|||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
@ -44,9 +46,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
|||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
|
@ -76,12 +77,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
|
||||
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -128,26 +127,27 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
|
||||
github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
|
||||
github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
|
||||
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
|
||||
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
|
||||
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
|
||||
github.com/tulir/go-whatsapp v0.5.12 h1:JGU5yhoh+CyDcSMUilwy7FL0gFo0zqqepsHRqEjrjKc=
|
||||
github.com/tulir/go-whatsapp v0.5.12/go.mod h1:7J3IIL3bEQiBJGtiZst1N4PgXHlWIartdVQLe6lcx9A=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
|
||||
github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
|
||||
github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
|
||||
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
|
||||
go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
|
||||
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c h1:ZmmT3L8pMKLW3JhcP6Rt0dJg09N+20a8fROxr8MUKzg=
|
||||
go.mau.fi/whatsmeow v0.0.0-20211029221633-b2fb3fda9a8c/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
|
||||
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -157,9 +157,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -172,16 +172,16 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
@ -198,8 +198,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
|||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
@ -217,8 +217,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||
maunium.net/go/maulogger/v2 v2.3.0 h1:TMCcO65fLk6+pJXo7sl38tzjzW0KBFgc6JWJMBJp4GE=
|
||||
maunium.net/go/maulogger/v2 v2.3.0/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||
maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is=
|
||||
maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
|
||||
maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=
|
||||
maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
|
||||
maunium.net/go/mautrix v0.9.31 h1:n7UF5tqq2zCyfdNsv++RyQ2anjjrFVOmOA2VkZCSgZc=
|
||||
maunium.net/go/mautrix v0.9.31/go.mod h1:3U7pOAx4bxdIVJuunLDAToI+M7YwxcGMm74zBmX5aY0=
|
||||
|
|
92
main.go
92
main.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -21,12 +21,18 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
flag "maunium.net/go/mauflag"
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
@ -41,21 +47,29 @@ import (
|
|||
"maunium.net/go/mautrix-whatsapp/database/upgrades"
|
||||
)
|
||||
|
||||
// The name and repo URL of the bridge.
|
||||
var (
|
||||
// These are static
|
||||
Name = "mautrix-whatsapp"
|
||||
URL = "https://github.com/mautrix/whatsapp"
|
||||
// This is changed when making a release
|
||||
Version = "0.1.9"
|
||||
// This is filled by init()
|
||||
WAVersion = ""
|
||||
VersionString = ""
|
||||
// These are filled at build time with the -X linker flag
|
||||
)
|
||||
|
||||
// Information to find out exactly which commit the bridge was built from.
|
||||
// These are filled at build time with the -X linker flag.
|
||||
var (
|
||||
Tag = "unknown"
|
||||
Commit = "unknown"
|
||||
BuildTime = "unknown"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version is the version number of the bridge. Changed manually when making a release.
|
||||
Version = "0.2.0+dev"
|
||||
// WAVersion is the version number exposed to WhatsApp. Filled in init()
|
||||
WAVersion = ""
|
||||
// VersionString is the bridge version, plus commit information. Filled in init() using the build-time values.
|
||||
VersionString = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
if len(Tag) > 0 && Tag[0] == 'v' {
|
||||
Tag = Tag[1:]
|
||||
|
@ -145,19 +159,19 @@ type Bridge struct {
|
|||
Provisioning *ProvisioningAPI
|
||||
Bot *appservice.IntentAPI
|
||||
Formatter *Formatter
|
||||
Relaybot *User
|
||||
Crypto Crypto
|
||||
Metrics *MetricsHandler
|
||||
WAContainer *sqlstore.Container
|
||||
|
||||
usersByMXID map[id.UserID]*User
|
||||
usersByJID map[whatsapp.JID]*User
|
||||
usersByUsername map[string]*User
|
||||
usersLock sync.Mutex
|
||||
managementRooms map[id.RoomID]*User
|
||||
managementRoomsLock sync.Mutex
|
||||
portalsByMXID map[id.RoomID]*Portal
|
||||
portalsByJID map[database.PortalKey]*Portal
|
||||
portalsLock sync.Mutex
|
||||
puppets map[whatsapp.JID]*Puppet
|
||||
puppets map[types.JID]*Puppet
|
||||
puppetsByCustomMXID map[id.UserID]*Puppet
|
||||
puppetsLock sync.Mutex
|
||||
}
|
||||
|
@ -176,11 +190,11 @@ type Crypto interface {
|
|||
func NewBridge() *Bridge {
|
||||
bridge := &Bridge{
|
||||
usersByMXID: make(map[id.UserID]*User),
|
||||
usersByJID: make(map[whatsapp.JID]*User),
|
||||
usersByUsername: make(map[string]*User),
|
||||
managementRooms: make(map[id.RoomID]*User),
|
||||
portalsByMXID: make(map[id.RoomID]*Portal),
|
||||
portalsByJID: make(map[database.PortalKey]*Portal),
|
||||
puppets: make(map[whatsapp.JID]*Puppet),
|
||||
puppets: make(map[types.JID]*Puppet),
|
||||
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
||||
}
|
||||
|
||||
|
@ -259,6 +273,8 @@ func (bridge *Bridge) Init() {
|
|||
bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
|
||||
bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
|
||||
|
||||
bridge.WAContainer = sqlstore.NewWithDB(bridge.DB.DB, bridge.Config.AppService.Database.Type, nil)
|
||||
|
||||
ss := bridge.Config.AppService.Provisioning.SharedSecret
|
||||
if len(ss) > 0 && ss != "disable" {
|
||||
bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
|
||||
|
@ -271,6 +287,23 @@ func (bridge *Bridge) Init() {
|
|||
bridge.Formatter = NewFormatter(bridge)
|
||||
bridge.Crypto = NewCryptoHelper(bridge)
|
||||
bridge.Metrics = NewMetricsHandler(bridge.Config.Metrics.Listen, bridge.Log.Sub("Metrics"), bridge.DB)
|
||||
|
||||
store.BaseClientPayload.UserAgent.OsVersion = proto.String(WAVersion)
|
||||
store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(WAVersion)
|
||||
store.CompanionProps.Os = proto.String(bridge.Config.WhatsApp.OSName)
|
||||
versionParts := strings.Split(WAVersion, ".")
|
||||
if len(versionParts) > 2 {
|
||||
primary, _ := strconv.Atoi(versionParts[0])
|
||||
secondary, _ := strconv.Atoi(versionParts[1])
|
||||
tertiary, _ := strconv.Atoi(versionParts[2])
|
||||
store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
|
||||
store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
|
||||
store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
|
||||
}
|
||||
platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(bridge.Config.WhatsApp.BrowserName)]
|
||||
if ok {
|
||||
store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *Bridge) Start() {
|
||||
|
@ -289,12 +322,10 @@ func (bridge *Bridge) Start() {
|
|||
os.Exit(19)
|
||||
}
|
||||
}
|
||||
bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateStarting}.fill(nil))
|
||||
if bridge.Provisioning != nil {
|
||||
bridge.Log.Debugln("Initializing provisioning API")
|
||||
bridge.Provisioning.Init()
|
||||
}
|
||||
bridge.LoadRelaybot()
|
||||
bridge.Log.Debugln("Starting application service HTTP server")
|
||||
go bridge.AS.Start()
|
||||
bridge.Log.Debugln("Starting event processor")
|
||||
|
@ -327,21 +358,6 @@ func (bridge *Bridge) ResendBridgeInfo() {
|
|||
bridge.Log.Infoln("Finished re-sending bridge info state events")
|
||||
}
|
||||
|
||||
func (bridge *Bridge) LoadRelaybot() {
|
||||
if !bridge.Config.Bridge.Relaybot.Enabled {
|
||||
return
|
||||
}
|
||||
bridge.Relaybot = bridge.GetUserByMXID("relaybot")
|
||||
if bridge.Relaybot.HasSession() {
|
||||
bridge.Log.Debugln("Relaybot is enabled")
|
||||
} else {
|
||||
bridge.Log.Debugln("Relaybot is enabled, but not logged in")
|
||||
}
|
||||
bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
|
||||
bridge.Relaybot.IsRelaybot = true
|
||||
bridge.Relaybot.Connect(false)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) UpdateBotProfile() {
|
||||
bridge.Log.Debugln("Updating bot profile")
|
||||
botConfig := bridge.Config.AppService.Bot
|
||||
|
@ -374,10 +390,10 @@ func (bridge *Bridge) StartUsers() {
|
|||
bridge.Log.Debugln("Starting users")
|
||||
foundAnySessions := false
|
||||
for _, user := range bridge.GetAllUsers() {
|
||||
if user.Session != nil {
|
||||
if !user.JID.IsEmpty() {
|
||||
foundAnySessions = true
|
||||
}
|
||||
go user.Connect(false)
|
||||
go user.Connect()
|
||||
}
|
||||
if !foundAnySessions {
|
||||
bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))
|
||||
|
@ -401,15 +417,13 @@ func (bridge *Bridge) Stop() {
|
|||
bridge.AS.Stop()
|
||||
bridge.Metrics.Stop()
|
||||
bridge.EventProcessor.Stop()
|
||||
for _, user := range bridge.usersByJID {
|
||||
if user.Conn == nil {
|
||||
for _, user := range bridge.usersByUsername {
|
||||
if user.Client == nil {
|
||||
continue
|
||||
}
|
||||
bridge.Log.Debugln("Disconnecting", user.MXID)
|
||||
err := user.Conn.Disconnect()
|
||||
if err != nil {
|
||||
bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
|
||||
}
|
||||
user.Client.Disconnect()
|
||||
close(user.historySyncs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
41
matrix.go
41
matrix.go
|
@ -121,29 +121,9 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
|
|||
return
|
||||
}
|
||||
|
||||
if evt.RoomID == mx.bridge.Config.Bridge.Relaybot.ManagementRoom {
|
||||
_, _ = intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.")
|
||||
mx.log.Debugln("Joined relaybot management room", evt.RoomID, "after invite from", evt.Sender)
|
||||
return
|
||||
}
|
||||
|
||||
hasPuppets := false
|
||||
for mxid, _ := range members.Joined {
|
||||
if mxid == intent.UserID || mxid == evt.Sender {
|
||||
continue
|
||||
} else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok {
|
||||
hasPuppets = true
|
||||
continue
|
||||
}
|
||||
mx.log.Debugln("Leaving multi-user room", evt.RoomID, "after accepting invite from", evt.Sender)
|
||||
_, _ = intent.SendNotice(evt.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.")
|
||||
_, _ = intent.LeaveRoom(evt.RoomID)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = mx.sendNoticeWithMarkdown(evt.RoomID, mx.bridge.Config.Bridge.ManagementRoomText.Welcome)
|
||||
|
||||
if !hasPuppets && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
|
||||
if len(members.Joined) == 2 && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) {
|
||||
user.SetManagementRoom(evt.RoomID)
|
||||
_, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room.")
|
||||
mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender)
|
||||
|
@ -223,13 +203,10 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
|
|||
portal.UpdateBridgeInfo()
|
||||
_, _ = intent.SendNotice(roomID, "Private chat portal created")
|
||||
|
||||
err := portal.FillInitialHistory(inviter)
|
||||
if err != nil {
|
||||
portal.log.Errorln("Failed to fill history:", err)
|
||||
}
|
||||
|
||||
inviter.addPortalToCommunity(portal)
|
||||
inviter.addPuppetToCommunity(puppet)
|
||||
//err := portal.FillInitialHistory(inviter)
|
||||
//if err != nil {
|
||||
// portal.log.Errorln("Failed to fill history:", err)
|
||||
//}
|
||||
}
|
||||
|
||||
func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
|
||||
|
@ -281,7 +258,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
|
|||
}
|
||||
|
||||
user := mx.bridge.GetUserByMXID(evt.Sender)
|
||||
if user == nil || !user.Whitelisted || !user.IsConnected() {
|
||||
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -322,7 +299,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
|
|||
}
|
||||
|
||||
user := mx.bridge.GetUserByMXID(evt.Sender)
|
||||
if user == nil || !user.Whitelisted || !user.IsConnected() {
|
||||
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -343,7 +320,7 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
|
|||
return true
|
||||
}
|
||||
user := mx.bridge.GetUserByMXID(evt.Sender)
|
||||
if !user.RelaybotWhitelisted {
|
||||
if !user.RelayWhitelisted {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -461,7 +438,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
|
|||
|
||||
if !user.HasSession() {
|
||||
return
|
||||
} else if !user.IsConnected() {
|
||||
} else if !user.IsLoggedIn() {
|
||||
msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+
|
||||
"You are not connected to WhatsApp, so your redaction was not bridged. "+
|
||||
"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)
|
||||
|
|
74
metrics.go
74
metrics.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
@ -59,16 +59,12 @@ type MetricsHandler struct {
|
|||
unencryptedGroupCount prometheus.Gauge
|
||||
unencryptedPrivateCount prometheus.Gauge
|
||||
|
||||
connected prometheus.Gauge
|
||||
connectedState map[whatsapp.JID]bool
|
||||
connectedStateLock sync.Mutex
|
||||
loggedIn prometheus.Gauge
|
||||
loggedInState map[whatsapp.JID]bool
|
||||
loggedInStateLock sync.Mutex
|
||||
syncLocked prometheus.Gauge
|
||||
syncLockedState map[whatsapp.JID]bool
|
||||
syncLockedStateLock sync.Mutex
|
||||
bufferLength *prometheus.GaugeVec
|
||||
connected prometheus.Gauge
|
||||
connectedState map[string]bool
|
||||
connectedStateLock sync.Mutex
|
||||
loggedIn prometheus.Gauge
|
||||
loggedInState map[string]bool
|
||||
loggedInStateLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewMetricsHandler(address string, log log.Logger, db *database.Database) *MetricsHandler {
|
||||
|
@ -125,21 +121,12 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
|
|||
Name: "bridge_logged_in",
|
||||
Help: "Users logged into the bridge",
|
||||
}),
|
||||
loggedInState: make(map[whatsapp.JID]bool),
|
||||
loggedInState: make(map[string]bool),
|
||||
connected: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "bridge_connected",
|
||||
Help: "Bridge users connected to WhatsApp",
|
||||
}),
|
||||
connectedState: make(map[whatsapp.JID]bool),
|
||||
syncLocked: promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "bridge_sync_locked",
|
||||
Help: "Bridge users locked in post-login sync",
|
||||
}),
|
||||
syncLockedState: make(map[whatsapp.JID]bool),
|
||||
bufferLength: promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "bridge_buffer_size",
|
||||
Help: "Number of messages in buffer",
|
||||
}, []string{"user_id"}),
|
||||
connectedState: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,7 +145,7 @@ func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
|
|||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType string) func() {
|
||||
func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp time.Time, messageType string) func() {
|
||||
if !mh.running {
|
||||
return noop
|
||||
}
|
||||
|
@ -169,7 +156,7 @@ func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType str
|
|||
mh.whatsappMessageHandling.
|
||||
With(prometheus.Labels{"message_type": messageType}).
|
||||
Observe(duration.Seconds())
|
||||
mh.whatsappMessageAge.Observe(time.Now().Sub(time.Unix(int64(timestamp), 0)).Seconds())
|
||||
mh.whatsappMessageAge.Observe(time.Now().Sub(timestamp).Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,15 +167,15 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
|
|||
mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
|
||||
func (mh *MetricsHandler) TrackLoginState(jid types.JID, loggedIn bool) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.loggedInStateLock.Lock()
|
||||
defer mh.loggedInStateLock.Unlock()
|
||||
currentVal, ok := mh.loggedInState[jid]
|
||||
currentVal, ok := mh.loggedInState[jid.User]
|
||||
if !ok || currentVal != loggedIn {
|
||||
mh.loggedInState[jid] = loggedIn
|
||||
mh.loggedInState[jid.User] = loggedIn
|
||||
if loggedIn {
|
||||
mh.loggedIn.Inc()
|
||||
} else {
|
||||
|
@ -197,16 +184,15 @@ func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool) {
|
||||
func (mh *MetricsHandler) TrackConnectionState(jid types.JID, connected bool) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
|
||||
mh.connectedStateLock.Lock()
|
||||
defer mh.connectedStateLock.Unlock()
|
||||
currentVal, ok := mh.connectedState[jid]
|
||||
currentVal, ok := mh.connectedState[jid.User]
|
||||
if !ok || currentVal != connected {
|
||||
mh.connectedState[jid] = connected
|
||||
mh.connectedState[jid.User] = connected
|
||||
if connected {
|
||||
mh.connected.Inc()
|
||||
} else {
|
||||
|
@ -215,30 +201,6 @@ func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool)
|
|||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.syncLockedStateLock.Lock()
|
||||
defer mh.syncLockedStateLock.Unlock()
|
||||
currentVal, ok := mh.syncLockedState[jid]
|
||||
if !ok || currentVal != locked {
|
||||
mh.syncLockedState[jid] = locked
|
||||
if locked {
|
||||
mh.syncLocked.Inc()
|
||||
} else {
|
||||
mh.syncLocked.Dec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) TrackBufferLength(id id.UserID, length int) {
|
||||
if !mh.running {
|
||||
return
|
||||
}
|
||||
mh.bufferLength.With(prometheus.Labels{"user_id": string(id)}).Set(float64(length))
|
||||
}
|
||||
|
||||
func (mh *MetricsHandler) updateStats() {
|
||||
start := time.Now()
|
||||
var puppetCount int
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !cgo || nocrypto
|
||||
// +build !cgo nocrypto
|
||||
|
||||
package main
|
||||
|
|
279
provisioning.go
279
provisioning.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -29,7 +29,7 @@ import (
|
|||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
|
@ -50,11 +50,13 @@ func (prov *ProvisioningAPI) Init() {
|
|||
r.HandleFunc("/login", prov.Login).Methods(http.MethodGet)
|
||||
r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost)
|
||||
r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost)
|
||||
r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost)
|
||||
r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost)
|
||||
r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost)
|
||||
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost)
|
||||
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost)
|
||||
|
||||
// Deprecated, just use /disconnect
|
||||
r.HandleFunc("/delete_connection", prov.Disconnect).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
type responseWrap struct {
|
||||
|
@ -122,7 +124,7 @@ type Response struct {
|
|||
|
||||
func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(*User)
|
||||
if user.Session == nil && user.Conn == nil {
|
||||
if user.Session == nil && user.Client == nil {
|
||||
jsonResponse(w, http.StatusNotFound, Error{
|
||||
Error: "Nothing to purge: no session information stored and no active connection.",
|
||||
ErrCode: "no session",
|
||||
|
@ -130,128 +132,43 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
user.DeleteConnection()
|
||||
user.SetSession(nil)
|
||||
user.DeleteSession()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(*User)
|
||||
if user.Conn == nil {
|
||||
jsonResponse(w, http.StatusNotFound, Error{
|
||||
Error: "You don't have a WhatsApp connection.",
|
||||
ErrCode: "not connected",
|
||||
})
|
||||
return
|
||||
}
|
||||
user.DeleteConnection()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"})
|
||||
user.removeFromJIDMap(StateLoggedOut)
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(*User)
|
||||
if user.Conn == nil {
|
||||
if user.Client == nil {
|
||||
jsonResponse(w, http.StatusNotFound, Error{
|
||||
Error: "You don't have a WhatsApp connection.",
|
||||
ErrCode: "no connection",
|
||||
})
|
||||
return
|
||||
}
|
||||
err := user.Conn.Disconnect()
|
||||
if err == whatsapp.ErrNotConnected {
|
||||
jsonResponse(w, http.StatusNotFound, Error{
|
||||
Error: "You were not connected",
|
||||
ErrCode: "not connected",
|
||||
})
|
||||
return
|
||||
} else if err != nil {
|
||||
user.log.Warnln("Error while disconnecting:", err)
|
||||
jsonResponse(w, http.StatusInternalServerError, Error{
|
||||
Error: fmt.Sprintf("Unknown error while disconnecting: %v", err),
|
||||
ErrCode: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
user.DeleteConnection()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value("user").(*User)
|
||||
if user.Conn == nil {
|
||||
if user.Client == nil {
|
||||
if user.Session == nil {
|
||||
jsonResponse(w, http.StatusForbidden, Error{
|
||||
Error: "No existing connection and no session. Please log in first.",
|
||||
ErrCode: "no session",
|
||||
})
|
||||
} else {
|
||||
user.Connect(false)
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."})
|
||||
user.Connect()
|
||||
jsonResponse(w, http.StatusAccepted, Response{true, "Created connection to WhatsApp."})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
user.log.Debugln("Received /reconnect request, disconnecting")
|
||||
wasConnected := true
|
||||
err := user.Conn.Disconnect()
|
||||
if err == whatsapp.ErrNotConnected {
|
||||
wasConnected = false
|
||||
} else if err != nil {
|
||||
user.log.Warnln("Error while disconnecting:", err)
|
||||
}
|
||||
|
||||
user.log.Debugln("Restoring session for /reconnect")
|
||||
err = user.Conn.Restore(true, r.Context())
|
||||
user.log.Debugfln("Restore session for /reconnect responded with %v", err)
|
||||
if err == whatsapp.ErrInvalidSession {
|
||||
if user.Session != nil {
|
||||
user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
|
||||
user.Conn.SetSession(*user.Session)
|
||||
err = user.Conn.Restore(true, r.Context())
|
||||
} else {
|
||||
jsonResponse(w, http.StatusForbidden, Error{
|
||||
Error: "You're not logged in",
|
||||
ErrCode: "not logged in",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err == whatsapp.ErrLoginInProgress {
|
||||
jsonResponse(w, http.StatusConflict, Error{
|
||||
Error: "A login or reconnection is already in progress.",
|
||||
ErrCode: "login in progress",
|
||||
})
|
||||
return
|
||||
} else if err == whatsapp.ErrAlreadyLoggedIn {
|
||||
jsonResponse(w, http.StatusConflict, Error{
|
||||
Error: "You were already connected.",
|
||||
ErrCode: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
user.log.Warnln("Error while reconnecting:", err)
|
||||
jsonResponse(w, http.StatusInternalServerError, Error{
|
||||
Error: fmt.Sprintf("Unknown error while reconnecting: %v", err),
|
||||
ErrCode: err.Error(),
|
||||
})
|
||||
user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
|
||||
err = user.Conn.Disconnect()
|
||||
if err != nil {
|
||||
user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
user.ConnectionErrors = 0
|
||||
user.PostLogin()
|
||||
|
||||
var msg string
|
||||
if wasConnected {
|
||||
msg = "Reconnected successfully."
|
||||
} else {
|
||||
msg = "Connected successfully."
|
||||
user.DeleteConnection()
|
||||
user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WANotConnected})
|
||||
user.Connect()
|
||||
jsonResponse(w, http.StatusAccepted, Response{true, "Restarted connection to WhatsApp"})
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, Response{true, msg})
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -259,39 +176,23 @@ func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
|
|||
wa := map[string]interface{}{
|
||||
"has_session": user.Session != nil,
|
||||
"management_room": user.ManagementRoom,
|
||||
"jid": user.JID,
|
||||
"conn": nil,
|
||||
"ping": nil,
|
||||
}
|
||||
if user.Conn != nil {
|
||||
if user.JID.IsEmpty() {
|
||||
wa["jid"] = user.JID.String()
|
||||
}
|
||||
if user.Client != nil {
|
||||
wa["conn"] = map[string]interface{}{
|
||||
"is_connected": user.Conn.IsConnected(),
|
||||
"is_logged_in": user.Conn.IsLoggedIn(),
|
||||
"is_login_in_progress": user.Conn.IsLoginInProgress(),
|
||||
"is_connected": user.Client.IsConnected(),
|
||||
"is_logged_in": user.Client.IsLoggedIn,
|
||||
}
|
||||
user.log.Debugln("Pinging WhatsApp mobile due to /ping API request")
|
||||
err := user.Conn.AdminTest()
|
||||
var errStr string
|
||||
if err == whatsapp.ErrPingFalse {
|
||||
user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
|
||||
go user.HandleError(err)
|
||||
}
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
wa["ping"] = map[string]interface{}{
|
||||
"ok": err == nil,
|
||||
"err": errStr,
|
||||
}
|
||||
user.log.Debugfln("Admin test response for /ping: %v (conn: %t, login: %t, in progress: %t)",
|
||||
err, user.Conn.IsConnected(), user.Conn.IsLoggedIn(), user.Conn.IsLoginInProgress())
|
||||
}
|
||||
resp := map[string]interface{}{
|
||||
"mxid": user.MXID,
|
||||
"admin": user.Admin,
|
||||
"whitelisted": user.Whitelisted,
|
||||
"relaybot_whitelisted": user.RelaybotWhitelisted,
|
||||
"whatsapp": wa,
|
||||
"mxid": user.MXID,
|
||||
"admin": user.Admin,
|
||||
"whitelisted": user.Whitelisted,
|
||||
"relay_whitelisted": user.RelayWhitelisted,
|
||||
"whatsapp": wa,
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, resp)
|
||||
}
|
||||
|
@ -314,7 +215,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
force := strings.ToLower(r.URL.Query().Get("force")) != "false"
|
||||
|
||||
if user.Conn == nil {
|
||||
if user.Client == nil {
|
||||
if !force {
|
||||
jsonResponse(w, http.StatusNotFound, Error{
|
||||
Error: "You're not connected",
|
||||
|
@ -322,7 +223,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
} else {
|
||||
err := user.Conn.Logout()
|
||||
err := user.Client.Logout()
|
||||
if err != nil {
|
||||
user.log.Warnln("Error while logging out:", err)
|
||||
if !force {
|
||||
|
@ -332,16 +233,15 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user.Session = nil
|
||||
}
|
||||
user.DeleteConnection()
|
||||
}
|
||||
|
||||
user.bridge.Metrics.TrackConnectionState(user.JID, false)
|
||||
user.removeFromJIDMap(StateLoggedOut)
|
||||
|
||||
// TODO this causes a foreign key violation, which should be fixed
|
||||
//ce.User.JID = ""
|
||||
user.SetSession(nil)
|
||||
user.DeleteSession()
|
||||
jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
|
||||
}
|
||||
|
||||
|
@ -361,26 +261,10 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
|
|||
prov.log.Errorln("Failed to upgrade connection to websocket:", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if !user.Connect(true) {
|
||||
user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "Failed to connect to WhatsApp",
|
||||
ErrCode: "connection error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
qrChan := make(chan string, 3)
|
||||
go func() {
|
||||
for code := range qrChan {
|
||||
if code == "stop" {
|
||||
return
|
||||
}
|
||||
_ = c.WriteJSON(map[string]interface{}{
|
||||
"code": code,
|
||||
})
|
||||
defer func() {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
user.log.Debugln("Error closing websocket:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -400,40 +284,63 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
|
|||
return nil
|
||||
})
|
||||
|
||||
user.log.Debugln("Starting login via provisioning API")
|
||||
session, jid, err := user.Conn.Login(qrChan, ctx)
|
||||
qrChan <- "stop"
|
||||
qrChan, err := user.Login(ctx)
|
||||
if err != nil {
|
||||
var msg string
|
||||
if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
|
||||
msg = "You're already logged in"
|
||||
} else if errors.Is(err, whatsapp.ErrLoginInProgress) {
|
||||
msg = "You have a login in progress already."
|
||||
} else if errors.Is(err, whatsapp.ErrLoginTimedOut) {
|
||||
msg = "QR code scan timed out. Please try again."
|
||||
} else if errors.Is(err, whatsapp.ErrInvalidWebsocket) {
|
||||
msg = "WhatsApp connection error. Please try again."
|
||||
// TODO might need to make sure it reconnects?
|
||||
} else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
|
||||
msg = "WhatsApp multi-device is not currently supported. Please disable it and try again."
|
||||
user.log.Errorf("Failed to log in from provisioning API:", err)
|
||||
if errors.Is(err, ErrAlreadyLoggedIn) {
|
||||
go user.Connect()
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "You're already logged into WhatsApp",
|
||||
ErrCode: "already logged in",
|
||||
})
|
||||
} else {
|
||||
msg = fmt.Sprintf("Unknown error while logging in: %v", err)
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "Failed to connect to WhatsApp",
|
||||
ErrCode: "connection error",
|
||||
})
|
||||
}
|
||||
}
|
||||
user.log.Debugln("Started login via provisioning API")
|
||||
|
||||
for {
|
||||
select {
|
||||
case evt := <-qrChan:
|
||||
switch evt {
|
||||
case whatsmeow.QRChannelSuccess:
|
||||
jid := user.Client.Store.ID
|
||||
user.log.Debugln("Successful login as", jid, "via provisioning API")
|
||||
_ = c.WriteJSON(map[string]interface{}{
|
||||
"success": true,
|
||||
"jid": jid,
|
||||
"phone": fmt.Sprintf("+%s", jid.User),
|
||||
})
|
||||
case whatsmeow.QRChannelTimeout:
|
||||
user.log.Debugln("Login via provisioning API timed out")
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "QR code scan timed out. Please try again.",
|
||||
ErrCode: "login timed out",
|
||||
})
|
||||
case whatsmeow.QRChannelErrUnexpectedEvent:
|
||||
user.log.Debugln("Login via provisioning API failed due to unexpected event")
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "Got unexpected event while waiting for QRs, perhaps you're already logged in?",
|
||||
ErrCode: "unexpected event",
|
||||
})
|
||||
case whatsmeow.QRChannelScannedWithoutMultidevice:
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: "Please enable the WhatsApp multidevice beta and scan the QR code again.",
|
||||
ErrCode: "multidevice not enabled",
|
||||
})
|
||||
continue
|
||||
default:
|
||||
_ = c.WriteJSON(map[string]interface{}{
|
||||
"code": string(evt),
|
||||
})
|
||||
continue
|
||||
}
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
user.log.Warnln("Failed to log in:", err)
|
||||
_ = c.WriteJSON(Error{
|
||||
Error: msg,
|
||||
ErrCode: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
user.log.Debugln("Successful login as", jid, "via provisioning API")
|
||||
user.ConnectionErrors = 0
|
||||
user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
|
||||
user.addToJIDMap()
|
||||
user.SetSession(&session)
|
||||
_ = c.WriteJSON(map[string]interface{}{
|
||||
"success": true,
|
||||
"jid": user.JID,
|
||||
})
|
||||
user.PostLogin()
|
||||
}
|
||||
|
|
156
puppet.go
156
puppet.go
|
@ -1,5 +1,5 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
// Copyright (C) 2021 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
|
@ -17,15 +17,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
log "maunium.net/go/maulogger/v2"
|
||||
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
|
@ -34,19 +38,18 @@ import (
|
|||
|
||||
var userIDRegex *regexp.Regexp
|
||||
|
||||
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (whatsapp.JID, bool) {
|
||||
func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (jid types.JID, ok bool) {
|
||||
if userIDRegex == nil {
|
||||
userIDRegex = regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
|
||||
bridge.Config.Bridge.FormatUsername("([0-9]+)"),
|
||||
bridge.Config.Homeserver.Domain))
|
||||
}
|
||||
match := userIDRegex.FindStringSubmatch(string(mxid))
|
||||
if match == nil || len(match) != 2 {
|
||||
return "", false
|
||||
if len(match) == 2 {
|
||||
jid = types.NewJID(match[1], types.DefaultUserServer)
|
||||
ok = true
|
||||
}
|
||||
|
||||
jid := whatsapp.JID(match[1] + whatsapp.NewUserSuffix)
|
||||
return jid, true
|
||||
return
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
|
||||
|
@ -58,7 +61,13 @@ func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
|
|||
return bridge.GetPuppetByJID(jid)
|
||||
}
|
||||
|
||||
func (bridge *Bridge) GetPuppetByJID(jid whatsapp.JID) *Puppet {
|
||||
func (bridge *Bridge) GetPuppetByJID(jid types.JID) *Puppet {
|
||||
jid = jid.ToNonAD()
|
||||
if jid.Server == types.LegacyUserServer {
|
||||
jid.Server = types.DefaultUserServer
|
||||
} else if jid.Server != types.DefaultUserServer {
|
||||
return nil
|
||||
}
|
||||
bridge.puppetsLock.Lock()
|
||||
defer bridge.puppetsLock.Unlock()
|
||||
puppet, ok := bridge.puppets[jid]
|
||||
|
@ -123,12 +132,9 @@ func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet
|
|||
return output
|
||||
}
|
||||
|
||||
func (bridge *Bridge) FormatPuppetMXID(jid whatsapp.JID) id.UserID {
|
||||
func (bridge *Bridge) FormatPuppetMXID(jid types.JID) id.UserID {
|
||||
return id.NewUserID(
|
||||
bridge.Config.Bridge.FormatUsername(
|
||||
strings.Replace(
|
||||
jid,
|
||||
whatsapp.NewUserSuffix, "", 1)),
|
||||
bridge.Config.Bridge.FormatUsername(jid.User),
|
||||
bridge.Config.Homeserver.Domain)
|
||||
}
|
||||
|
||||
|
@ -149,7 +155,7 @@ type Puppet struct {
|
|||
log log.Logger
|
||||
|
||||
typingIn id.RoomID
|
||||
typingAt int64
|
||||
typingAt time.Time
|
||||
|
||||
MXID id.UserID
|
||||
|
||||
|
@ -160,14 +166,8 @@ type Puppet struct {
|
|||
syncLock sync.Mutex
|
||||
}
|
||||
|
||||
func (puppet *Puppet) PhoneNumber() string {
|
||||
return strings.Replace(puppet.JID, whatsapp.NewUserSuffix, "", 1)
|
||||
}
|
||||
|
||||
func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI {
|
||||
if (!portal.IsPrivateChat() && puppet.customIntent == nil) ||
|
||||
(portal.backfilling && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling) ||
|
||||
portal.Key.JID == puppet.JID {
|
||||
if (!portal.IsPrivateChat() && puppet.customIntent == nil) || portal.Key.JID == puppet.JID {
|
||||
return puppet.DefaultIntent()
|
||||
}
|
||||
return puppet.customIntent
|
||||
|
@ -181,63 +181,64 @@ func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI {
|
|||
return puppet.bridge.AS.Intent(puppet.MXID)
|
||||
}
|
||||
|
||||
func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsapp.ProfilePicInfo) bool {
|
||||
if avatar == nil {
|
||||
var err error
|
||||
avatar, err = source.Conn.GetProfilePicThumb(puppet.JID)
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to get avatar:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if avatar.Status == 404 {
|
||||
avatar.Tag = "remove"
|
||||
avatar.Status = 0
|
||||
} else if avatar.Status == 401 && puppet.Avatar != "unauthorized" {
|
||||
puppet.Avatar = "unauthorized"
|
||||
return true
|
||||
}
|
||||
if avatar.Status != 0 || avatar.Tag == puppet.Avatar {
|
||||
return false
|
||||
}
|
||||
|
||||
if avatar.Tag == "remove" || len(avatar.URL) == 0 {
|
||||
err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{})
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to remove avatar:", err)
|
||||
}
|
||||
puppet.AvatarURL = id.ContentURI{}
|
||||
puppet.Avatar = avatar.Tag
|
||||
go puppet.updatePortalAvatar()
|
||||
return true
|
||||
}
|
||||
|
||||
data, err := avatar.DownloadBytes()
|
||||
func reuploadAvatar(intent *appservice.IntentAPI, url string) (id.ContentURI, error) {
|
||||
getResp, err := http.DefaultClient.Get(url)
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to download avatar:", err)
|
||||
return false
|
||||
return id.ContentURI{}, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
data, err := io.ReadAll(getResp.Body)
|
||||
_ = getResp.Body.Close()
|
||||
if err != nil {
|
||||
return id.ContentURI{}, fmt.Errorf("failed to read avatar bytes: %w", err)
|
||||
}
|
||||
|
||||
mime := http.DetectContentType(data)
|
||||
resp, err := puppet.DefaultIntent().UploadBytes(data, mime)
|
||||
resp, err := intent.UploadBytes(data, mime)
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to upload avatar:", err)
|
||||
return id.ContentURI{}, fmt.Errorf("failed to upload avatar to Matrix: %w", err)
|
||||
}
|
||||
return resp.ContentURI, nil
|
||||
}
|
||||
|
||||
func (puppet *Puppet) UpdateAvatar(source *User) bool {
|
||||
avatar, err := source.Client.GetProfilePictureInfo(puppet.JID, false)
|
||||
if err != nil {
|
||||
if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) {
|
||||
puppet.log.Warnln("Failed to get avatar URL:", err)
|
||||
}
|
||||
return false
|
||||
} else if avatar == nil {
|
||||
if puppet.Avatar == "remove" {
|
||||
return false
|
||||
}
|
||||
puppet.AvatarURL = id.ContentURI{}
|
||||
avatar = &types.ProfilePictureInfo{ID: "remove"}
|
||||
} else if avatar.ID == puppet.Avatar {
|
||||
return false
|
||||
} else if len(avatar.URL) == 0 {
|
||||
puppet.log.Warnln("Didn't get URL in response to avatar query")
|
||||
return false
|
||||
} else {
|
||||
url, err := reuploadAvatar(puppet.DefaultIntent(), avatar.URL)
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to reupload avatar:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
puppet.AvatarURL = url
|
||||
}
|
||||
|
||||
puppet.AvatarURL = resp.ContentURI
|
||||
err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL)
|
||||
if err != nil {
|
||||
puppet.log.Warnln("Failed to set avatar:", err)
|
||||
}
|
||||
puppet.Avatar = avatar.Tag
|
||||
puppet.Avatar = avatar.ID
|
||||
go puppet.updatePortalAvatar()
|
||||
return true
|
||||
}
|
||||
|
||||
func (puppet *Puppet) UpdateName(source *User, contact whatsapp.Contact) bool {
|
||||
newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
|
||||
func (puppet *Puppet) UpdateName(source *User, contact types.ContactInfo) bool {
|
||||
newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(puppet.JID, contact)
|
||||
if puppet.Displayname != newName && quality >= puppet.NameQuality {
|
||||
err := puppet.DefaultIntent().SetDisplayName(newName)
|
||||
if err == nil {
|
||||
|
@ -288,25 +289,21 @@ func (puppet *Puppet) updatePortalName() {
|
|||
})
|
||||
}
|
||||
|
||||
func (puppet *Puppet) SyncContactIfNecessary(source *User) {
|
||||
if len(puppet.Displayname) > 0 {
|
||||
func (puppet *Puppet) SyncContact(source *User, onlyIfNoName bool) {
|
||||
if onlyIfNoName && len(puppet.Displayname) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
source.Conn.Store.ContactsLock.RLock()
|
||||
contact, ok := source.Conn.Store.Contacts[puppet.JID]
|
||||
source.Conn.Store.ContactsLock.RUnlock()
|
||||
if !ok {
|
||||
puppet.log.Warnfln("No contact info found through %s in SyncContactIfNecessary", source.MXID)
|
||||
contact.JID = puppet.JID
|
||||
// Sync anyway to set a phone number name
|
||||
} else {
|
||||
puppet.log.Debugfln("Syncing contact info through %s / %s because puppet has no displayname", source.MXID, source.JID)
|
||||
contact, err := source.Client.Store.Contacts.GetContact(puppet.JID)
|
||||
if err != nil {
|
||||
puppet.log.Warnfln("Failed to get contact info through %s in SyncContact: %v", source.MXID)
|
||||
} else if !contact.Found {
|
||||
puppet.log.Warnfln("No contact info found through %s in SyncContact", source.MXID)
|
||||
}
|
||||
puppet.Sync(source, contact)
|
||||
}
|
||||
|
||||
func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
|
||||
func (puppet *Puppet) Sync(source *User, contact types.ContactInfo) {
|
||||
puppet.syncLock.Lock()
|
||||
defer puppet.syncLock.Unlock()
|
||||
err := puppet.DefaultIntent().EnsureRegistered()
|
||||
|
@ -314,15 +311,14 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
|
|||
puppet.log.Errorln("Failed to ensure registered:", err)
|
||||
}
|
||||
|
||||
if contact.JID == source.JID {
|
||||
contact.Notify = source.pushName
|
||||
if puppet.JID.User == source.JID.User {
|
||||
contact.PushName = source.Client.Store.PushName
|
||||
}
|
||||
|
||||
update := false
|
||||
update = puppet.UpdateName(source, contact) || update
|
||||
// TODO figure out how to update avatars after being offline
|
||||
if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync {
|
||||
update = puppet.UpdateAvatar(source, nil) || update
|
||||
update = puppet.UpdateAvatar(source) || update
|
||||
}
|
||||
if update {
|
||||
puppet.Update()
|
||||
|
|
Loading…
Reference in a new issue