Implement logout and provisioning API login

This commit is contained in:
Tulir Asokan 2021-10-27 15:54:34 +03:00
parent 7aa838dce6
commit ded2fb9799
7 changed files with 226 additions and 336 deletions

View file

@ -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 {

View file

@ -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()

16
go.mod
View file

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

43
go.sum
View file

@ -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=

View file

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

View file

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

79
user.go
View file

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