From ded2fb97994c29286e7f5660a0a06ef79d906a13 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 27 Oct 2021 15:54:34 +0300 Subject: [PATCH] Implement logout and provisioning API login --- bridgestate.go | 2 +- commands.go | 142 ++++++++++--------------- go.mod | 16 +-- go.sum | 43 ++++---- main.go | 4 +- provisioning.go | 276 ++++++++++++++++++------------------------------ user.go | 79 +++++++------- 7 files changed, 226 insertions(+), 336 deletions(-) diff --git a/bridgestate.go b/bridgestate.go index 668e51b..d833bdb 100644 --- a/bridgestate.go +++ b/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 { diff --git a/commands.go b/commands.go index d56a162..8007eab 100644 --- a/commands.go +++ b/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") - } -} - -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) + qrChan, err := ce.User.Login(context.Background()) if err != nil { - user.log.Errorln("Failed to send QR code to user:", err) + ce.User.log.Errorf("Failed to log in:", err) + ce.Reply("Failed to log in: %v", err) return } - 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 + 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) sendQR(ce *CommandEvent, code string, prevEvent id.EventID) id.EventID { + url, ok := user.uploadQR(ce, code) + if !ok { + return prevEvent + } + content := event.MessageEventContent{ + MsgType: event.MsgImage, + Body: code, + URL: url.CUString(), + } + if len(prevEvent) != 0 { + content.SetEdit(prevEvent) + } + resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content) + if err != nil { + user.log.Errorln("Failed to send edited QR code to user:", err) + } else if len(prevEvent) == 0 { + prevEvent = resp.EventID + } + return prevEvent } func (user *User) uploadQR(ce *CommandEvent, code string) (id.ContentURI, bool) { @@ -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() diff --git a/go.mod b/go.mod index ccf8274..600a338 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index fad1236..0876509 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index 1ed0be3..77a7558 100644 --- a/main.go +++ b/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)) diff --git a/provisioning.go b/provisioning.go index 7def565..15be72c 100644 --- a/provisioning.go +++ b/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 + } + } } diff --git a/user.go b/user.go index d141256..01a5f16 100644 --- a/user.go +++ b/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)