2021-06-01 14:19:47 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2021 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"bytes"
"context"
"encoding/json"
2021-06-02 20:43:31 +02:00
"fmt"
2021-10-31 12:30:19 +01:00
"io"
2021-06-01 14:19:47 +02:00
"net/http"
2022-04-22 12:26:37 +02:00
"runtime/debug"
2021-06-01 14:19:47 +02:00
"time"
"maunium.net/go/mautrix/id"
)
2021-08-04 15:14:26 +02:00
type BridgeStateEvent string
const (
StateUnconfigured BridgeStateEvent = "UNCONFIGURED"
2021-08-25 14:04:40 +02:00
StateRunning BridgeStateEvent = "RUNNING"
2021-08-04 15:14:26 +02:00
StateConnecting BridgeStateEvent = "CONNECTING"
StateBackfilling BridgeStateEvent = "BACKFILLING"
StateConnected BridgeStateEvent = "CONNECTED"
StateTransientDisconnect BridgeStateEvent = "TRANSIENT_DISCONNECT"
StateBadCredentials BridgeStateEvent = "BAD_CREDENTIALS"
StateUnknownError BridgeStateEvent = "UNKNOWN_ERROR"
StateLoggedOut BridgeStateEvent = "LOGGED_OUT"
)
2021-06-01 14:19:47 +02:00
type BridgeErrorCode string
const (
2022-04-20 12:48:58 +02:00
WALoggedOut BridgeErrorCode = "wa-logged-out"
WAAccountBanned BridgeErrorCode = "wa-account-banned"
WAUnknownLogout BridgeErrorCode = "wa-unknown-logout"
WANotConnected BridgeErrorCode = "wa-not-connected"
WAConnecting BridgeErrorCode = "wa-connecting"
WAServerTimeout BridgeErrorCode = "wa-server-timeout"
WAPhoneOffline BridgeErrorCode = "wa-phone-offline"
WAConnectionFailed BridgeErrorCode = "wa-connection-failed"
2021-06-01 14:19:47 +02:00
)
var bridgeHumanErrors = map [ BridgeErrorCode ] string {
2022-04-20 12:48:58 +02:00
WALoggedOut : "You were logged out from another device. Relogin to continue using the bridge." ,
WAAccountBanned : "Your account was banned from WhatsApp. You can contact support from the WhatsApp mobile app on your phone." ,
WAUnknownLogout : "You were logged out for an unknown reason. Relogin to continue using the bridge." ,
WANotConnected : "You're not connected to WhatsApp" ,
WAConnecting : "Reconnecting to WhatsApp..." ,
WAServerTimeout : "The WhatsApp web servers are not responding. The bridge will try to reconnect." ,
WAPhoneOffline : "Your phone hasn't been seen in over 12 days. The bridge is currently connected, but will get disconnected if you don't open the app soon." ,
WAConnectionFailed : "Connecting to the WhatsApp web servers failed." ,
2021-06-01 14:19:47 +02:00
}
type BridgeState struct {
2021-08-04 15:14:26 +02:00
StateEvent BridgeStateEvent ` json:"state_event" `
Timestamp int64 ` json:"timestamp" `
TTL int ` json:"ttl" `
2021-06-09 18:15:53 +02:00
2021-08-05 14:44:55 +02:00
Source string ` json:"source,omitempty" `
Error BridgeErrorCode ` json:"error,omitempty" `
Message string ` json:"message,omitempty" `
2021-06-01 14:19:47 +02:00
2021-08-04 15:14:26 +02:00
UserID id . UserID ` json:"user_id,omitempty" `
RemoteID string ` json:"remote_id,omitempty" `
RemoteName string ` json:"remote_name,omitempty" `
2022-04-20 12:48:58 +02:00
Reason string ` json:"reason,omitempty" `
Info map [ string ] interface { } ` json:"info,omitempty" `
2021-06-01 14:19:47 +02:00
}
2021-08-25 14:04:40 +02:00
type GlobalBridgeState struct {
RemoteStates map [ string ] BridgeState ` json:"remoteState" `
BridgeState BridgeState ` json:"bridgeState" `
}
2021-08-04 15:14:26 +02:00
func ( pong BridgeState ) fill ( user * User ) BridgeState {
if user != nil {
pong . UserID = user . MXID
2021-10-29 15:50:29 +02:00
pong . RemoteID = fmt . Sprintf ( "%s_a%d_d%d" , user . JID . User , user . JID . Agent , user . JID . Device )
2021-10-22 19:14:34 +02:00
pong . RemoteName = fmt . Sprintf ( "+%s" , user . JID . User )
2021-08-04 15:14:26 +02:00
}
2021-06-09 18:15:53 +02:00
2021-06-01 14:19:47 +02:00
pong . Timestamp = time . Now ( ) . Unix ( )
2021-08-05 14:44:55 +02:00
pong . Source = "bridge"
2021-08-04 15:14:26 +02:00
if len ( pong . Error ) > 0 {
2021-06-01 14:19:47 +02:00
pong . TTL = 60
pong . Message = bridgeHumanErrors [ pong . Error ]
} else {
pong . TTL = 240
}
2021-08-04 15:14:26 +02:00
return pong
2021-06-01 14:19:47 +02:00
}
func ( pong * BridgeState ) shouldDeduplicate ( newPong * BridgeState ) bool {
2021-08-04 15:14:26 +02:00
if pong == nil || pong . StateEvent != newPong . StateEvent || pong . Error != newPong . Error {
2021-06-01 14:19:47 +02:00
return false
}
return pong . Timestamp + int64 ( pong . TTL / 5 ) > time . Now ( ) . Unix ( )
}
2022-04-22 12:26:37 +02:00
func ( bridge * Bridge ) sendBridgeState ( ctx context . Context , state * BridgeState ) error {
2021-06-02 20:43:31 +02:00
var body bytes . Buffer
2022-04-22 12:26:37 +02:00
if err := json . NewEncoder ( & body ) . Encode ( & state ) ; err != nil {
return fmt . Errorf ( "failed to encode bridge state JSON: %w" , err )
2021-06-02 20:43:31 +02:00
}
2022-04-22 12:26:37 +02:00
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , bridge . Config . Homeserver . StatusEndpoint , & body )
2021-06-02 20:43:31 +02:00
if err != nil {
2022-04-22 12:26:37 +02:00
return fmt . Errorf ( "failed to prepare request: %w" , err )
2021-06-02 20:43:31 +02:00
}
2021-08-04 15:14:26 +02:00
req . Header . Set ( "Authorization" , "Bearer " + bridge . Config . AppService . ASToken )
2021-06-02 20:43:31 +02:00
req . Header . Set ( "Content-Type" , "application/json" )
2021-08-04 15:14:26 +02:00
resp , err := http . DefaultClient . Do ( req )
if err != nil {
2022-04-22 12:26:37 +02:00
return fmt . Errorf ( "failed to send request: %w" , err )
2021-08-04 15:14:26 +02:00
}
defer resp . Body . Close ( )
if resp . StatusCode < 200 || resp . StatusCode > 299 {
2021-10-31 12:30:19 +01:00
respBody , _ := io . ReadAll ( resp . Body )
2021-08-04 15:14:26 +02:00
if respBody != nil {
respBody = bytes . ReplaceAll ( respBody , [ ] byte ( "\n" ) , [ ] byte ( "\\n" ) )
}
2022-04-22 12:26:37 +02:00
return fmt . Errorf ( "unexpected status code %d sending bridge state update: %s" , resp . StatusCode , respBody )
2021-08-04 15:14:26 +02:00
}
2022-04-22 12:26:37 +02:00
return nil
2021-08-04 15:14:26 +02:00
}
func ( bridge * Bridge ) sendGlobalBridgeState ( state BridgeState ) {
if len ( bridge . Config . Homeserver . StatusEndpoint ) == 0 {
return
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
2022-04-22 12:26:37 +02:00
if err := bridge . sendBridgeState ( ctx , & state ) ; err != nil {
bridge . Log . Warnln ( "Failed to update global bridge state:" , err )
} else {
2021-08-04 15:14:26 +02:00
bridge . Log . Debugfln ( "Sent new global bridge state %+v" , state )
}
}
2022-04-22 12:26:37 +02:00
func ( user * User ) bridgeStateLoop ( ) {
defer func ( ) {
err := recover ( )
if err != nil {
user . log . Errorfln ( "Bridge state loop panicked: %v\n%s" , err , debug . Stack ( ) )
}
} ( )
for state := range user . bridgeStateQueue {
user . immediateSendBridgeState ( state )
}
}
func ( user * User ) immediateSendBridgeState ( state BridgeState ) {
retryIn := 2
for {
if user . prevBridgeStatus != nil && user . prevBridgeStatus . shouldDeduplicate ( & state ) {
user . log . Debugfln ( "Not sending bridge state %s as it's a duplicate" , state . StateEvent )
return
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
err := user . bridge . sendBridgeState ( ctx , & state )
cancel ( )
if err != nil {
user . log . Warnfln ( "Failed to update bridge state: %v, retrying in %d seconds" , err , retryIn )
time . Sleep ( time . Duration ( retryIn ) * time . Second )
retryIn *= 2
if retryIn > 64 {
retryIn = 64
}
} else {
user . log . Debugfln ( "Sent new bridge state %+v" , state )
return
}
}
}
2021-06-01 14:19:47 +02:00
func ( user * User ) sendBridgeState ( state BridgeState ) {
if len ( user . bridge . Config . Homeserver . StatusEndpoint ) == 0 {
return
}
2021-08-04 15:14:26 +02:00
state = state . fill ( user )
2021-06-01 14:19:47 +02:00
2022-04-22 12:26:37 +02:00
if len ( user . bridgeStateQueue ) >= 8 {
user . log . Warnln ( "Bridge state queue is nearly full, discarding an item" )
select {
case <- user . bridgeStateQueue :
default :
}
}
select {
case user . bridgeStateQueue <- state :
default :
user . log . Errorfln ( "Bridge state queue is full, dropped new state" )
2021-06-01 14:19:47 +02:00
}
}
2022-01-25 13:26:24 +01:00
func ( user * User ) GetPrevBridgeState ( ) BridgeState {
if user . prevBridgeStatus != nil {
return * user . prevBridgeStatus
}
return BridgeState { }
}
2021-06-01 14:19:47 +02:00
func ( prov * ProvisioningAPI ) BridgeStatePing ( w http . ResponseWriter , r * http . Request ) {
if ! prov . bridge . AS . CheckServerToken ( w , r ) {
return
}
userID := r . URL . Query ( ) . Get ( "user_id" )
user := prov . bridge . GetUserByMXID ( id . UserID ( userID ) )
2021-08-25 14:04:40 +02:00
var global BridgeState
global . StateEvent = StateRunning
var remote BridgeState
2021-10-27 14:54:34 +02:00
if user . IsConnected ( ) {
2021-11-30 14:14:56 +01:00
if user . Client . IsLoggedIn ( ) {
2021-10-22 19:14:34 +02:00
remote . StateEvent = StateConnected
} else if user . Session != nil {
2021-08-25 14:04:40 +02:00
remote . StateEvent = StateConnecting
remote . Error = WAConnecting
2021-09-23 20:04:20 +02:00
} // else: unconfigured
} else if user . Session != nil {
remote . StateEvent = StateBadCredentials
remote . Error = WANotConnected
} // else: unconfigured
2021-08-25 14:04:40 +02:00
global = global . fill ( nil )
2021-09-23 20:04:20 +02:00
resp := GlobalBridgeState {
BridgeState : global ,
RemoteStates : map [ string ] BridgeState { } ,
}
2021-08-25 14:04:40 +02:00
if len ( remote . StateEvent ) > 0 {
remote = remote . fill ( user )
2021-09-23 20:04:20 +02:00
resp . RemoteStates [ remote . RemoteID ] = remote
2021-08-25 14:04:40 +02:00
}
2021-06-01 14:57:03 +02:00
user . log . Debugfln ( "Responding bridge state in bridge status endpoint: %+v" , resp )
2021-06-01 14:19:47 +02:00
jsonResponse ( w , http . StatusOK , & resp )
2021-08-25 14:04:40 +02:00
if len ( resp . RemoteStates ) > 0 {
user . prevBridgeStatus = & remote
}
2021-06-01 14:19:47 +02:00
}