Эх сурвалжийг харах

Implement logout and provisioning API login

Tulir Asokan 3 жил өмнө
parent
commit
ded2fb9799
7 өөрчлөгдсөн 224 нэмэгдсэн , 334 устгасан
  1. 1 1
      bridgestate.go
  2. 52 86
      commands.go
  3. 8 8
      go.mod
  4. 19 24
      go.sum
  5. 2 2
      main.go
  6. 100 176
      provisioning.go
  7. 42 37
      user.go

+ 1 - 1
bridgestate.go

@@ -188,7 +188,7 @@ func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Requ
 	var global BridgeState
 	global.StateEvent = StateRunning
 	var remote BridgeState
-	if user.Client != nil && user.Client.IsConnected() {
+	if user.IsConnected() {
 		if user.Client.IsLoggedIn {
 			remote.StateEvent = StateConnected
 		} else if user.Session != nil {

+ 52 - 86
commands.go

@@ -22,15 +22,14 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/skip2/go-qrcode"
 
-	"go.mau.fi/whatsmeow/types"
-	"go.mau.fi/whatsmeow/types/events"
-
 	"maunium.net/go/maulogger/v2"
 
+	"go.mau.fi/whatsmeow"
+	"go.mau.fi/whatsmeow/types"
+
 	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix/appservice"
 	"maunium.net/go/mautrix/event"
@@ -377,95 +376,63 @@ func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) {
 	}
 }
 
-const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
+const cmdLoginHelp = `login - Link the bridge to your WhatsApp account as a web client`
 
 // CommandLogin handles login command
 func (handler *CommandHandler) CommandLogin(ce *CommandEvent) {
 	if ce.User.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.")
+		if ce.User.IsConnected() {
+			ce.Reply("You're already logged in")
+		} else {
+			ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
+		}
 		return
 	}
 
-	select {
-	case success := <-loginChan:
-		ce.Reply("Successfully logged in as +%s (device #%d)", success.ID.User, success.ID.Device)
-		cancel()
-	case <-ctx.Done():
-		ce.Reply("Login timed out")
+	qrChan, err := ce.User.Login(context.Background())
+	if err != nil {
+		ce.User.log.Errorf("Failed to log in:", err)
+		ce.Reply("Failed to log in: %v", err)
+		return
+	}
+
+	var qrEventID id.EventID
+	for item := range qrChan {
+		switch item {
+		case whatsmeow.QRChannelSuccess:
+			jid := ce.User.Client.Store.ID
+			ce.Reply("Successfully logged in as +%s (device #%d)", jid.User, jid.Device)
+		case whatsmeow.QRChannelTimeout:
+			ce.Reply("QR code timed out. Please restart the login.")
+		case whatsmeow.QRChannelErrUnexpectedEvent:
+			ce.Reply("Failed to log in: unexpected connection event from server")
+		default:
+			qrEventID = ce.User.sendQR(ce, string(item), qrEventID)
+		}
 	}
+	_, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
 }
 
-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:]
+func (user *User) sendQR(ce *CommandEvent, code string, prevEvent id.EventID) id.EventID {
 	url, ok := user.uploadQR(ce, code)
 	if !ok {
-		return
+		return prevEvent
 	}
-	sendResp, err := bot.SendImage(ce.RoomID, code, url)
-	if err != nil {
-		user.log.Errorln("Failed to send QR code to user:", err)
-		return
+	content := event.MessageEventContent{
+		MsgType: event.MsgImage,
+		Body:    code,
+		URL:     url.CUString(),
 	}
-	qrEventID := sendResp.EventID
-
-	for {
-		select {
-		case <-time.After(qrEvt.Timeout):
-			if len(qrEvt.Codes) == 0 {
-				_, _ = bot.RedactEvent(ce.RoomID, qrEventID)
-				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():
-			_, _ = bot.RedactEvent(ce.RoomID, qrEventID)
-			return
-		}
+	if len(prevEvent) != 0 {
+		content.SetEdit(prevEvent)
 	}
+	resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content)
+	if err != nil {
+		user.log.Errorln("Failed to send edited QR code to user:", err)
+	} else if len(prevEvent) == 0 {
+		prevEvent = resp.EventID
+	}
+	return prevEvent
 }
 
 func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool) {
@@ -487,7 +454,7 @@ func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool)
 	return resp.ContentURI, true
 }
 
-const cmdLogoutHelp = `logout - Logout from WhatsApp`
+const cmdLogoutHelp = `logout - Unlink the bridge from your WhatsApp account`
 
 // CommandLogout handles !logout command
 func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
@@ -505,13 +472,12 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) {
 			ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
 		}
 	}
-	// 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
-	//}
+	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)
 	ce.User.DeleteConnection()
 	ce.User.DeleteSession()

+ 8 - 8
go.mod

@@ -8,13 +8,13 @@ require (
 	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-20211026140006-b484ee326162
+	go.mau.fi/whatsmeow v0.0.0-20211027125031-660696c7c724
 	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.30
+	maunium.net/go/maulogger/v2 v2.3.1
+	maunium.net/go/mautrix v0.9.31
 )
 
 require (
@@ -29,12 +29,12 @@ require (
 	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
+	github.com/tidwall/gjson v1.10.2 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/tidwall/sjson v1.2.3 // indirect
 	go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 // indirect
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
-	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
+	golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
 )

+ 19 - 24
go.sum

@@ -77,10 +77,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.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.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=
@@ -129,26 +127,25 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w=
-github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI=
-github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
-github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
-github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
-github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
-github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
+github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
+github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
 go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ=
 go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos=
-go.mau.fi/whatsmeow v0.0.0-20211026140006-b484ee326162 h1:nwQ9gDQsvAmhW6B2a97RV0bkO9PEb7C7UZiMEYADRtw=
-go.mau.fi/whatsmeow v0.0.0-20211026140006-b484ee326162/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
+go.mau.fi/whatsmeow v0.0.0-20211027125031-660696c7c724 h1:vk1AkBxc0tVEmPa5mUrzMNwA5wMe/yKrILr32xgW4KA=
+go.mau.fi/whatsmeow v0.0.0-20211027125031-660696c7c724/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-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=
@@ -160,9 +157,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
+golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -175,17 +172,16 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 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=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -221,8 +217,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
 maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
-maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/maulogger/v2 v2.3.0 h1:TMCcO65fLk6+pJXo7sl38tzjzW0KBFgc6JWJMBJp4GE=
-maunium.net/go/maulogger/v2 v2.3.0/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.9.30 h1:iOJ9Cl576jxCL1x8J3bKQx29nc5hoewZlVyPmNWdzF8=
-maunium.net/go/mautrix v0.9.30/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8=
+maunium.net/go/maulogger/v2 v2.3.1 h1:fwBYJne0pHvJrrIPHK+TAPfyxxbBEz46oVGez2x0ODE=
+maunium.net/go/maulogger/v2 v2.3.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
+maunium.net/go/mautrix v0.9.31 h1:n7UF5tqq2zCyfdNsv++RyQ2anjjrFVOmOA2VkZCSgZc=
+maunium.net/go/mautrix v0.9.31/go.mod h1:3U7pOAx4bxdIVJuunLDAToI+M7YwxcGMm74zBmX5aY0=

+ 2 - 2
main.go

@@ -365,7 +365,7 @@ func (bridge *Bridge) LoadRelaybot() {
 	}
 	bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom
 	bridge.Relaybot.IsRelaybot = true
-	bridge.Relaybot.Connect(false)
+	bridge.Relaybot.Connect()
 }
 
 func (bridge *Bridge) UpdateBotProfile() {
@@ -403,7 +403,7 @@ func (bridge *Bridge) StartUsers() {
 		if !user.JID.IsEmpty() {
 			foundAnySessions = true
 		}
-		go user.Connect(false)
+		go user.Connect()
 	}
 	if !foundAnySessions {
 		bridge.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))

+ 100 - 176
provisioning.go

@@ -21,6 +21,7 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net"
 	"net/http"
 	"strings"
@@ -28,6 +29,8 @@ import (
 
 	"github.com/gorilla/websocket"
 
+	"go.mau.fi/whatsmeow"
+
 	log "maunium.net/go/maulogger/v2"
 
 	"maunium.net/go/mautrix/id"
@@ -47,11 +50,13 @@ func (prov *ProvisioningAPI) Init() {
 	r.HandleFunc("/login", prov.Login).Methods(http.MethodGet)
 	r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost)
 	r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost)
-	r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost)
 	r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost)
 	r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost)
 	prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost)
 	prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost)
+
+	// Deprecated, just use /disconnect
+	r.HandleFunc("/delete_connection", prov.Disconnect).Methods(http.MethodPost)
 }
 
 type responseWrap struct {
@@ -131,19 +136,6 @@ func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Reques
 	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.Client == nil {
-		jsonResponse(w, http.StatusNotFound, Error{
-			Error:   "You don't have a WhatsApp connection.",
-			ErrCode: "not connected",
-		})
-		return
-	}
-	user.DeleteConnection()
-	jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"})
-}
-
 func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
 	user := r.Context().Value("user").(*User)
 	if user.Client == nil {
@@ -166,75 +158,14 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
 				ErrCode: "no session",
 			})
 		} else {
-			user.Connect(false)
-			jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."})
+			user.Connect()
+			jsonResponse(w, http.StatusAccepted, Response{true, "Created connection to WhatsApp."})
 		}
-		return
+	} else {
+		user.DeleteConnection()
+		user.Connect()
+		jsonResponse(w, http.StatusAccepted, Response{true, "Restarted connection to WhatsApp"})
 	}
-
-	// 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) {
@@ -289,18 +220,17 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
 			})
 		}
 	} else {
-		// 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
-		//	}
-		//}
+		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()
 	}
 
@@ -318,88 +248,82 @@ 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))
 
-	// 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()
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		prov.log.Errorln("Failed to upgrade connection to websocket:", err)
+		return
+	}
+	defer func() {
+		err := c.Close()
+		if err != nil {
+			user.log.Debugln("Error closing websocket:", err)
+		}
+	}()
+
+	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
+	})
+
+	qrChan, err := user.Login(ctx)
+	if err != nil {
+		user.log.Errorf("Failed to log in from provisioning API:", err)
+		if errors.Is(err, ErrAlreadyLoggedIn) {
+			go user.Connect()
+			_ = c.WriteJSON(Error{
+				Error:   "You're already logged into WhatsApp",
+				ErrCode: "already logged in",
+			})
+		} else {
+			_ = c.WriteJSON(Error{
+				Error:   "Failed to connect to WhatsApp",
+				ErrCode: "connection error",
+			})
+		}
+	}
+	user.log.Debugln("Started login via provisioning API")
+
+	for {
+		select {
+		case evt := <-qrChan:
+			switch evt {
+			case whatsmeow.QRChannelSuccess:
+				jid := user.Client.Store.ID
+				user.log.Debugln("Successful login as", jid, "via provisioning API")
+				_ = c.WriteJSON(map[string]interface{}{
+					"success": true,
+					"jid":     jid,
+					"phone":   fmt.Sprintf("+%s", jid.User),
+				})
+			case whatsmeow.QRChannelTimeout:
+				user.log.Debugln("Login via provisioning API timed out")
+				_ = c.WriteJSON(Error{
+					Error:   "QR code scan timed out. Please try again.",
+					ErrCode: "login timed out",
+				})
+			default:
+				_ = c.WriteJSON(map[string]interface{}{
+					"code": string(evt),
+				})
+				continue
+			}
+			return
+		case <-ctx.Done():
+			return
+		}
+	}
 }

+ 42 - 37
user.go

@@ -17,6 +17,7 @@
 package main
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -62,9 +63,6 @@ type User struct {
 	mgmtCreateLock sync.Mutex
 	connLock       sync.Mutex
 
-	qrListener    chan<- *events.QR
-	loginListener chan<- *events.PairSuccess
-
 	historySyncs chan *events.HistorySync
 
 	prevBridgeStatus *BridgeState
@@ -141,7 +139,7 @@ func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User {
 		var err error
 		user.Session, err = bridge.WAContainer.GetDevice(user.JID)
 		if err != nil {
-			user.log.Errorfln("Failed to scan user's whatsapp session: %v", err)
+			user.log.Errorfln("Failed to load user's whatsapp session: %v", err)
 		} else if user.Session == nil {
 			user.log.Warnfln("Didn't find session data for %s, treating user as logged out", user.JID)
 			user.JID = types.EmptyJID
@@ -238,25 +236,41 @@ func (w *waLogger) Warnf(msg string, args ...interface{})  { w.l.Warnfln(msg, ar
 func (w *waLogger) Errorf(msg string, args ...interface{}) { w.l.Errorfln(msg, args...) }
 func (w *waLogger) Sub(module string) waLog.Logger         { return &waLogger{l: w.l.Sub(module)} }
 
-func (user *User) Connect(evenIfNoSession bool) bool {
+var ErrAlreadyLoggedIn = errors.New("already logged in")
+
+func (user *User) Login(ctx context.Context) (<-chan whatsmeow.QRChannelItem, error) {
+	user.connLock.Lock()
+	defer user.connLock.Unlock()
+	if user.Session != nil {
+		return nil, ErrAlreadyLoggedIn
+	} else if user.Client != nil {
+		user.unlockedDeleteConnection()
+	}
+	newSession := user.bridge.WAContainer.NewDevice()
+	newSession.Log = &waLogger{user.log.Sub("Session")}
+	user.Client = whatsmeow.NewClient(newSession, &waLogger{user.log.Sub("Client")})
+	qrChan, err := user.Client.GetQRChannel(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get QR channel: %w", err)
+	}
+	err = user.Client.Connect()
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect to WhatsApp: %w", err)
+	}
+	return qrChan, nil
+}
+
+func (user *User) Connect() bool {
 	user.connLock.Lock()
 	defer user.connLock.Unlock()
 	if user.Client != nil {
 		return user.Client.IsConnected()
-	} else if !evenIfNoSession && user.Session == nil {
+	} else if user.Session == nil {
 		return false
 	}
 	user.log.Debugln("Connecting to WhatsApp")
-	if user.Session != nil {
-		user.sendBridgeState(BridgeState{StateEvent: StateConnecting, Error: WAConnecting})
-	}
-	if user.Session == nil {
-		newSession := user.bridge.WAContainer.NewDevice()
-		newSession.Log = &waLogger{user.log.Sub("Session")}
-		user.Client = whatsmeow.NewClient(newSession, &waLogger{user.log.Sub("Client")})
-	} else {
-		user.Client = whatsmeow.NewClient(user.Session, &waLogger{user.log.Sub("Client")})
-	}
+	user.sendBridgeState(BridgeState{StateEvent: StateConnecting, Error: WAConnecting})
+	user.Client = whatsmeow.NewClient(user.Session, &waLogger{user.log.Sub("Client")})
 	user.Client.AddEventHandler(user.HandleEvent)
 	err := user.Client.Connect()
 	if err != nil {
@@ -266,9 +280,7 @@ func (user *User) Connect(evenIfNoSession bool) bool {
 	return true
 }
 
-func (user *User) DeleteConnection() {
-	user.connLock.Lock()
-	defer user.connLock.Unlock()
+func (user *User) unlockedDeleteConnection() {
 	if user.Client == nil {
 		return
 	}
@@ -276,6 +288,12 @@ func (user *User) DeleteConnection() {
 	user.Client.RemoveEventHandlers()
 	user.Client = nil
 	user.bridge.Metrics.TrackConnectionState(user.JID, false)
+}
+
+func (user *User) DeleteConnection() {
+	user.connLock.Lock()
+	defer user.connLock.Unlock()
+	user.unlockedDeleteConnection()
 	user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotConnected})
 }
 
@@ -297,8 +315,12 @@ func (user *User) DeleteSession() {
 	}
 }
 
+func (user *User) IsConnected() bool {
+	return user.Client != nil && user.Client.IsConnected()
+}
+
 func (user *User) IsLoggedIn() bool {
-	return user.Client != nil && user.Client.IsConnected() && user.Client.IsLoggedIn
+	return user.IsConnected() && user.Client.IsLoggedIn
 }
 
 func (user *User) tryAutomaticDoublePuppeting() {
@@ -400,23 +422,6 @@ func (user *User) HandleEvent(event interface{}) {
 		user.addToJIDMap()
 		user.Update()
 		user.Session = user.Client.Store
-		if user.loginListener != nil {
-			select {
-			case user.loginListener <- v:
-				return
-			default:
-			}
-		}
-		user.log.Warnln("Got pair success event, but nothing waiting for it")
-	case *events.QR:
-		if user.qrListener != nil {
-			select {
-			case user.qrListener <- v:
-				return
-			default:
-			}
-		}
-		user.log.Warnln("Got QR code event, but nothing waiting for it")
 	case *events.ConnectFailure, *events.StreamError:
 		go user.sendBridgeState(BridgeState{StateEvent: StateUnknownError})
 		user.bridge.Metrics.TrackConnectionState(user.JID, false)