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"
"time"
2021-08-04 15:14:26 +02:00
log "maunium.net/go/maulogger/v2"
2021-06-09 18:15:53 +02:00
2021-06-01 14:19:47 +02:00
"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-01-25 13:26:24 +01:00
WALoggedOut BridgeErrorCode = "wa-logged-out"
2022-03-15 15:04:10 +01:00
WAAccountBanned BridgeErrorCode = "wa-account-banned"
WAUnknownLogout BridgeErrorCode = "wa-unknown-logout"
2021-06-15 14:07:42 +02:00
WANotConnected BridgeErrorCode = "wa-not-connected"
WAConnecting BridgeErrorCode = "wa-connecting"
WAServerTimeout BridgeErrorCode = "wa-server-timeout"
2022-01-25 13:26:24 +01:00
WAPhoneOffline BridgeErrorCode = "wa-phone-offline"
2021-06-01 14:19:47 +02:00
)
var bridgeHumanErrors = map [ BridgeErrorCode ] string {
2022-01-25 13:26:24 +01:00
WALoggedOut : "You were logged out from another device. Relogin to continue using the bridge." ,
2022-03-15 15:04:10 +01:00
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." ,
2021-06-15 14:07:42 +02:00
WANotConnected : "You're not connected to WhatsApp" ,
2021-11-08 16:31:50 +01:00
WAConnecting : "Reconnecting to WhatsApp..." ,
2021-06-15 14:07:42 +02:00
WAServerTimeout : "The WhatsApp web servers are not responding. The bridge will try to reconnect." ,
2022-01-25 13:26:24 +01:00
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." ,
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" `
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 ( )
}
2021-08-04 15:14:26 +02:00
func ( bridge * Bridge ) createBridgeStateRequest ( ctx context . Context , state * BridgeState ) ( req * http . Request , err error ) {
2021-06-02 20:43:31 +02:00
var body bytes . Buffer
if err = json . NewEncoder ( & body ) . Encode ( & state ) ; err != nil {
return nil , fmt . Errorf ( "failed to encode bridge state JSON: %w" , err )
}
2021-08-04 15:14:26 +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 {
return
}
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" )
return
}
2021-08-04 15:14:26 +02:00
func sendPreparedBridgeStateRequest ( logger log . Logger , req * http . Request ) bool {
resp , err := http . DefaultClient . Do ( req )
if err != nil {
logger . Warnln ( "Failed to send bridge state update:" , err )
return false
}
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" ) )
}
logger . Warnfln ( "Unexpected status code %d sending bridge state update: %s" , resp . StatusCode , respBody )
return false
}
return true
}
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 ( )
if req , err := bridge . createBridgeStateRequest ( ctx , & state ) ; err != nil {
bridge . Log . Warnln ( "Failed to prepare global bridge state update request:" , err )
} else if ok := sendPreparedBridgeStateRequest ( bridge . Log , req ) ; ok {
bridge . Log . Debugfln ( "Sent new global bridge state %+v" , state )
}
}
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
if user . prevBridgeStatus != nil && user . prevBridgeStatus . shouldDeduplicate ( & state ) {
return
}
2021-06-01 14:57:03 +02:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
2021-06-01 14:19:47 +02:00
defer cancel ( )
2021-08-04 15:14:26 +02:00
if req , err := user . bridge . createBridgeStateRequest ( ctx , & state ) ; err != nil {
2021-06-01 14:19:47 +02:00
user . log . Warnln ( "Failed to prepare bridge state update request:" , err )
2021-08-04 15:14:26 +02:00
} else if ok := sendPreparedBridgeStateRequest ( user . log , req ) ; ok {
2021-06-01 14:19:47 +02:00
user . prevBridgeStatus = & state
2021-06-01 14:57:03 +02:00
user . log . Debugfln ( "Sent new bridge state %+v" , 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
}