Bläddra i källkod

Initial switch to go.mau.fi/whatsmeow

Tulir Asokan 3 år sedan
förälder
incheckning
56850bb698
25 ändrade filer med 1702 tillägg och 2515 borttagningar
  1. 4 5
      README.md
  2. 6 64
      bridgestate.go
  3. 355 337
      commands.go
  4. 0 132
      community.go
  5. 30 41
      config/bridge.go
  6. 2 1
      crypto.go
  7. 14 13
      custompuppet.go
  8. 6 7
      database/message.go
  9. 39 41
      database/portal.go
  10. 20 15
      database/puppet.go
  11. 13 0
      database/upgrades/2021-10-21-add-whatsmeow-store.go
  12. 87 0
      database/upgrades/2021-10-21-multidevice-updates.go
  13. 1 1
      database/upgrades/upgrades.go
  14. 119 157
      database/user.go
  15. 6 14
      example-config.yaml
  16. 27 32
      formatting.go
  17. 27 6
      go.mod
  18. 19 15
      go.sum
  19. 37 14
      main.go
  20. 7 10
      matrix.go
  21. 16 50
      metrics.go
  22. 329 410
      portal.go
  23. 173 207
      provisioning.go
  24. 72 75
      puppet.go
  25. 293 868
      user.go

+ 4 - 5
README.md

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

+ 6 - 64
bridgestate.go

@@ -20,16 +20,11 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
-	"strings"
-	"sync/atomic"
 	"time"
 
-	"github.com/Rhymen/go-whatsapp"
-
 	log "maunium.net/go/maulogger/v2"
 
 	"maunium.net/go/mautrix/id"
@@ -94,8 +89,8 @@ type GlobalBridgeState struct {
 func (pong BridgeState) fill(user *User) BridgeState {
 	if user != nil {
 		pong.UserID = user.MXID
-		pong.RemoteID = strings.TrimSuffix(user.JID, whatsapp.NewUserSuffix)
-		pong.RemoteName = fmt.Sprintf("+%s", pong.RemoteID)
+		pong.RemoteID = user.JID.String()
+		pong.RemoteName = fmt.Sprintf("+%s", user.JID.User)
 	}
 
 	pong.Timestamp = time.Now().Unix()
@@ -116,32 +111,6 @@ func (pong *BridgeState) shouldDeduplicate(newPong *BridgeState) bool {
 	return pong.Timestamp+int64(pong.TTL/5) > time.Now().Unix()
 }
 
-func (user *User) setupAdminTestHooks() {
-	if len(user.bridge.Config.Homeserver.StatusEndpoint) == 0 {
-		return
-	}
-	user.Conn.AdminTestHook = func(err error) {
-		if errors.Is(err, whatsapp.ErrConnectionTimeout) {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
-		} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
-		} else if errors.Is(err, whatsapp.ErrPingFalse) {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingFalse})
-		} else if err == nil {
-			user.sendBridgeState(BridgeState{StateEvent: StateConnected})
-		} else {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPingError})
-		}
-	}
-	user.Conn.CountTimeoutHook = func(wsKeepaliveErrorCount int) {
-		if wsKeepaliveErrorCount > 0 {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAServerTimeout})
-		} else {
-			user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WATimeout})
-		}
-	}
-}
-
 func (bridge *Bridge) createBridgeStateRequest(ctx context.Context, state *BridgeState) (req *http.Request, err error) {
 	var body bytes.Buffer
 	if err = json.NewEncoder(&body).Encode(&state); err != nil {
@@ -210,8 +179,6 @@ func (user *User) sendBridgeState(state BridgeState) {
 	}
 }
 
-var bridgeStatePingID uint32 = 0
-
 func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
 	if !prov.bridge.AS.CheckServerToken(w, r) {
 		return
@@ -221,37 +188,12 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
 	var global BridgeState
 	global.StateEvent = StateRunning
 	var remote BridgeState
-	if user.Conn != nil {
-		if user.Conn.IsConnected() && user.Conn.IsLoggedIn() {
-			pingID := atomic.AddUint32(&bridgeStatePingID, 1)
-			user.log.Debugfln("Pinging WhatsApp mobile due to bridge status /ping API request (ID %d)", pingID)
-			err := user.Conn.AdminTestWithSuppress(true)
-			if errors.Is(r.Context().Err(), context.Canceled) {
-				user.log.Warnfln("Ping request %d was canceled before we responded (response was %v)", pingID, err)
-				user.prevBridgeStatus = nil
-				return
-			}
-			user.log.Debugfln("Ping %d response: %v", pingID, err)
-			remote.StateEvent = StateTransientDisconnect
-			if err == whatsapp.ErrPingFalse {
-				user.log.Debugln("Forwarding ping false error from provisioning API to HandleError")
-				go user.HandleError(err)
-				remote.Error = WAPingFalse
-			} else if errors.Is(err, whatsapp.ErrConnectionTimeout) {
-				remote.Error = WATimeout
-			} else if errors.Is(err, whatsapp.ErrWebsocketKeepaliveFailed) {
-				remote.Error = WAServerTimeout
-			} else if err != nil {
-				remote.Error = WAPingError
-			} else {
-				remote.StateEvent = StateConnected
-			}
-		} else if user.Conn.IsLoginInProgress() && user.Session != nil {
+	if user.Client != nil && user.Client.IsConnected() {
+		if user.Client.IsLoggedIn {
+			remote.StateEvent = StateConnected
+		} else if user.Session != nil {
 			remote.StateEvent = StateConnecting
 			remote.Error = WAConnecting
-		} else if !user.Conn.IsConnected() && user.Session != nil {
-			remote.StateEvent = StateBadCredentials
-			remote.Error = WANotConnected
 		} // else: unconfigured
 	} else if user.Session != nil {
 		remote.StateEvent = StateBadCredentials

+ 355 - 337
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,12 +20,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"math"
-	"sort"
 	"strconv"
 	"strings"
+	"time"
 
-	"github.com/Rhymen/go-whatsapp"
+	"github.com/skip2/go-qrcode"
+
+	"go.mau.fi/whatsmeow/types"
+	"go.mau.fi/whatsmeow/types/events"
 
 	"maunium.net/go/maulogger/v2"
 
@@ -34,8 +36,6 @@ import (
 	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/format"
 	"maunium.net/go/mautrix/id"
-
-	"maunium.net/go/mautrix-whatsapp/database"
 )
 
 type CommandHandler struct {
@@ -119,8 +119,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
 		handler.CommandDisconnect(ce)
 	case "ping":
 		handler.CommandPing(ce)
-	case "delete-connection":
-		handler.CommandDeleteConnection(ce)
 	case "delete-session":
 		handler.CommandDeleteSession(ce)
 	case "delete-portal":
@@ -141,7 +139,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
 		if !ce.User.HasSession() {
 			ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.")
 			return
-		} else if !ce.User.IsConnected() {
+		} else if !ce.User.IsLoggedIn() {
 			ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect.")
 			return
 		}
@@ -149,8 +147,6 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
 		switch ce.Command {
 		case "login-matrix":
 			handler.CommandLoginMatrix(ce)
-		case "sync":
-			handler.CommandSync(ce)
 		case "list":
 			handler.CommandList(ce)
 		case "open":
@@ -226,12 +222,13 @@ func (handler *CommandHandler) CommandInviteLink(ce *CommandEvent) {
 		return
 	}
 
-	link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
-	if err != nil {
-		ce.Reply("Failed to get invite link: %v", err)
-		return
-	}
-	ce.Reply("%s%s", inviteLinkPrefix, link)
+	// TODO reimplement
+	//link, err := ce.User.Conn.GroupInviteLink(ce.Portal.Key.JID)
+	//if err != nil {
+	//	ce.Reply("Failed to get invite link: %v", err)
+	//	return
+	//}
+	//ce.Reply("%s%s", inviteLinkPrefix, link)
 }
 
 const cmdJoinHelp = `join <invite link> - Join a group chat with an invite link.`
@@ -246,26 +243,27 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) {
 		return
 	}
 
-	jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
-	if err != nil {
-		ce.Reply("Failed to join group: %v", err)
-		return
-	}
-
-	handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
-	portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
-	if len(portal.MXID) > 0 {
-		portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
-		ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
-	} else {
-		err = portal.CreateMatrixRoom(ce.User)
-		if err != nil {
-			ce.Reply("Failed to create portal room: %v", err)
-			return
-		}
-
-		ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
-	}
+	// TODO reimplement
+	//jid, err := ce.User.Conn.GroupAcceptInviteCode(ce.Args[0][len(inviteLinkPrefix):])
+	//if err != nil {
+	//	ce.Reply("Failed to join group: %v", err)
+	//	return
+	//}
+	//
+	//handler.log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
+	//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(jid))
+	//if len(portal.MXID) > 0 {
+	//	portal.Sync(ce.User, whatsapp.Contact{JID: portal.Key.JID})
+	//	ce.Reply("Successfully joined group \"%s\" and synced portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
+	//} else {
+	//	err = portal.CreateMatrixRoom(ce.User)
+	//	if err != nil {
+	//		ce.Reply("Failed to create portal room: %v", err)
+	//		return
+	//	}
+	//
+	//	ce.Reply("Successfully joined group \"%s\" and created portal room: [%s](https://matrix.to/#/%s)", portal.Name, portal.Name, portal.MXID)
+	//}
 }
 
 const cmdCreateHelp = `create - Create a group chat.`
@@ -299,43 +297,44 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) {
 		return
 	}
 
-	participants := []string{ce.User.JID}
+	participants := []types.JID{ce.User.JID.ToNonAD()}
 	for userID := range members.Joined {
 		jid, ok := handler.bridge.ParsePuppetMXID(userID)
-		if ok && jid != ce.User.JID {
+		if ok && jid.User != ce.User.JID.User {
 			participants = append(participants, jid)
 		}
 	}
 
-	resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
-	if err != nil {
-		ce.Reply("Failed to create group: %v", err)
-		return
-	}
-	portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
-	portal.roomCreateLock.Lock()
-	defer portal.roomCreateLock.Unlock()
-	if len(portal.MXID) != 0 {
-		portal.log.Warnln("Detected race condition in room creation")
-		// TODO race condition, clean up the old room
-	}
-	portal.MXID = ce.RoomID
-	portal.Name = roomNameEvent.Name
-	portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
-	if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
-		_, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
-		if err != nil {
-			portal.log.Warnln("Failed to enable e2be:", err)
-		}
-		portal.Encrypted = true
-	}
-
-	portal.Update()
-	portal.UpdateBridgeInfo()
-
-	ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
-	inCommunity := ce.User.addPortalToCommunity(portal)
-	ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
+	// TODO reimplement
+	//resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants)
+	//if err != nil {
+	//	ce.Reply("Failed to create group: %v", err)
+	//	return
+	//}
+	//portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID))
+	//portal.roomCreateLock.Lock()
+	//defer portal.roomCreateLock.Unlock()
+	//if len(portal.MXID) != 0 {
+	//	portal.log.Warnln("Detected race condition in room creation")
+	//	// TODO race condition, clean up the old room
+	//}
+	//portal.MXID = ce.RoomID
+	//portal.Name = roomNameEvent.Name
+	//portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1
+	//if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default {
+	//	_, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1})
+	//	if err != nil {
+	//		portal.log.Warnln("Failed to enable e2be:", err)
+	//	}
+	//	portal.Encrypted = true
+	//}
+	//
+	//portal.Update()
+	//portal.UpdateBridgeInfo()
+	//
+	//ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID)
+	//inCommunity := ce.User.addPortalToCommunity(portal)
+	//ce.User.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity})
 }
 
 const cmdSetPowerLevelHelp = `set-pl [user ID] <power level> - Change the power level in a portal room. Only for bridge admins.`
@@ -382,11 +381,108 @@ const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
 
 // CommandLogin handles login command
 func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
+	if ce.User.Session != nil {
+		ce.Reply("You're already logged in")
+		return
+	}
+	qrChan := make(chan *events.QR, 1)
+	loginChan := make(chan *events.PairSuccess, 1)
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	go ce.User.loginQrChannel(ctx, ce, qrChan, cancel)
+
+	ce.User.qrListener = qrChan
+	ce.User.loginListener = loginChan
 	if !ce.User.Connect(true) {
 		ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
 		return
 	}
-	ce.User.Login(ce)
+
+	select {
+	case success := <-loginChan:
+		ce.Reply("Successfully logged in as +%s", success.ID.User)
+		cancel()
+	case <-ctx.Done():
+		ce.Reply("Login timed out")
+	}
+}
+
+func (user *User) loginQrChannel(ctx context.Context, ce *CommandEvent, qrChan <-chan *events.QR, cancel func()) {
+	var qrEvt *events.QR
+	select {
+	case qrEvt = <-qrChan:
+	case <-ctx.Done():
+		return
+	}
+
+	bot := user.bridge.AS.BotClient()
+
+	code := qrEvt.Codes[0]
+	qrEvt.Codes = qrEvt.Codes[1:]
+	url, ok := user.uploadQR(ce, code)
+	if !ok {
+		return
+	}
+	sendResp, err := bot.SendImage(ce.RoomID, code, url)
+	if err != nil {
+		user.log.Errorln("Failed to send QR code to user:", err)
+		return
+	}
+	qrEventID := sendResp.EventID
+
+	for {
+		select {
+		case <-time.After(qrEvt.Timeout):
+			if len(qrEvt.Codes) == 0 {
+				cancel()
+				return
+			}
+			code, qrEvt.Codes = qrEvt.Codes[0], qrEvt.Codes[1:]
+
+			url, ok = user.uploadQR(ce, code)
+			if !ok {
+				continue
+			}
+			_, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{
+				MsgType: event.MsgImage,
+				Body:    code,
+				URL:     url.CUString(),
+				NewContent: &event.MessageEventContent{
+					MsgType: event.MsgImage,
+					Body:    code,
+					URL:     url.CUString(),
+				},
+				RelatesTo: &event.RelatesTo{
+					Type:    event.RelReplace,
+					EventID: qrEventID,
+				},
+			})
+			if err != nil {
+				user.log.Errorln("Failed to send edited QR code to user:", err)
+			}
+		case <-ctx.Done():
+			return
+		}
+	}
+}
+
+func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool) {
+	qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
+	if err != nil {
+		user.log.Errorln("Failed to encode QR code:", err)
+		ce.Reply("Failed to encode QR code: %v", err)
+		return id.ContentURI{}, false
+	}
+
+	bot := user.bridge.AS.BotClient()
+
+	resp, err := bot.UploadBytes(qrCode, "image/png")
+	if err != nil {
+		user.log.Errorln("Failed to upload QR code:", err)
+		ce.Reply("Failed to upload QR code: %v", err)
+		return id.ContentURI{}, false
+	}
+	return resp.ContentURI, true
 }
 
 const cmdLogoutHelp = `logout - Logout from WhatsApp`
@@ -396,7 +492,7 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
 	if ce.User.Session == nil {
 		ce.Reply("You're not logged in.")
 		return
-	} else if !ce.User.IsConnected() {
+	} else if !ce.User.IsLoggedIn() {
 		ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.")
 		return
 	}
@@ -407,17 +503,16 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
 			ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
 		}
 	}
-	err := ce.User.Conn.Logout()
-	if err != nil {
-		ce.User.log.Warnln("Error while logging out:", err)
-		ce.Reply("Unknown error while logging out: %v", err)
-		return
-	}
+	// TODO reimplement
+	//err := ce.User.Client.Logout()
+	//if err != nil {
+	//	ce.User.log.Warnln("Error while logging out:", err)
+	//	ce.Reply("Unknown error while logging out: %v", err)
+	//	return
+	//}
 	ce.User.removeFromJIDMap(StateLoggedOut)
-	// TODO this causes a foreign key violation, which should be fixed
-	//ce.User.JID = ""
-	ce.User.SetSession(nil)
 	ce.User.DeleteConnection()
+	ce.User.DeleteSession()
 	ce.Reply("Logged out successfully.")
 }
 
@@ -438,21 +533,21 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
 		return
 	}
 	if ce.Args[0] == "presence" || ce.Args[0] == "all" {
-		customPuppet.EnablePresence = !customPuppet.EnablePresence
-		var newPresence whatsapp.Presence
-		if customPuppet.EnablePresence {
-			newPresence = whatsapp.PresenceAvailable
-			ce.Reply("Enabled presence bridging")
-		} else {
-			newPresence = whatsapp.PresenceUnavailable
-			ce.Reply("Disabled presence bridging")
-		}
-		if ce.User.IsConnected() {
-			_, err := ce.User.Conn.Presence("", newPresence)
-			if err != nil {
-				ce.User.log.Warnln("Failed to set presence:", err)
-			}
-		}
+		//customPuppet.EnablePresence = !customPuppet.EnablePresence
+		//var newPresence whatsapp.Presence
+		//if customPuppet.EnablePresence {
+		//	newPresence = whatsapp.PresenceAvailable
+		//	ce.Reply("Enabled presence bridging")
+		//} else {
+		//	newPresence = whatsapp.PresenceUnavailable
+		//	ce.Reply("Disabled presence bridging")
+		//}
+		//if ce.User.IsConnected() {
+		//	_, err := ce.User.Conn.Presence("", newPresence)
+		//	if err != nil {
+		//		ce.User.log.Warnln("Failed to set presence:", err)
+		//	}
+		//}
 	}
 	if ce.Args[0] == "receipts" || ce.Args[0] == "all" {
 		customPuppet.EnableReceipts = !customPuppet.EnableReceipts
@@ -468,108 +563,82 @@ func (handler *CommandHandler) CommandToggle(ce *CommandEvent) {
 const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request`
 
 func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) {
-	if ce.User.Session == nil && ce.User.Conn == nil {
+	if ce.User.Session == nil && ce.User.Client == nil {
 		ce.Reply("Nothing to purge: no session information stored and no active connection.")
 		return
 	}
-	//ce.User.JID = ""
 	ce.User.removeFromJIDMap(StateLoggedOut)
-	ce.User.SetSession(nil)
 	ce.User.DeleteConnection()
+	ce.User.DeleteSession()
 	ce.Reply("Session information purged")
 }
 
 const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp`
 
 func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) {
-	if ce.User.Conn == nil {
-		if ce.User.Session == nil {
-			ce.Reply("No existing connection and no session. Did you mean `login`?")
-		} else {
-			ce.Reply("No existing connection, creating one...")
-			ce.User.Connect(false)
-		}
-		return
-	}
-
-	wasConnected := true
-	err := ce.User.Conn.Disconnect()
-	if err == whatsapp.ErrNotConnected {
-		wasConnected = false
-	} else if err != nil {
-		ce.User.log.Warnln("Error while disconnecting:", err)
-	}
-
-	ctx := context.Background()
-
-	err = ce.User.Conn.Restore(true, ctx)
-	if err == whatsapp.ErrInvalidSession {
-		if ce.User.Session != nil {
-			ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
-			ce.User.Conn.SetSession(*ce.User.Session)
-			err = ce.User.Conn.Restore(true, ctx)
-		} else {
-			ce.Reply("You are not logged in.")
-			return
-		}
-	} else if err == whatsapp.ErrLoginInProgress {
-		ce.Reply("A login or reconnection is already in progress.")
-		return
-	} else if err == whatsapp.ErrAlreadyLoggedIn {
-		ce.Reply("You were already connected.")
-		return
-	}
-	if err != nil {
-		ce.User.log.Warnln("Error while reconnecting:", err)
-		ce.Reply("Unknown error while reconnecting: %v", err)
-		ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
-		err = ce.User.Conn.Disconnect()
-		if err != nil {
-			ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
-		}
-		return
-	}
-	ce.User.ConnectionErrors = 0
-
-	var msg string
-	if wasConnected {
-		msg = "Reconnected successfully."
-	} else {
-		msg = "Connected successfully."
-	}
-	ce.Reply(msg)
-	ce.User.PostLogin()
-}
-
-const cmdDeleteConnectionHelp = `delete-connection - Disconnect ignoring errors and delete internal connection state.`
-
-func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) {
-	if ce.User.Conn == nil {
-		ce.Reply("You don't have a WhatsApp connection.")
-		return
-	}
-	ce.User.DeleteConnection()
-	ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
+	// TODO reimplement
+	//if ce.User.Client == nil {
+	//	if ce.User.Session == nil {
+	//		ce.Reply("No existing connection and no session. Did you mean `login`?")
+	//	} else {
+	//		ce.Reply("No existing connection, creating one...")
+	//		ce.User.Connect(false)
+	//	}
+	//	return
+	//}
+	//
+	//wasConnected := true
+	//ce.User.Client.Disconnect()
+	//ctx := context.Background()
+	//connected := ce.User.Connect(false)
+	//
+	//err = ce.User.Conn.Restore(true, ctx)
+	//if err == whatsapp.ErrInvalidSession {
+	//	if ce.User.Session != nil {
+	//		ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
+	//		ce.User.Conn.SetSession(*ce.User.Session)
+	//		err = ce.User.Conn.Restore(true, ctx)
+	//	} else {
+	//		ce.Reply("You are not logged in.")
+	//		return
+	//	}
+	//} else if err == whatsapp.ErrLoginInProgress {
+	//	ce.Reply("A login or reconnection is already in progress.")
+	//	return
+	//} else if err == whatsapp.ErrAlreadyLoggedIn {
+	//	ce.Reply("You were already connected.")
+	//	return
+	//}
+	//if err != nil {
+	//	ce.User.log.Warnln("Error while reconnecting:", err)
+	//	ce.Reply("Unknown error while reconnecting: %v", err)
+	//	ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
+	//	err = ce.User.Conn.Disconnect()
+	//	if err != nil {
+	//		ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
+	//	}
+	//	return
+	//}
+	//ce.User.ConnectionErrors = 0
+	//
+	//var msg string
+	//if wasConnected {
+	//	msg = "Reconnected successfully."
+	//} else {
+	//	msg = "Connected successfully."
+	//}
+	//ce.Reply(msg)
+	//ce.User.PostLogin()
 }
 
 const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without logging out)`
 
 func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) {
-	if ce.User.Conn == nil {
+	if ce.User.Client == nil {
 		ce.Reply("You don't have a WhatsApp connection.")
 		return
 	}
-	err := ce.User.Conn.Disconnect()
-	if err == whatsapp.ErrNotConnected {
-		ce.Reply("You were not connected.")
-		return
-	} else if err != nil {
-		ce.User.log.Warnln("Error while disconnecting:", err)
-		ce.Reply("Unknown error while disconnecting: %v", err)
-		return
-	}
-	ce.User.bridge.Metrics.TrackConnectionState(ce.User.JID, false)
-	ce.User.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
+	ce.User.DeleteConnection()
 	ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.")
 }
 
@@ -577,21 +646,11 @@ const cmdPingHelp = `ping - Check your connection to WhatsApp.`
 
 func (handler *CommandHandler) CommandPing(ce *CommandEvent) {
 	if ce.User.Session == nil {
-		if ce.User.IsLoginInProgress() {
-			ce.Reply("You're not logged into WhatsApp, but there's a login in progress.")
-		} else {
-			ce.Reply("You're not logged into WhatsApp.")
-		}
-	} else if ce.User.Conn == nil {
+		ce.Reply("You're not logged into WhatsApp.")
+	} else if ce.User.Client == nil || !ce.User.Client.IsConnected() {
 		ce.Reply("You don't have a WhatsApp connection.")
-	} else if err := ce.User.Conn.AdminTest(); err != nil {
-		if ce.User.IsLoginInProgress() {
-			ce.Reply("Connection not OK: %v, but login in progress", err)
-		} else {
-			ce.Reply("Connection not OK: %v", err)
-		}
 	} else {
-		ce.Reply("Connection to WhatsApp OK")
+		ce.Reply("Connection to WhatsApp OK (probably)")
 	}
 }
 
@@ -612,12 +671,10 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
 		cmdPrefix + cmdDeleteSessionHelp,
 		cmdPrefix + cmdReconnectHelp,
 		cmdPrefix + cmdDisconnectHelp,
-		cmdPrefix + cmdDeleteConnectionHelp,
 		cmdPrefix + cmdPingHelp,
 		cmdPrefix + cmdLoginMatrixHelp,
 		cmdPrefix + cmdLogoutMatrixHelp,
 		cmdPrefix + cmdToggleHelp,
-		cmdPrefix + cmdSyncHelp,
 		cmdPrefix + cmdListHelp,
 		cmdPrefix + cmdOpenHelp,
 		cmdPrefix + cmdPMHelp,
@@ -630,37 +687,6 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) {
 	}, "\n* "))
 }
 
-const cmdSyncHelp = `sync [--create-all] - Synchronize contacts from phone and optionally create portals for group chats.`
-
-// CommandSync handles sync command
-func (handler *CommandHandler) CommandSync(ce *CommandEvent) {
-	user := ce.User
-	create := len(ce.Args) > 0 && ce.Args[0] == "--create-all"
-
-	ce.Reply("Updating contact and chat list...")
-	handler.log.Debugln("Importing contacts of", user.MXID)
-	_, err := user.Conn.Contacts()
-	if err != nil {
-		user.log.Errorln("Error updating contacts:", err)
-		ce.Reply("Failed to sync contact list (see logs for details)")
-		return
-	}
-	handler.log.Debugln("Importing chats of", user.MXID)
-	_, err = user.Conn.Chats()
-	if err != nil {
-		user.log.Errorln("Error updating chats:", err)
-		ce.Reply("Failed to sync chat list (see logs for details)")
-		return
-	}
-
-	ce.Reply("Syncing contacts...")
-	user.syncPuppets(nil)
-	ce.Reply("Syncing chats...")
-	user.syncPortals(nil, create)
-
-	ce.Reply("Sync complete.")
-}
-
 const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.`
 
 func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
@@ -670,11 +696,13 @@ func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) {
 	}
 
 	if !ce.User.Admin {
-		users := ce.Portal.GetUserIDs()
-		if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) {
-			ce.Reply("Only bridge admins can delete portals with other Matrix users")
-			return
-		}
+		// TODO reimplement
+		//users := ce.Portal.GetUserIDs()
+		//if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) {
+		//	ce.Reply("Only bridge admins can delete portals with other Matrix users")
+		//	return
+		//}
+		return
 	}
 
 	ce.Portal.log.Infoln(ce.User.MXID, "requested deletion of portal.")
@@ -687,12 +715,13 @@ const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals th
 func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
 	portals := ce.User.GetPortals()
 	portalsToDelete := make([]*Portal, 0, len(portals))
-	for _, portal := range portals {
-		users := portal.GetUserIDs()
-		if len(users) == 1 && users[0] == ce.User.MXID {
-			portalsToDelete = append(portalsToDelete, portal)
-		}
-	}
+	// TODO reimplement
+	//for _, portal := range portals {
+	//	users := portal.GetUserIDs()
+	//	if len(users) == 1 && users[0] == ce.User.MXID {
+	//		portalsToDelete = append(portalsToDelete, portal)
+	//	}
+	//}
 	leave := func(portal *Portal) {
 		if len(portal.MXID) > 0 {
 			_, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
@@ -729,21 +758,21 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) {
 
 const cmdListHelp = `list <contacts|groups> [page] [items per page] - Get a list of all contacts and groups.`
 
-func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
-	for jid, contact := range input {
-		if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
-			continue
-		}
-
-		if contacts {
-			result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
-		} else {
-			result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
-		}
-	}
-	sort.Sort(sort.StringSlice(result))
-	return
-}
+//func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) {
+//	for jid, contact := range input {
+//		if strings.HasSuffix(jid, whatsapp.NewUserSuffix) != contacts {
+//			continue
+//		}
+//
+//		if contacts {
+//			result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.JID[:len(contact.JID)-len(whatsapp.NewUserSuffix)]))
+//		} else {
+//			result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.JID))
+//		}
+//	}
+//	sort.Sort(sort.StringSlice(result))
+//	return
+//}
 
 func (handler *CommandHandler) CommandList(ce *CommandEvent) {
 	if len(ce.Args) == 0 {
@@ -774,33 +803,34 @@ func (handler *CommandHandler) CommandList(ce *CommandEvent) {
 			ce.Reply("Warning: a high number of items per page may fail to send a reply")
 		}
 	}
-	contacts := mode[0] == 'c'
-	typeName := "Groups"
-	if contacts {
-		typeName = "Contacts"
-	}
-	ce.User.Conn.Store.ContactsLock.RLock()
-	result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
-	ce.User.Conn.Store.ContactsLock.RUnlock()
-	if len(result) == 0 {
-		ce.Reply("No %s found", strings.ToLower(typeName))
-		return
-	}
-	pages := int(math.Ceil(float64(len(result)) / float64(max)))
-	if (page-1)*max >= len(result) {
-		if pages == 1 {
-			ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
-		} else {
-			ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
-		}
-		return
-	}
-	lastIndex := page * max
-	if lastIndex > len(result) {
-		lastIndex = len(result)
-	}
-	result = result[(page-1)*max : lastIndex]
-	ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
+	// TODO reimplement
+	//contacts := mode[0] == 'c'
+	//typeName := "Groups"
+	//if contacts {
+	//	typeName = "Contacts"
+	//}
+	//ce.User.Conn.Store.ContactsLock.RLock()
+	//result := formatContacts(contacts, ce.User.Conn.Store.Contacts)
+	//ce.User.Conn.Store.ContactsLock.RUnlock()
+	//if len(result) == 0 {
+	//	ce.Reply("No %s found", strings.ToLower(typeName))
+	//	return
+	//}
+	//pages := int(math.Ceil(float64(len(result)) / float64(max)))
+	//if (page-1)*max >= len(result) {
+	//	if pages == 1 {
+	//		ce.Reply("There is only 1 page of %s", strings.ToLower(typeName))
+	//	} else {
+	//		ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName))
+	//	}
+	//	return
+	//}
+	//lastIndex := page * max
+	//if lastIndex > len(result) {
+	//	lastIndex = len(result)
+	//}
+	//result = result[(page-1)*max : lastIndex]
+	//ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n"))
 }
 
 const cmdOpenHelp = `open <_group JID_> - Open a group chat portal.`
@@ -811,80 +841,68 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) {
 		return
 	}
 
-	user := ce.User
-	jid := ce.Args[0]
-
-	if strings.HasSuffix(jid, whatsapp.NewUserSuffix) {
-		ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsapp.NewUserSuffix)])
-		return
-	}
-
-	user.Conn.Store.ContactsLock.RLock()
-	contact, ok := user.Conn.Store.Contacts[jid]
-	user.Conn.Store.ContactsLock.RUnlock()
-	if !ok {
-		ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.")
-		return
-	}
-	handler.log.Debugln("Importing", jid, "for", user)
-	portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid))
-	if len(portal.MXID) > 0 {
-		portal.Sync(user, contact)
-		ce.Reply("Portal room synced.")
-	} else {
-		portal.Sync(user, contact)
-		ce.Reply("Portal room created.")
-	}
-	_, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
-}
-
-const cmdPMHelp = `pm [--force] <_international phone number_> - Open a private chat with the given phone number.`
+	// 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 <_international phone number_> - Open a private chat with the given phone number.`
 
 func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
 	if len(ce.Args) == 0 {
-		ce.Reply("**Usage:** `pm [--force] <international phone number>`")
+		ce.Reply("**Usage:** `pm <international phone number>`")
 		return
 	}
 
-	force := ce.Args[0] == "--force"
-	if force {
-		ce.Args = ce.Args[1:]
-	}
-
 	user := ce.User
 
 	number := strings.Join(ce.Args, "")
-	if number[0] == '+' {
-		number = number[1:]
+	resp, err := ce.User.Client.IsOnWhatsApp([]string{number})
+	if err != nil {
+		ce.Reply("Failed to check if user is on WhatsApp: %v", err)
+		return
+	} else if len(resp) == 0 {
+		ce.Reply("Didn't get a response to checking if the user is on WhatsApp")
+		return
 	}
-	for _, char := range number {
-		if char < '0' || char > '9' {
-			ce.Reply("Invalid phone number.")
-			return
-		}
+	targetUser := resp[0]
+	if !targetUser.IsIn {
+		ce.Reply("The server said +%s is not on WhatsApp", targetUser.JID.User)
+		return
 	}
-	jid := number + whatsapp.NewUserSuffix
-
-	handler.log.Debugln("Importing", jid, "for", user)
 
-	user.Conn.Store.ContactsLock.RLock()
-	contact, ok := user.Conn.Store.Contacts[jid]
-	user.Conn.Store.ContactsLock.RUnlock()
-	if !ok {
-		if !force {
-			ce.Reply("Phone number not found in contacts. Try syncing contacts with `sync` first. " +
-				"To create a portal anyway, use `pm --force <number>`.")
-			return
-		}
-		contact = whatsapp.Contact{JID: jid}
-	}
-	puppet := user.bridge.GetPuppetByJID(contact.JID)
-	puppet.Sync(user, contact)
-	portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.JID, user.JID))
+	handler.log.Debugln("Importing", targetUser.JID, "for", user)
+	puppet := user.bridge.GetPuppetByJID(targetUser.JID)
+	puppet.SyncContact(user, true)
+	portal := user.GetPortalByJID(puppet.JID)
 	if len(portal.MXID) > 0 {
-		var err error
 		if !user.IsRelaybot {
-			err = portal.MainIntent().EnsureInvited(portal.MXID, user.MXID)
+			_, err = portal.MainIntent().Client.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID})
+			if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
+				err = nil
+			}
 		}
 		if err != nil {
 			portal.log.Warnfln("Failed to invite %s to portal: %v. Creating new portal", user.MXID, err)
@@ -894,7 +912,7 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) {
 			return
 		}
 	}
-	err := portal.CreateMatrixRoom(user)
+	err = portal.CreateMatrixRoom(user)
 	if err != nil {
 		ce.Reply("Failed to create portal room: %v", err)
 		return

+ 0 - 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
-}

+ 30 - 41
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,7 +30,6 @@ import (
 type BridgeConfig struct {
 	UsernameTemplate    string `yaml:"username_template"`
 	DisplaynameTemplate string `yaml:"displayname_template"`
-	CommunityTemplate   string `yaml:"community_template"`
 
 	ConnectionTimeout     int  `yaml:"connection_timeout"`
 	FetchMessageOnTimeout bool `yaml:"fetch_message_on_timeout"`
@@ -100,7 +98,6 @@ type BridgeConfig struct {
 
 	usernameTemplate    *template.Template `yaml:"-"`
 	displaynameTemplate *template.Template `yaml:"-"`
-	communityTemplate   *template.Template `yaml:"-"`
 }
 
 func (bc *BridgeConfig) setDefaults() {
@@ -156,13 +153,6 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 		return err
 	}
 
-	if len(bc.CommunityTemplate) > 0 {
-		bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate)
-		if err != nil {
-			return err
-		}
-	}
-
 	return nil
 }
 
@@ -170,44 +160,43 @@ type UsernameTemplateArgs struct {
 	UserID id.UserID
 }
 
-func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
-	var buf bytes.Buffer
-	if index := strings.IndexRune(contact.JID, '@'); index > 0 {
-		contact.JID = "+" + contact.JID[:index]
-	}
-	bc.displaynameTemplate.Execute(&buf, contact)
+type legacyContactInfo struct {
+	types.ContactInfo
+	Phone string
+
+	Notify string
+	VName  string
+	Name   string
+	Short  string
+	JID    string
+}
+
+func (bc BridgeConfig) FormatDisplayname(jid types.JID, contact types.ContactInfo) (string, int8) {
+	var buf strings.Builder
+	_ = bc.displaynameTemplate.Execute(&buf, legacyContactInfo{
+		ContactInfo: contact,
+		Notify:      contact.PushName,
+		VName:       contact.BusinessName,
+		Name:        contact.FullName,
+		Short:       contact.FirstName,
+		Phone:       "+" + jid.User,
+		JID:         "+" + jid.User,
+	})
 	var quality int8
 	switch {
-	case len(contact.Notify) > 0 || len(contact.VName) > 0:
+	case len(contact.PushName) > 0 || len(contact.BusinessName) > 0:
 		quality = 3
-	case len(contact.Name) > 0 || len(contact.Short) > 0:
+	case len(contact.FullName) > 0 || len(contact.FirstName) > 0:
 		quality = 2
-	case len(contact.JID) > 0:
-		quality = 1
 	default:
-		quality = 0
+		quality = 1
 	}
 	return buf.String(), quality
 }
 
-func (bc BridgeConfig) FormatUsername(userID whatsapp.JID) string {
-	var buf bytes.Buffer
-	bc.usernameTemplate.Execute(&buf, userID)
-	return buf.String()
-}
-
-type CommunityTemplateArgs struct {
-	Localpart string
-	Server    string
-}
-
-func (bc BridgeConfig) EnableCommunities() bool {
-	return bc.communityTemplate != nil
-}
-
-func (bc BridgeConfig) FormatCommunity(localpart, server string) string {
-	var buf bytes.Buffer
-	bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server})
+func (bc BridgeConfig) FormatUsername(username string) string {
+	var buf strings.Builder
+	_ = bc.usernameTemplate.Execute(&buf, username)
 	return buf.String()
 }
 

+ 2 - 1
crypto.go

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

+ 14 - 13
custompuppet.go

@@ -23,7 +23,7 @@ import (
 	"errors"
 	"time"
 
-	"github.com/Rhymen/go-whatsapp"
+	"go.mau.fi/whatsmeow/types"
 
 	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix/appservice"
@@ -160,7 +160,7 @@ func (puppet *Puppet) stopSyncing() {
 }
 
 func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
-	if !puppet.customUser.IsConnected() {
+	if !puppet.customUser.IsLoggedIn() {
 		puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
 		return nil
 	}
@@ -200,14 +200,14 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
 }
 
 func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
-	presence := whatsapp.PresenceAvailable
+	presence := types.PresenceAvailable
 	if event.Content.Raw["presence"].(string) != "online" {
-		presence = whatsapp.PresenceUnavailable
+		presence = types.PresenceUnavailable
 		puppet.customUser.log.Debugln("Marking offline")
 	} else {
 		puppet.customUser.log.Debugln("Marking online")
 	}
-	_, err := puppet.customUser.Conn.Presence("", presence)
+	err := puppet.customUser.Client.SendPresence(presence)
 	if err != nil {
 		puppet.customUser.log.Warnln("Failed to set presence:", err)
 	}
@@ -221,11 +221,12 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
 			puppet.customUser.log.Debugfln("Ignoring double puppeted read receipt %+v", event.Content.Raw)
 			// Ignore double puppeted read receipts.
 		} else if message := puppet.bridge.DB.Message.GetByMXID(eventID); message != nil {
-			puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
-			_, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
-			if err != nil {
-				puppet.customUser.log.Warnln("Error marking read:", err)
-			}
+			// TODO reimplement
+			//puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
+			//_, err := puppet.customUser.Client.Read(portal.Key.JID, message.JID)
+			//if err != nil {
+			//	puppet.customUser.log.Warnln("Error marking read:", err)
+			//}
 		}
 	}
 }
@@ -240,14 +241,14 @@ func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
 	}
 	if puppet.customTypingIn[evt.RoomID] != isTyping {
 		puppet.customTypingIn[evt.RoomID] = isTyping
-		presence := whatsapp.PresenceComposing
+		presence := types.ChatPresenceComposing
 		if !isTyping {
 			puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
-			presence = whatsapp.PresencePaused
+			presence = types.ChatPresencePaused
 		} else {
 			puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
 		}
-		_, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
+		err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID)
 		if err != nil {
 			puppet.customUser.log.Warnln("Error setting typing:", err)
 		}

+ 6 - 7
database/message.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,11 +21,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/Rhymen/go-whatsapp"
-
 	log "maunium.net/go/maulogger/v2"
-
 	"maunium.net/go/mautrix/id"
+
+	"go.mau.fi/whatsmeow/types"
 )
 
 type MessageQuery struct {
@@ -52,7 +51,7 @@ func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) {
 	return
 }
 
-func (mq *MessageQuery) GetByJID(chat PortalKey, jid whatsapp.MessageID) *Message {
+func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.MessageID) *Message {
 	return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent "+
 		"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
 }
@@ -90,9 +89,9 @@ type Message struct {
 	log log.Logger
 
 	Chat      PortalKey
-	JID       whatsapp.MessageID
+	JID       types.MessageID
 	MXID      id.EventID
-	Sender    whatsapp.JID
+	Sender    types.JID
 	Timestamp int64
 	Sent      bool
 }

+ 39 - 41
database/portal.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,42 +18,40 @@ package database
 
 import (
 	"database/sql"
-	"strings"
 
 	log "maunium.net/go/maulogger/v2"
-
-	"github.com/Rhymen/go-whatsapp"
-
 	"maunium.net/go/mautrix/id"
+
+	"go.mau.fi/whatsmeow/types"
 )
 
 type PortalKey struct {
-	JID      whatsapp.JID
-	Receiver whatsapp.JID
+	JID      types.JID
+	Receiver types.JID
 }
 
-func GroupPortalKey(jid whatsapp.JID) PortalKey {
+func GroupPortalKey(jid types.JID) PortalKey {
 	return PortalKey{
-		JID:      jid,
-		Receiver: jid,
+		JID:      jid.ToNonAD(),
+		Receiver: jid.ToNonAD(),
 	}
 }
 
-func NewPortalKey(jid, receiver whatsapp.JID) PortalKey {
-	if strings.HasSuffix(jid, whatsapp.GroupSuffix) {
+func NewPortalKey(jid, receiver types.JID) PortalKey {
+	if jid.Server == types.GroupServer {
 		receiver = jid
 	}
 	return PortalKey{
-		JID:      jid,
-		Receiver: receiver,
+		JID:      jid.ToNonAD(),
+		Receiver: receiver.ToNonAD(),
 	}
 }
 
 func (key PortalKey) String() string {
 	if key.Receiver == key.JID {
-		return key.JID
+		return key.JID.String()
 	}
-	return key.JID + "-" + key.Receiver
+	return key.JID.String() + "-" + key.Receiver.String()
 }
 
 type PortalQuery struct {
@@ -80,12 +78,12 @@ func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
 	return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
 }
 
-func (pq *PortalQuery) GetAllByJID(jid whatsapp.JID) []*Portal {
+func (pq *PortalQuery) GetAllByJID(jid types.JID) []*Portal {
 	return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid)
 }
 
-func (pq *PortalQuery) FindPrivateChats(receiver whatsapp.JID) []*Portal {
-	return pq.getAll("SELECT * FROM portal WHERE receiver=$1 AND jid LIKE '%@s.whatsapp.net'", receiver)
+func (pq *PortalQuery) FindPrivateChats(receiver types.JID) []*Portal {
+	return pq.getAll("SELECT * FROM portal WHERE receiver='$1@s.whatsapp.net' AND jid LIKE '%@s.whatsapp.net'", receiver)
 }
 
 func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) {
@@ -170,25 +168,25 @@ func (portal *Portal) Delete() {
 	}
 }
 
-func (portal *Portal) GetUserIDs() []id.UserID {
-	rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
-		WHERE "user".jid=user_portal.user_jid
-			AND user_portal.portal_jid=$1
-			AND user_portal.portal_receiver=$2`,
-		portal.Key.JID, portal.Key.Receiver)
-	if err != nil {
-		portal.log.Debugln("Failed to get portal user ids:", err)
-		return nil
-	}
-	var userIDs []id.UserID
-	for rows.Next() {
-		var userID id.UserID
-		err = rows.Scan(&userID)
-		if err != nil {
-			portal.log.Warnln("Failed to scan row:", err)
-			continue
-		}
-		userIDs = append(userIDs, userID)
-	}
-	return userIDs
-}
+//func (portal *Portal) GetUserIDs() []id.UserID {
+//	rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
+//		WHERE "user".jid=user_portal.user_jid
+//			AND user_portal.portal_jid=$1
+//			AND user_portal.portal_receiver=$2`,
+//		portal.Key.JID, portal.Key.Receiver)
+//	if err != nil {
+//		portal.log.Debugln("Failed to get portal user ids:", err)
+//		return nil
+//	}
+//	var userIDs []id.UserID
+//	for rows.Next() {
+//		var userID id.UserID
+//		err = rows.Scan(&userID)
+//		if err != nil {
+//			portal.log.Warnln("Failed to scan row:", err)
+//			continue
+//		}
+//		userIDs = append(userIDs, userID)
+//	}
+//	return userIDs
+//}

+ 20 - 15
database/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
@@ -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 - 0
database/upgrades/2021-10-21-add-whatsmeow-store.go

@@ -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))
+	}}
+}

+ 87 - 0
database/upgrades/2021-10-21-multidevice-updates.go

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

+ 1 - 1
database/upgrades/upgrades.go

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

+ 119 - 157
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
+func (user *User) usernamePtr() *string {
+	if !user.JID.IsEmpty() {
+		return &user.JID.User
 	}
-
-	index := strings.IndexRune(jid, '@')
-	if index < 0 {
-		return jid
-	}
-
-	return jid[:index]
+	return nil
 }
 
-func (user *User) jidPtr() *string {
-	if len(user.JID) > 0 {
-		str := stripSuffix(user.JID)
-		return &str
+func (user *User) agentPtr() *uint8 {
+	if !user.JID.IsEmpty() {
+		return &user.JID.Agent
 	}
 	return nil
 }
 
-func (user *User) sessionUnptr() (sess whatsapp.Session) {
-	if user.Session != nil {
-		sess = *user.Session
+func (user *User) devicePtr() *uint8 {
+	if !user.JID.IsEmpty() {
+		return &user.JID.Device
 	}
-	return
+	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)
+//	}
+//}

+ 6 - 14
example-config.yaml

@@ -73,21 +73,13 @@ bridge:
     # {{.}} is replaced with the phone number of the WhatsApp user.
     username_template: whatsapp_{{.}}
     # Displayname template for WhatsApp users.
-    # {{.Notify}} - nickname set by the WhatsApp user
-    # {{.VName}} - validated WhatsApp business name
-    # {{.JID}}    - phone number (international format)
+    # {{.PushName}}     - nickname set by the WhatsApp user
+    # {{.BusinessName}} - validated WhatsApp business name
+    # {{.Phone}}        - phone number (international format)
     # The following variables are also available, but will cause problems on multi-user instances:
-    # {{.Name}}   - display name from contact list
-    # {{.Short}}  - short display name from contact list
-    displayname_template: "{{if .Notify}}{{.Notify}}{{else if .VName}}{{.VName}}{{else}}{{.JID}}{{end}} (WA)"
-    # Localpart template for per-user room grouping community IDs.
-    # On startup, the bridge will try to create these communities, add all of the specific user's
-    # portals to the community, and invite the Matrix user to it.
-    # (Note that, by default, non-admins might not have your homeserver's permission to create
-    #  communities.)
-    # {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user.
-    # whatsapp_{{.Localpart}}={{.Server}} is a good value that should work for any user.
-    community_template: null
+    # {{.FullName}}  - full name from contact list
+    # {{.FirstName}} - first name from contact list
+    displayname_template: "{{if .PushName}}{{.PushName}}{{else if .BusinessName}}{{.BusinessName}}{{else}}{{.JID}}{{end}} (WA)"
 
     # WhatsApp connection timeout in seconds.
     connection_timeout: 20

+ 27 - 32
formatting.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
@@ -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
 }

+ 27 - 6
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-20211022171408-90a9b647d253
 	golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
+	google.golang.org/protobuf v1.27.1
 	gopkg.in/yaml.v2 v2.4.0
 	maunium.net/go/mauflag v1.0.0
 	maunium.net/go/maulogger/v2 v2.3.0
-	maunium.net/go/mautrix v0.9.27
+	maunium.net/go/mautrix v0.9.29
 )
 
-replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.5.12
+require (
+	filippo.io/edwards25519 v1.0.0-rc.1 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/btcsuite/btcutil v1.0.2 // indirect
+	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/golang/protobuf v1.5.0 // indirect
+	github.com/gorilla/mux v1.8.0 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+	github.com/prometheus/client_model v0.2.0 // indirect
+	github.com/prometheus/common v0.26.0 // indirect
+	github.com/prometheus/procfs v0.6.0 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/tidwall/gjson v1.6.8 // indirect
+	github.com/tidwall/match v1.0.3 // indirect
+	github.com/tidwall/pretty v1.0.2 // indirect
+	github.com/tidwall/sjson v1.1.5 // indirect
+	go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488 // indirect
+	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
+	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
+	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
+)

+ 19 - 15
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=
@@ -77,11 +78,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
-github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
+github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
-github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
+github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -138,16 +139,18 @@ github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
 github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
 github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
-github.com/tulir/go-whatsapp v0.5.12 h1:JGU5yhoh+CyDcSMUilwy7FL0gFo0zqqepsHRqEjrjKc=
-github.com/tulir/go-whatsapp v0.5.12/go.mod h1:7J3IIL3bEQiBJGtiZst1N4PgXHlWIartdVQLe6lcx9A=
+go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488 h1:dIOtV7Fl8bxdOOvBndilSmWFcufBArgq2sZJOqV3Enc=
+go.mau.fi/libsignal v0.0.0-20211016130347-464152efc488/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
+go.mau.fi/whatsmeow v0.0.0-20211022171408-90a9b647d253 h1:poKOYLU6AFJF5wqq4iuV4zYvl4TUCe2D77ZMv4of36k=
+go.mau.fi/whatsmeow v0.0.0-20211022171408-90a9b647d253/go.mod h1:GJl+Pfu5TEvDM+lXG/PnX9/yMf6vEMwD8HC4Nq75Vhg=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
-golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -179,8 +182,9 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -198,8 +202,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
@@ -220,5 +224,5 @@ maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfk
 maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
 maunium.net/go/maulogger/v2 v2.3.0 h1:TMCcO65fLk6+pJXo7sl38tzjzW0KBFgc6JWJMBJp4GE=
 maunium.net/go/maulogger/v2 v2.3.0/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.9.27 h1:6MV6YSCGqfw8Rb0G1PHjTOkYkTY0vcZaz6wd+U+V1Is=
-maunium.net/go/mautrix v0.9.27/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
+maunium.net/go/mautrix v0.9.29 h1:qJyTSZQuogkkEFrJd+oZiTuE/6Cq7ca3wxiLYadYUoM=
+maunium.net/go/mautrix v0.9.29/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=

+ 37 - 14
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"
@@ -48,7 +54,7 @@ var (
 	// This is changed when making a release
 	Version = "0.1.8"
 	// This is filled by init()
-	WAVersion = ""
+	WAVersion     = ""
 	VersionString = ""
 	// These are filled at build time with the -X linker flag
 	Tag       = "unknown"
@@ -148,16 +154,17 @@ type Bridge struct {
 	Relaybot       *User
 	Crypto         Crypto
 	Metrics        *MetricsHandler
+	WAContainer    *sqlstore.Container
 
 	usersByMXID         map[id.UserID]*User
-	usersByJID          map[whatsapp.JID]*User
+	usersByUsername     map[string]*User
 	usersLock           sync.Mutex
 	managementRooms     map[id.RoomID]*User
 	managementRoomsLock sync.Mutex
 	portalsByMXID       map[id.RoomID]*Portal
 	portalsByJID        map[database.PortalKey]*Portal
 	portalsLock         sync.Mutex
-	puppets             map[whatsapp.JID]*Puppet
+	puppets             map[types.JID]*Puppet
 	puppetsByCustomMXID map[id.UserID]*Puppet
 	puppetsLock         sync.Mutex
 }
@@ -176,11 +183,11 @@ type Crypto interface {
 func NewBridge() *Bridge {
 	bridge := &Bridge{
 		usersByMXID:         make(map[id.UserID]*User),
-		usersByJID:          make(map[whatsapp.JID]*User),
+		usersByUsername:     make(map[string]*User),
 		managementRooms:     make(map[id.RoomID]*User),
 		portalsByMXID:       make(map[id.RoomID]*Portal),
 		portalsByJID:        make(map[database.PortalKey]*Portal),
-		puppets:             make(map[whatsapp.JID]*Puppet),
+		puppets:             make(map[types.JID]*Puppet),
 		puppetsByCustomMXID: make(map[id.UserID]*Puppet),
 	}
 
@@ -259,6 +266,8 @@ func (bridge *Bridge) Init() {
 	bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns)
 	bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns)
 
+	bridge.WAContainer = sqlstore.NewWithDB(bridge.DB.DB, bridge.Config.AppService.Database.Type, nil)
+
 	ss := bridge.Config.AppService.Provisioning.SharedSecret
 	if len(ss) > 0 && ss != "disable" {
 		bridge.Provisioning = &ProvisioningAPI{bridge: bridge}
@@ -271,6 +280,23 @@ func (bridge *Bridge) Init() {
 	bridge.Formatter = NewFormatter(bridge)
 	bridge.Crypto = NewCryptoHelper(bridge)
 	bridge.Metrics = NewMetricsHandler(bridge.Config.Metrics.Listen, bridge.Log.Sub("Metrics"), bridge.DB)
+
+	store.BaseClientPayload.UserAgent.OsVersion = proto.String(WAVersion)
+	store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(WAVersion)
+	store.CompanionProps.Os = proto.String(bridge.Config.WhatsApp.OSName)
+	versionParts := strings.Split(WAVersion, ".")
+	if len(versionParts) > 2 {
+		primary, _ := strconv.Atoi(versionParts[0])
+		secondary, _ := strconv.Atoi(versionParts[1])
+		tertiary, _ := strconv.Atoi(versionParts[2])
+		store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
+		store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
+		store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
+	}
+	platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(bridge.Config.WhatsApp.BrowserName)]
+	if ok {
+		store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
+	}
 }
 
 func (bridge *Bridge) Start() {
@@ -374,7 +400,7 @@ func (bridge *Bridge) StartUsers() {
 	bridge.Log.Debugln("Starting users")
 	foundAnySessions := false
 	for _, user := range bridge.GetAllUsers() {
-		if user.Session != nil {
+		if !user.JID.IsEmpty() {
 			foundAnySessions = true
 		}
 		go user.Connect(false)
@@ -401,15 +427,12 @@ func (bridge *Bridge) Stop() {
 	bridge.AS.Stop()
 	bridge.Metrics.Stop()
 	bridge.EventProcessor.Stop()
-	for _, user := range bridge.usersByJID {
-		if user.Conn == nil {
+	for _, user := range bridge.usersByUsername {
+		if user.Client == nil {
 			continue
 		}
 		bridge.Log.Debugln("Disconnecting", user.MXID)
-		err := user.Conn.Disconnect()
-		if err != nil {
-			bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err)
-		}
+		user.Client.Disconnect()
 	}
 }
 

+ 7 - 10
matrix.go

@@ -201,13 +201,10 @@ func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter
 	portal.UpdateBridgeInfo()
 	_, _ = intent.SendNotice(roomID, "Private chat portal created")
 
-	err := portal.FillInitialHistory(inviter)
-	if err != nil {
-		portal.log.Errorln("Failed to fill history:", err)
-	}
-
-	inviter.addPortalToCommunity(portal)
-	inviter.addPuppetToCommunity(puppet)
+	//err := portal.FillInitialHistory(inviter)
+	//if err != nil {
+	//	portal.log.Errorln("Failed to fill history:", err)
+	//}
 }
 
 func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) {
@@ -259,7 +256,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
 	}
 
 	user := mx.bridge.GetUserByMXID(evt.Sender)
-	if user == nil || !user.Whitelisted || !user.IsConnected() {
+	if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
 		return
 	}
 
@@ -300,7 +297,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
 	}
 
 	user := mx.bridge.GetUserByMXID(evt.Sender)
-	if user == nil || !user.Whitelisted || !user.IsConnected() {
+	if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
 		return
 	}
 
@@ -439,7 +436,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
 
 	if !user.HasSession() {
 		return
-	} else if !user.IsConnected() {
+	} else if !user.IsLoggedIn() {
 		msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+
 			"You are not connected to WhatsApp, so your redaction was not bridged. "+
 			"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)

+ 16 - 50
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
@@ -27,7 +27,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	log "maunium.net/go/maulogger/v2"
 
-	"github.com/Rhymen/go-whatsapp"
+	"go.mau.fi/whatsmeow/types"
 
 	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/id"
@@ -58,13 +58,10 @@ type MetricsHandler struct {
 	unencryptedGroupCount   prometheus.Gauge
 	unencryptedPrivateCount prometheus.Gauge
 
-	connected       prometheus.Gauge
-	connectedState  map[whatsapp.JID]bool
-	loggedIn        prometheus.Gauge
-	loggedInState   map[whatsapp.JID]bool
-	syncLocked      prometheus.Gauge
-	syncLockedState map[whatsapp.JID]bool
-	bufferLength    *prometheus.GaugeVec
+	connected      prometheus.Gauge
+	connectedState map[string]bool
+	loggedIn       prometheus.Gauge
+	loggedInState  map[string]bool
 }
 
 func NewMetricsHandler(address string, log log.Logger, db *database.Database) *MetricsHandler {
@@ -121,21 +118,12 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
 			Name: "bridge_logged_in",
 			Help: "Users logged into the bridge",
 		}),
-		loggedInState: make(map[whatsapp.JID]bool),
+		loggedInState: make(map[string]bool),
 		connected: promauto.NewGauge(prometheus.GaugeOpts{
 			Name: "bridge_connected",
 			Help: "Bridge users connected to WhatsApp",
 		}),
-		connectedState: make(map[whatsapp.JID]bool),
-		syncLocked: promauto.NewGauge(prometheus.GaugeOpts{
-			Name: "bridge_sync_locked",
-			Help: "Bridge users locked in post-login sync",
-		}),
-		syncLockedState: make(map[whatsapp.JID]bool),
-		bufferLength: promauto.NewGaugeVec(prometheus.GaugeOpts{
-			Name: "bridge_buffer_size",
-			Help: "Number of messages in buffer",
-		}, []string{"user_id"}),
+		connectedState: make(map[string]bool),
 	}
 }
 
@@ -154,7 +142,7 @@ func (mh *MetricsHandler) TrackMatrixEvent(eventType event.Type) func() {
 	}
 }
 
-func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType string) func() {
+func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp time.Time, messageType string) func() {
 	if !mh.running {
 		return noop
 	}
@@ -165,7 +153,7 @@ func (mh *MetricsHandler) TrackWhatsAppMessage(timestamp uint64, messageType str
 		mh.whatsappMessageHandling.
 			With(prometheus.Labels{"message_type": messageType}).
 			Observe(duration.Seconds())
-		mh.whatsappMessageAge.Observe(time.Now().Sub(time.Unix(int64(timestamp), 0)).Seconds())
+		mh.whatsappMessageAge.Observe(time.Now().Sub(timestamp).Seconds())
 	}
 }
 
@@ -176,13 +164,13 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
 	mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
 }
 
-func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
+func (mh *MetricsHandler) TrackLoginState(jid types.JID, loggedIn bool) {
 	if !mh.running {
 		return
 	}
-	currentVal, ok := mh.loggedInState[jid]
+	currentVal, ok := mh.loggedInState[jid.User]
 	if !ok || currentVal != loggedIn {
-		mh.loggedInState[jid] = loggedIn
+		mh.loggedInState[jid.User] = loggedIn
 		if loggedIn {
 			mh.loggedIn.Inc()
 		} else {
@@ -191,13 +179,13 @@ func (mh *MetricsHandler) TrackLoginState(jid whatsapp.JID, loggedIn bool) {
 	}
 }
 
-func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool) {
+func (mh *MetricsHandler) TrackConnectionState(jid types.JID, connected bool) {
 	if !mh.running {
 		return
 	}
-	currentVal, ok := mh.connectedState[jid]
+	currentVal, ok := mh.connectedState[jid.User]
 	if !ok || currentVal != connected {
-		mh.connectedState[jid] = connected
+		mh.connectedState[jid.User] = connected
 		if connected {
 			mh.connected.Inc()
 		} else {
@@ -206,28 +194,6 @@ func (mh *MetricsHandler) TrackConnectionState(jid whatsapp.JID, connected bool)
 	}
 }
 
-func (mh *MetricsHandler) TrackSyncLock(jid whatsapp.JID, locked bool) {
-	if !mh.running {
-		return
-	}
-	currentVal, ok := mh.syncLockedState[jid]
-	if !ok || currentVal != locked {
-		mh.syncLockedState[jid] = locked
-		if locked {
-			mh.syncLocked.Inc()
-		} else {
-			mh.syncLocked.Dec()
-		}
-	}
-}
-
-func (mh *MetricsHandler) TrackBufferLength(id id.UserID, length int) {
-	if !mh.running {
-		return
-	}
-	mh.bufferLength.With(prometheus.Labels{"user_id": string(id)}).Set(float64(length))
-}
-
 func (mh *MetricsHandler) updateStats() {
 	start := time.Now()
 	var puppetCount int

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 329 - 410
portal.go


+ 173 - 207
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
@@ -21,7 +21,6 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
-	"fmt"
 	"net"
 	"net/http"
 	"strings"
@@ -29,8 +28,6 @@ import (
 
 	"github.com/gorilla/websocket"
 
-	"github.com/Rhymen/go-whatsapp"
-
 	log "maunium.net/go/maulogger/v2"
 
 	"maunium.net/go/mautrix/id"
@@ -122,7 +119,7 @@ type Response struct {
 
 func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) {
 	user := r.Context().Value("user").(*User)
-	if user.Session == nil && user.Conn == nil {
+	if user.Session == nil && user.Client == nil {
 		jsonResponse(w, http.StatusNotFound, Error{
 			Error:   "Nothing to purge: no session information stored and no active connection.",
 			ErrCode: "no session",
@@ -130,13 +127,13 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
 		return
 	}
 	user.DeleteConnection()
-	user.SetSession(nil)
+	user.DeleteSession()
 	jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
 }
 
 func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) {
 	user := r.Context().Value("user").(*User)
-	if user.Conn == nil {
+	if user.Client == nil {
 		jsonResponse(w, http.StatusNotFound, Error{
 			Error:   "You don't have a WhatsApp connection.",
 			ErrCode: "not connected",
@@ -149,35 +146,20 @@ func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Req
 
 func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
 	user := r.Context().Value("user").(*User)
-	if user.Conn == nil {
+	if user.Client == nil {
 		jsonResponse(w, http.StatusNotFound, Error{
 			Error:   "You don't have a WhatsApp connection.",
 			ErrCode: "no connection",
 		})
 		return
 	}
-	err := user.Conn.Disconnect()
-	if err == whatsapp.ErrNotConnected {
-		jsonResponse(w, http.StatusNotFound, Error{
-			Error:   "You were not connected",
-			ErrCode: "not connected",
-		})
-		return
-	} else if err != nil {
-		user.log.Warnln("Error while disconnecting:", err)
-		jsonResponse(w, http.StatusInternalServerError, Error{
-			Error:   fmt.Sprintf("Unknown error while disconnecting: %v", err),
-			ErrCode: err.Error(),
-		})
-		return
-	}
-	user.bridge.Metrics.TrackConnectionState(user.JID, false)
+	user.DeleteConnection()
 	jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
 }
 
 func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
 	user := r.Context().Value("user").(*User)
-	if user.Conn == nil {
+	if user.Client == nil {
 		if user.Session == nil {
 			jsonResponse(w, http.StatusForbidden, Error{
 				Error:   "No existing connection and no session. Please log in first.",
@@ -190,68 +172,69 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	user.log.Debugln("Received /reconnect request, disconnecting")
-	wasConnected := true
-	err := user.Conn.Disconnect()
-	if err == whatsapp.ErrNotConnected {
-		wasConnected = false
-	} else if err != nil {
-		user.log.Warnln("Error while disconnecting:", err)
-	}
-
-	user.log.Debugln("Restoring session for /reconnect")
-	err = user.Conn.Restore(true, r.Context())
-	user.log.Debugfln("Restore session for /reconnect responded with %v", err)
-	if err == whatsapp.ErrInvalidSession {
-		if user.Session != nil {
-			user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
-			user.Conn.SetSession(*user.Session)
-			err = user.Conn.Restore(true, r.Context())
-		} else {
-			jsonResponse(w, http.StatusForbidden, Error{
-				Error:   "You're not logged in",
-				ErrCode: "not logged in",
-			})
-			return
-		}
-	}
-	if err == whatsapp.ErrLoginInProgress {
-		jsonResponse(w, http.StatusConflict, Error{
-			Error:   "A login or reconnection is already in progress.",
-			ErrCode: "login in progress",
-		})
-		return
-	} else if err == whatsapp.ErrAlreadyLoggedIn {
-		jsonResponse(w, http.StatusConflict, Error{
-			Error:   "You were already connected.",
-			ErrCode: err.Error(),
-		})
-		return
-	}
-	if err != nil {
-		user.log.Warnln("Error while reconnecting:", err)
-		jsonResponse(w, http.StatusInternalServerError, Error{
-			Error:   fmt.Sprintf("Unknown error while reconnecting: %v", err),
-			ErrCode: err.Error(),
-		})
-		user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
-		err = user.Conn.Disconnect()
-		if err != nil {
-			user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
-		}
-		return
-	}
-	user.ConnectionErrors = 0
-	user.PostLogin()
-
-	var msg string
-	if wasConnected {
-		msg = "Reconnected successfully."
-	} else {
-		msg = "Connected successfully."
-	}
-
-	jsonResponse(w, http.StatusOK, Response{true, msg})
+	// TODO reimplement
+	//user.log.Debugln("Received /reconnect request, disconnecting")
+	//wasConnected := true
+	//err := user.Conn.Disconnect()
+	//if err == whatsapp.ErrNotConnected {
+	//	wasConnected = false
+	//} else if err != nil {
+	//	user.log.Warnln("Error while disconnecting:", err)
+	//}
+	//
+	//user.log.Debugln("Restoring session for /reconnect")
+	//err = user.Conn.Restore(true, r.Context())
+	//user.log.Debugfln("Restore session for /reconnect responded with %v", err)
+	//if err == whatsapp.ErrInvalidSession {
+	//	if user.Session != nil {
+	//		user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
+	//		user.Conn.SetSession(*user.Session)
+	//		err = user.Conn.Restore(true, r.Context())
+	//	} else {
+	//		jsonResponse(w, http.StatusForbidden, Error{
+	//			Error:   "You're not logged in",
+	//			ErrCode: "not logged in",
+	//		})
+	//		return
+	//	}
+	//}
+	//if err == whatsapp.ErrLoginInProgress {
+	//	jsonResponse(w, http.StatusConflict, Error{
+	//		Error:   "A login or reconnection is already in progress.",
+	//		ErrCode: "login in progress",
+	//	})
+	//	return
+	//} else if err == whatsapp.ErrAlreadyLoggedIn {
+	//	jsonResponse(w, http.StatusConflict, Error{
+	//		Error:   "You were already connected.",
+	//		ErrCode: err.Error(),
+	//	})
+	//	return
+	//}
+	//if err != nil {
+	//	user.log.Warnln("Error while reconnecting:", err)
+	//	jsonResponse(w, http.StatusInternalServerError, Error{
+	//		Error:   fmt.Sprintf("Unknown error while reconnecting: %v", err),
+	//		ErrCode: err.Error(),
+	//	})
+	//	user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
+	//	err = user.Conn.Disconnect()
+	//	if err != nil {
+	//		user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
+	//	}
+	//	return
+	//}
+	//user.ConnectionErrors = 0
+	//user.PostLogin()
+	//
+	//var msg string
+	//if wasConnected {
+	//	msg = "Reconnected successfully."
+	//} else {
+	//	msg = "Connected successfully."
+	//}
+	//
+	//jsonResponse(w, http.StatusOK, Response{true, msg})
 }
 
 func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
@@ -259,32 +242,16 @@ func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
 	wa := map[string]interface{}{
 		"has_session":     user.Session != nil,
 		"management_room": user.ManagementRoom,
-		"jid":             user.JID,
 		"conn":            nil,
-		"ping":            nil,
 	}
-	if user.Conn != nil {
+	if user.JID.IsEmpty() {
+		wa["jid"] = user.JID.String()
+	}
+	if user.Client != nil {
 		wa["conn"] = map[string]interface{}{
-			"is_connected":         user.Conn.IsConnected(),
-			"is_logged_in":         user.Conn.IsLoggedIn(),
-			"is_login_in_progress": user.Conn.IsLoginInProgress(),
-		}
-		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()
+			"is_connected": user.Client.IsConnected(),
+			"is_logged_in": user.Client.IsLoggedIn,
 		}
-		wa["ping"] = map[string]interface{}{
-			"ok":  err == nil,
-			"err": errStr,
-		}
-		user.log.Debugfln("Admin test response for /ping: %v (conn: %t, login: %t, in progress: %t)",
-			err, user.Conn.IsConnected(), user.Conn.IsLoggedIn(), user.Conn.IsLoginInProgress())
 	}
 	resp := map[string]interface{}{
 		"mxid":                 user.MXID,
@@ -314,7 +281,7 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
 
 	force := strings.ToLower(r.URL.Query().Get("force")) != "false"
 
-	if user.Conn == nil {
+	if user.Client == nil {
 		if !force {
 			jsonResponse(w, http.StatusNotFound, Error{
 				Error:   "You're not connected",
@@ -322,26 +289,24 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
 			})
 		}
 	} else {
-		err := user.Conn.Logout()
-		if err != nil {
-			user.log.Warnln("Error while logging out:", err)
-			if !force {
-				jsonResponse(w, http.StatusInternalServerError, Error{
-					Error:   fmt.Sprintf("Unknown error while logging out: %v", err),
-					ErrCode: err.Error(),
-				})
-				return
-			}
-		}
+		// TODO reimplement
+		//err := user.Client.Logout()
+		//if err != nil {
+		//	user.log.Warnln("Error while logging out:", err)
+		//	if !force {
+		//		jsonResponse(w, http.StatusInternalServerError, Error{
+		//			Error:   fmt.Sprintf("Unknown error while logging out: %v", err),
+		//			ErrCode: err.Error(),
+		//		})
+		//		return
+		//	}
+		//}
 		user.DeleteConnection()
 	}
 
 	user.bridge.Metrics.TrackConnectionState(user.JID, false)
 	user.removeFromJIDMap(StateLoggedOut)
-
-	// TODO this causes a foreign key violation, which should be fixed
-	//ce.User.JID = ""
-	user.SetSession(nil)
+	user.DeleteSession()
 	jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
 }
 
@@ -353,87 +318,88 @@ var upgrader = websocket.Upgrader{
 }
 
 func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
-	userID := r.URL.Query().Get("user_id")
-	user := prov.bridge.GetUserByMXID(id.UserID(userID))
-
-	c, err := upgrader.Upgrade(w, r, nil)
-	if err != nil {
-		prov.log.Errorln("Failed to upgrade connection to websocket:", err)
-		return
-	}
-	defer c.Close()
-
-	if !user.Connect(true) {
-		user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
-		_ = c.WriteJSON(Error{
-			Error:   "Failed to connect to WhatsApp",
-			ErrCode: "connection error",
-		})
-		return
-	}
 
-	qrChan := make(chan string, 3)
-	go func() {
-		for code := range qrChan {
-			if code == "stop" {
-				return
-			}
-			_ = c.WriteJSON(map[string]interface{}{
-				"code": code,
-			})
-		}
-	}()
-
-	go func() {
-		// Read everything so SetCloseHandler() works
-		for {
-			_, _, err = c.ReadMessage()
-			if err != nil {
-				break
-			}
-		}
-	}()
-	ctx, cancel := context.WithCancel(context.Background())
-	c.SetCloseHandler(func(code int, text string) error {
-		user.log.Debugfln("Login websocket closed (%d), cancelling login", code)
-		cancel()
-		return nil
-	})
-
-	user.log.Debugln("Starting login via provisioning API")
-	session, jid, err := user.Conn.Login(qrChan, ctx)
-	qrChan <- "stop"
-	if err != nil {
-		var msg string
-		if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
-			msg = "You're already logged in"
-		} else if errors.Is(err, whatsapp.ErrLoginInProgress) {
-			msg = "You have a login in progress already."
-		} else if errors.Is(err, whatsapp.ErrLoginTimedOut) {
-			msg = "QR code scan timed out. Please try again."
-		} else if errors.Is(err, whatsapp.ErrInvalidWebsocket) {
-			msg = "WhatsApp connection error. Please try again."
-			// TODO might need to make sure it reconnects?
-		} else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
-			msg = "WhatsApp multi-device is not currently supported. Please disable it and try again."
-		} else {
-			msg = fmt.Sprintf("Unknown error while logging in: %v", err)
-		}
-		user.log.Warnln("Failed to log in:", err)
-		_ = c.WriteJSON(Error{
-			Error:   msg,
-			ErrCode: err.Error(),
-		})
-		return
-	}
-	user.log.Debugln("Successful login as", jid, "via provisioning API")
-	user.ConnectionErrors = 0
-	user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
-	user.addToJIDMap()
-	user.SetSession(&session)
-	_ = c.WriteJSON(map[string]interface{}{
-		"success": true,
-		"jid":     user.JID,
-	})
-	user.PostLogin()
+	// TODO reimplement
+	//userID := r.URL.Query().Get("user_id")
+	//user := prov.bridge.GetUserByMXID(id.UserID(userID))
+	//
+	//c, err := upgrader.Upgrade(w, r, nil)
+	//if err != nil {
+	//	prov.log.Errorln("Failed to upgrade connection to websocket:", err)
+	//	return
+	//}
+	//defer c.Close()
+	//if !user.Connect(true) {
+	//	user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
+	//	_ = c.WriteJSON(Error{
+	//		Error:   "Failed to connect to WhatsApp",
+	//		ErrCode: "connection error",
+	//	})
+	//	return
+	//}
+	//
+	//qrChan := make(chan string, 3)
+	//go func() {
+	//	for code := range qrChan {
+	//		if code == "stop" {
+	//			return
+	//		}
+	//		_ = c.WriteJSON(map[string]interface{}{
+	//			"code": code,
+	//		})
+	//	}
+	//}()
+	//
+	//go func() {
+	//	// Read everything so SetCloseHandler() works
+	//	for {
+	//		_, _, err = c.ReadMessage()
+	//		if err != nil {
+	//			break
+	//		}
+	//	}
+	//}()
+	//ctx, cancel := context.WithCancel(context.Background())
+	//c.SetCloseHandler(func(code int, text string) error {
+	//	user.log.Debugfln("Login websocket closed (%d), cancelling login", code)
+	//	cancel()
+	//	return nil
+	//})
+	//
+	//user.log.Debugln("Starting login via provisioning API")
+	//session, jid, err := user.Conn.Login(qrChan, ctx)
+	//qrChan <- "stop"
+	//if err != nil {
+	//	var msg string
+	//	if errors.Is(err, whatsapp.ErrAlreadyLoggedIn) {
+	//		msg = "You're already logged in"
+	//	} else if errors.Is(err, whatsapp.ErrLoginInProgress) {
+	//		msg = "You have a login in progress already."
+	//	} else if errors.Is(err, whatsapp.ErrLoginTimedOut) {
+	//		msg = "QR code scan timed out. Please try again."
+	//	} else if errors.Is(err, whatsapp.ErrInvalidWebsocket) {
+	//		msg = "WhatsApp connection error. Please try again."
+	//		// TODO might need to make sure it reconnects?
+	//	} else if errors.Is(err, whatsapp.ErrMultiDeviceNotSupported) {
+	//		msg = "WhatsApp multi-device is not currently supported. Please disable it and try again."
+	//	} else {
+	//		msg = fmt.Sprintf("Unknown error while logging in: %v", err)
+	//	}
+	//	user.log.Warnln("Failed to log in:", err)
+	//	_ = c.WriteJSON(Error{
+	//		Error:   msg,
+	//		ErrCode: err.Error(),
+	//	})
+	//	return
+	//}
+	//user.log.Debugln("Successful login as", jid, "via provisioning API")
+	//user.ConnectionErrors = 0
+	//user.JID = strings.Replace(jid, whatsapp.OldUserSuffix, whatsapp.NewUserSuffix, 1)
+	//user.addToJIDMap()
+	//user.SetSession(&session)
+	//_ = c.WriteJSON(map[string]interface{}{
+	//	"success": true,
+	//	"jid":     user.JID,
+	//})
+	//user.PostLogin()
 }

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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 293 - 868
user.go


Vissa filer visades inte eftersom för många filer har ändrats