2018-08-13 22:24:44 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
2022-03-01 19:25:46 +01:00
// Copyright (C) 2022 Tulir Asokan
2018-08-13 22:24:44 +02:00
//
// 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
2018-08-16 14:59:18 +02:00
import (
2021-10-27 14:54:34 +02:00
"context"
2022-05-17 18:30:46 +02:00
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
2019-03-14 00:06:06 +01:00
"encoding/json"
2020-09-24 14:25:36 +02:00
"errors"
2019-05-15 22:04:09 +02:00
"fmt"
2022-01-25 13:26:24 +01:00
"math"
2020-08-22 12:07:55 +02:00
"net/http"
2021-11-09 16:49:34 +01:00
"strconv"
2021-12-29 20:40:08 +01:00
"strings"
2019-05-22 22:05:58 +02:00
"sync"
2018-08-16 14:59:18 +02:00
"time"
2018-08-24 18:46:14 +02:00
2019-01-11 20:17:31 +01:00
log "maunium.net/go/maulogger/v2"
2021-05-18 14:23:19 +02:00
2021-12-25 19:50:36 +01:00
"maunium.net/go/mautrix"
2021-04-19 21:14:32 +02:00
"maunium.net/go/mautrix/appservice"
2021-12-25 19:50:36 +01:00
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
2021-04-19 21:14:32 +02:00
"maunium.net/go/mautrix/pushrules"
2019-01-11 20:17:31 +01:00
2021-10-22 19:14:34 +02:00
"go.mau.fi/whatsmeow"
2021-12-25 19:50:36 +01:00
"go.mau.fi/whatsmeow/appstate"
2022-01-28 14:06:19 +01:00
waProto "go.mau.fi/whatsmeow/binary/proto"
2021-10-22 19:14:34 +02:00
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
2019-05-21 22:44:14 +02:00
2018-08-24 18:46:14 +02:00
"maunium.net/go/mautrix-whatsapp/database"
2018-08-16 14:59:18 +02:00
)
type User struct {
* database . User
2021-10-22 19:14:34 +02:00
Client * whatsmeow . Client
Session * store . Device
2018-08-16 14:59:18 +02:00
bridge * Bridge
2018-08-16 18:20:07 +02:00
log log . Logger
2018-08-16 14:59:18 +02:00
2021-10-28 13:03:55 +02:00
Admin bool
Whitelisted bool
RelayWhitelisted bool
2019-11-10 20:22:11 +01:00
2021-12-29 13:19:16 +01:00
mgmtCreateLock sync . Mutex
spaceCreateLock sync . Mutex
connLock sync . Mutex
2019-05-22 22:05:58 +02:00
2021-11-08 12:04:54 +01:00
historySyncs chan * events . HistorySync
2021-06-01 14:19:47 +02:00
prevBridgeStatus * BridgeState
2021-11-08 12:04:54 +01:00
lastPresence types . Presence
2021-12-29 20:40:08 +01:00
2022-04-19 19:54:05 +02:00
historySyncLoopsStarted bool
2022-01-25 13:26:24 +01:00
spaceMembershipChecked bool
lastPhoneOfflineWarning time . Time
2022-03-01 19:25:46 +01:00
groupListCache [ ] * types . GroupInfo
groupListCacheLock sync . Mutex
groupListCacheTime time . Time
2022-03-24 21:21:23 +01:00
2022-04-22 12:26:37 +02:00
bridgeStateQueue chan BridgeState
2022-03-24 21:21:23 +01:00
BackfillQueue * BackfillQueue
2018-08-28 23:40:54 +02:00
}
2018-08-26 16:08:37 +02:00
2021-12-07 15:02:51 +01:00
func ( bridge * Bridge ) getUserByMXID ( userID id . UserID , onlyIfExists bool ) * User {
2019-05-16 19:14:32 +02:00
_ , isPuppet := bridge . ParsePuppetMXID ( userID )
if isPuppet || userID == bridge . Bot . UserID {
return nil
}
2018-08-28 23:40:54 +02:00
bridge . usersLock . Lock ( )
defer bridge . usersLock . Unlock ( )
user , ok := bridge . usersByMXID [ userID ]
if ! ok {
2021-12-07 15:02:51 +01:00
userIDPtr := & userID
if onlyIfExists {
userIDPtr = nil
}
return bridge . loadDBUser ( bridge . DB . User . GetByMXID ( userID ) , userIDPtr )
2018-08-28 23:40:54 +02:00
}
return user
2018-08-16 14:59:18 +02:00
}
2021-12-07 15:02:51 +01:00
func ( bridge * Bridge ) GetUserByMXID ( userID id . UserID ) * User {
return bridge . getUserByMXID ( userID , false )
}
func ( bridge * Bridge ) GetUserByMXIDIfExists ( userID id . UserID ) * User {
return bridge . getUserByMXID ( userID , true )
}
2021-10-22 19:14:34 +02:00
func ( bridge * Bridge ) GetUserByJID ( jid types . JID ) * User {
2018-08-28 23:40:54 +02:00
bridge . usersLock . Lock ( )
defer bridge . usersLock . Unlock ( )
2021-10-22 19:14:34 +02:00
user , ok := bridge . usersByUsername [ jid . User ]
2018-08-16 14:59:18 +02:00
if ! ok {
2021-10-22 19:14:34 +02:00
return bridge . loadDBUser ( bridge . DB . User . GetByUsername ( jid . User ) , nil )
2018-08-16 14:59:18 +02:00
}
return user
}
2020-05-21 18:49:01 +02:00
func ( user * User ) addToJIDMap ( ) {
user . bridge . usersLock . Lock ( )
2021-10-22 19:14:34 +02:00
user . bridge . usersByUsername [ user . JID . User ] = user
2020-05-21 18:49:01 +02:00
user . bridge . usersLock . Unlock ( )
}
2022-03-10 21:20:10 +01:00
func ( user * User ) removeFromJIDMap ( state BridgeState ) {
2020-05-21 18:49:01 +02:00
user . bridge . usersLock . Lock ( )
2021-10-22 19:14:34 +02:00
jidUser , ok := user . bridge . usersByUsername [ user . JID . User ]
2020-10-04 12:55:09 +02:00
if ok && user == jidUser {
2021-10-22 19:14:34 +02:00
delete ( user . bridge . usersByUsername , user . JID . User )
2020-10-04 12:55:09 +02:00
}
2020-05-21 18:49:01 +02:00
user . bridge . usersLock . Unlock ( )
2020-09-27 21:30:08 +02:00
user . bridge . Metrics . TrackLoginState ( user . JID , false )
2022-03-10 21:20:10 +01:00
user . sendBridgeState ( state )
2020-05-21 18:49:01 +02:00
}
2018-08-16 14:59:18 +02:00
func ( bridge * Bridge ) GetAllUsers ( ) [ ] * User {
2018-08-28 23:40:54 +02:00
bridge . usersLock . Lock ( )
defer bridge . usersLock . Unlock ( )
2018-08-16 14:59:18 +02:00
dbUsers := bridge . DB . User . GetAll ( )
output := make ( [ ] * User , len ( dbUsers ) )
for index , dbUser := range dbUsers {
2018-08-28 23:40:54 +02:00
user , ok := bridge . usersByMXID [ dbUser . MXID ]
2018-08-16 14:59:18 +02:00
if ! ok {
2019-05-28 20:31:25 +02:00
user = bridge . loadDBUser ( dbUser , nil )
2018-08-16 14:59:18 +02:00
}
output [ index ] = user
}
return output
}
2020-05-08 21:32:22 +02:00
func ( bridge * Bridge ) loadDBUser ( dbUser * database . User , mxid * id . UserID ) * User {
2019-05-28 20:31:25 +02:00
if dbUser == nil {
if mxid == nil {
return nil
}
dbUser = bridge . DB . User . New ( )
dbUser . MXID = * mxid
dbUser . Insert ( )
}
user := bridge . NewUser ( dbUser )
bridge . usersByMXID [ user . MXID ] = user
2021-10-22 19:14:34 +02:00
if ! user . JID . IsEmpty ( ) {
var err error
user . Session , err = bridge . WAContainer . GetDevice ( user . JID )
if err != nil {
2021-10-27 14:54:34 +02:00
user . log . Errorfln ( "Failed to load user's whatsapp session: %v" , err )
2021-10-22 19:14:34 +02:00
} 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
user . Update ( )
} else {
2022-04-29 18:38:44 +02:00
user . Session . Log = & waLogger { user . log . Sub ( "Session" ) }
2021-10-22 19:14:34 +02:00
bridge . usersByUsername [ user . JID . User ] = user
}
2019-05-28 20:31:25 +02:00
}
if len ( user . ManagementRoom ) > 0 {
bridge . managementRooms [ user . ManagementRoom ] = user
}
return user
}
2018-08-18 21:57:08 +02:00
func ( bridge * Bridge ) NewUser ( dbUser * database . User ) * User {
2018-08-25 23:26:24 +02:00
user := & User {
2019-01-11 20:17:31 +01:00
User : dbUser ,
bridge : bridge ,
log : bridge . Log . Sub ( "User" ) . Sub ( string ( dbUser . MXID ) ) ,
2019-05-22 22:05:58 +02:00
2021-10-26 16:01:10 +02:00
historySyncs : make ( chan * events . HistorySync , 32 ) ,
2021-11-08 12:04:54 +01:00
lastPresence : types . PresenceUnavailable ,
2018-08-16 14:59:18 +02:00
}
2021-10-28 13:03:55 +02:00
user . RelayWhitelisted = user . bridge . Config . Bridge . Permissions . IsRelayWhitelisted ( user . MXID )
2018-08-28 23:40:54 +02:00
user . Whitelisted = user . bridge . Config . Bridge . Permissions . IsWhitelisted ( user . MXID )
user . Admin = user . bridge . Config . Bridge . Permissions . IsAdmin ( user . MXID )
2022-04-22 12:26:37 +02:00
if len ( user . bridge . Config . Homeserver . StatusEndpoint ) > 0 {
user . bridgeStateQueue = make ( chan BridgeState , 10 )
go user . bridgeStateLoop ( )
}
2018-08-25 23:26:24 +02:00
return user
2018-08-16 14:59:18 +02:00
}
2021-12-29 20:40:08 +01:00
func ( user * User ) ensureInvited ( intent * appservice . IntentAPI , roomID id . RoomID , isDirect bool ) ( ok bool ) {
inviteContent := event . Content {
Parsed : & event . MemberEventContent {
Membership : event . MembershipInvite ,
IsDirect : isDirect ,
} ,
Raw : map [ string ] interface { } { } ,
}
customPuppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
inviteContent . Raw [ "fi.mau.will_auto_accept" ] = true
}
_ , err := intent . SendStateEvent ( roomID , event . StateMember , user . MXID . String ( ) , & inviteContent )
var httpErr mautrix . HTTPError
if err != nil && errors . As ( err , & httpErr ) && httpErr . RespError != nil && strings . Contains ( httpErr . RespError . Err , "is already in the room" ) {
user . bridge . StateStore . SetMembership ( roomID , user . MXID , event . MembershipJoin )
ok = true
2022-01-17 14:44:00 +01:00
return
2021-12-29 20:40:08 +01:00
} else if err != nil {
user . log . Warnfln ( "Failed to invite user to %s: %v" , roomID , err )
} else {
ok = true
}
2021-12-28 12:14:07 +01:00
2021-12-29 20:40:08 +01:00
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
2022-01-17 14:44:00 +01:00
err = customPuppet . CustomIntent ( ) . EnsureJoined ( roomID , appservice . EnsureJoinedParams { IgnoreCache : true } )
2021-12-29 20:40:08 +01:00
if err != nil {
user . log . Warnfln ( "Failed to auto-join %s: %v" , roomID , err )
ok = false
} else {
ok = true
}
}
return
}
2021-12-28 12:14:07 +01:00
2021-12-29 20:40:08 +01:00
func ( user * User ) GetSpaceRoom ( ) id . RoomID {
if ! user . bridge . Config . Bridge . PersonalFilteringSpaces {
return ""
}
if len ( user . SpaceRoom ) == 0 {
2021-12-29 13:19:16 +01:00
user . spaceCreateLock . Lock ( )
defer user . spaceCreateLock . Unlock ( )
2021-12-29 20:40:08 +01:00
if len ( user . SpaceRoom ) > 0 {
return user . SpaceRoom
}
resp , err := user . bridge . Bot . CreateRoom ( & mautrix . ReqCreateRoom {
Visibility : "private" ,
Name : "WhatsApp" ,
Topic : "Your WhatsApp bridged chats" ,
InitialState : [ ] * event . Event { {
Type : event . StateRoomAvatar ,
Content : event . Content {
Parsed : & event . RoomAvatarEventContent {
URL : user . bridge . Config . AppService . Bot . ParsedAvatar ,
} ,
} ,
} } ,
CreationContent : map [ string ] interface { } {
"type" : event . RoomTypeSpace ,
} ,
2021-12-30 11:30:12 +01:00
PowerLevelOverride : & event . PowerLevelsEventContent {
Users : map [ id . UserID ] int {
user . bridge . Bot . UserID : 9001 ,
user . MXID : 50 ,
} ,
} ,
2021-12-29 20:40:08 +01:00
} )
2021-12-28 12:14:07 +01:00
2021-12-29 20:40:08 +01:00
if err != nil {
user . log . Errorln ( "Failed to auto-create space room:" , err )
2021-12-28 12:14:07 +01:00
} else {
2021-12-29 20:40:08 +01:00
user . SpaceRoom = resp . RoomID
user . Update ( )
user . ensureInvited ( user . bridge . Bot , user . SpaceRoom , false )
2021-12-28 12:14:07 +01:00
}
2021-12-29 20:40:08 +01:00
} else if ! user . spaceMembershipChecked && ! user . bridge . StateStore . IsInRoom ( user . SpaceRoom , user . MXID ) {
user . ensureInvited ( user . bridge . Bot , user . SpaceRoom , false )
2021-12-28 12:14:07 +01:00
}
2021-12-29 20:40:08 +01:00
user . spaceMembershipChecked = true
2021-12-29 13:19:16 +01:00
2021-12-29 20:40:08 +01:00
return user . SpaceRoom
2021-12-28 12:14:07 +01:00
}
2020-05-27 11:16:05 +02:00
func ( user * User ) GetManagementRoom ( ) id . RoomID {
if len ( user . ManagementRoom ) == 0 {
user . mgmtCreateLock . Lock ( )
defer user . mgmtCreateLock . Unlock ( )
if len ( user . ManagementRoom ) > 0 {
return user . ManagementRoom
}
2021-11-01 10:17:44 +01:00
creationContent := make ( map [ string ] interface { } )
if ! user . bridge . Config . Bridge . FederateRooms {
creationContent [ "m.federate" ] = false
}
2020-05-27 11:16:05 +02:00
resp , err := user . bridge . Bot . CreateRoom ( & mautrix . ReqCreateRoom {
2021-11-01 10:17:44 +01:00
Topic : "WhatsApp bridge notices" ,
IsDirect : true ,
CreationContent : creationContent ,
2020-05-27 11:16:05 +02:00
} )
if err != nil {
user . log . Errorln ( "Failed to auto-create management room:" , err )
} else {
user . SetManagementRoom ( resp . RoomID )
}
}
return user . ManagementRoom
}
2020-05-08 21:32:22 +02:00
func ( user * User ) SetManagementRoom ( roomID id . RoomID ) {
2018-08-18 21:57:08 +02:00
existingUser , ok := user . bridge . managementRooms [ roomID ]
if ok {
existingUser . ManagementRoom = ""
existingUser . Update ( )
}
user . ManagementRoom = roomID
user . bridge . managementRooms [ user . ManagementRoom ] = user
user . Update ( )
}
2021-10-22 19:14:34 +02:00
type waLogger struct { l log . Logger }
func ( w * waLogger ) Debugf ( msg string , args ... interface { } ) { w . l . Debugfln ( msg , args ... ) }
func ( w * waLogger ) Infof ( msg string , args ... interface { } ) { w . l . Infofln ( msg , args ... ) }
func ( w * waLogger ) Warnf ( msg string , args ... interface { } ) { w . l . Warnfln ( msg , args ... ) }
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 ) } }
2018-08-18 21:57:08 +02:00
2021-10-27 14:54:34 +02:00
var ErrAlreadyLoggedIn = errors . New ( "already logged in" )
2022-05-17 18:30:46 +02:00
func ( user * User ) obfuscateJID ( jid types . JID ) string {
// Turn the first 4 bytes of HMAC-SHA256(hs_token, phone) into a number and replace the middle of the actual phone with that deterministic random number.
randomNumber := binary . BigEndian . Uint32 ( hmac . New ( sha256 . New , [ ] byte ( user . bridge . Config . AppService . HSToken ) ) . Sum ( [ ] byte ( jid . User ) ) [ : 4 ] )
return fmt . Sprintf ( "+%s-%d-%s:%d" , jid . User [ : 1 ] , randomNumber , jid . User [ len ( jid . User ) - 2 : ] , jid . Device )
}
2022-01-28 14:06:19 +01:00
func ( user * User ) createClient ( sess * store . Device ) {
user . Client = whatsmeow . NewClient ( sess , & waLogger { user . log . Sub ( "Client" ) } )
user . Client . AddEventHandler ( user . HandleEvent )
2022-02-25 00:27:24 +01:00
user . Client . SetForceActiveDeliveryReceipts ( user . bridge . Config . Bridge . ForceActiveDeliveryReceipts )
2022-05-17 18:30:46 +02:00
user . Client . GetMessageForRetry = func ( requester , to types . JID , id types . MessageID ) * waProto . Message {
2022-05-17 16:35:23 +02:00
Segment . Track ( user . MXID , "WhatsApp incoming retry (message not found)" , map [ string ] interface { } {
2022-05-17 18:30:46 +02:00
"requester" : user . obfuscateJID ( requester ) ,
2022-05-17 16:35:23 +02:00
"messageID" : id ,
} )
2022-01-28 14:06:19 +01:00
user . bridge . Metrics . TrackRetryReceipt ( 0 , false )
return nil
}
2022-05-16 12:46:32 +02:00
user . Client . PreRetryCallback = func ( receipt * events . Receipt , messageID types . MessageID , retryCount int , msg * waProto . Message ) bool {
Segment . Track ( user . MXID , "WhatsApp incoming retry (accepted)" , map [ string ] interface { } {
2022-05-17 18:30:46 +02:00
"requester" : user . obfuscateJID ( receipt . Sender ) ,
2022-05-16 12:46:32 +02:00
"messageID" : messageID ,
"retryCount" : retryCount ,
} )
2022-01-28 14:06:19 +01:00
user . bridge . Metrics . TrackRetryReceipt ( retryCount , true )
return true
}
}
2021-10-27 14:54:34 +02:00
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" ) }
2022-01-28 14:06:19 +01:00
user . createClient ( newSession )
2021-10-27 14:54:34 +02:00
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 {
2021-02-05 18:26:09 +01:00
user . connLock . Lock ( )
2021-10-22 19:14:34 +02:00
defer user . connLock . Unlock ( )
if user . Client != nil {
return user . Client . IsConnected ( )
2021-10-27 14:54:34 +02:00
} else if user . Session == nil {
2018-08-18 21:57:08 +02:00
return false
}
user . log . Debugln ( "Connecting to WhatsApp" )
2021-10-27 14:54:34 +02:00
user . sendBridgeState ( BridgeState { StateEvent : StateConnecting , Error : WAConnecting } )
2022-01-28 14:06:19 +01:00
user . createClient ( user . Session )
2021-10-22 19:14:34 +02:00
err := user . Client . Connect ( )
if err != nil {
user . log . Warnln ( "Error connecting to WhatsApp:" , err )
2022-04-20 12:48:58 +02:00
user . sendBridgeState ( BridgeState {
StateEvent : StateUnknownError ,
Error : WAConnectionFailed ,
Info : map [ string ] interface { } {
"go_error" : err . Error ( ) ,
} ,
} )
2021-10-22 19:14:34 +02:00
return false
2019-05-16 17:08:30 +02:00
}
2021-10-22 19:14:34 +02:00
return true
2018-08-16 14:59:18 +02:00
}
2021-10-27 14:54:34 +02:00
func ( user * User ) unlockedDeleteConnection ( ) {
2021-10-22 19:14:34 +02:00
if user . Client == nil {
2021-02-18 22:36:14 +01:00
return
}
2021-10-22 19:14:34 +02:00
user . Client . Disconnect ( )
user . Client . RemoveEventHandlers ( )
user . Client = nil
2021-02-05 18:26:09 +01:00
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
2021-10-27 14:54:34 +02:00
}
func ( user * User ) DeleteConnection ( ) {
user . connLock . Lock ( )
defer user . connLock . Unlock ( )
user . unlockedDeleteConnection ( )
2018-08-16 14:59:18 +02:00
}
2019-08-24 21:39:12 +02:00
func ( user * User ) HasSession ( ) bool {
return user . Session != nil
}
2021-10-22 19:14:34 +02:00
func ( user * User ) DeleteSession ( ) {
if user . Session != nil {
err := user . Session . Delete ( )
2018-08-16 14:59:18 +02:00
if err != nil {
2021-10-22 19:14:34 +02:00
user . log . Warnln ( "Failed to delete session:" , err )
2019-01-21 22:55:16 +01:00
}
2021-10-22 19:14:34 +02:00
user . Session = nil
2019-07-17 23:14:04 +02:00
}
2021-10-22 19:14:34 +02:00
if ! user . JID . IsEmpty ( ) {
user . JID = types . EmptyJID
user . Update ( )
2018-08-16 14:59:18 +02:00
}
2022-04-06 16:45:45 +02:00
// Delete all of the backfill and history sync data.
2022-05-10 22:28:30 +02:00
user . bridge . DB . Backfill . DeleteAll ( user . MXID )
user . bridge . DB . HistorySync . DeleteAllConversations ( user . MXID )
user . bridge . DB . HistorySync . DeleteAllMessages ( user . MXID )
2022-05-11 23:37:30 +02:00
user . bridge . DB . MediaBackfillRequest . DeleteAllMediaBackfillRequests ( user . MXID )
2019-05-22 15:46:18 +02:00
}
2021-10-27 14:54:34 +02:00
func ( user * User ) IsConnected ( ) bool {
return user . Client != nil && user . Client . IsConnected ( )
}
2021-10-22 19:14:34 +02:00
func ( user * User ) IsLoggedIn ( ) bool {
2021-11-30 14:14:56 +01:00
return user . IsConnected ( ) && user . Client . IsLoggedIn ( )
2019-05-22 15:46:18 +02:00
}
2019-12-31 19:17:03 +01:00
func ( user * User ) tryAutomaticDoublePuppeting ( ) {
2021-11-06 12:57:35 +01:00
if ! user . bridge . Config . CanAutoDoublePuppet ( user . MXID ) {
2019-12-30 19:21:04 +01:00
return
}
2020-11-06 01:29:14 +01:00
user . log . Debugln ( "Checking if double puppeting needs to be enabled" )
2019-12-30 19:21:04 +01:00
puppet := user . bridge . GetPuppetByJID ( user . JID )
if len ( puppet . CustomMXID ) > 0 {
2020-11-06 01:29:14 +01:00
user . log . Debugln ( "User already has double-puppeting enabled" )
2019-12-30 19:21:04 +01:00
// Custom puppet already enabled
return
}
accessToken , err := puppet . loginWithSharedSecret ( user . MXID )
if err != nil {
user . log . Warnln ( "Failed to login with shared secret:" , err )
return
}
err = puppet . SwitchCustomMXID ( accessToken , user . MXID )
if err != nil {
puppet . log . Warnln ( "Failed to switch to auto-logined custom puppet:" , err )
return
}
user . log . Infoln ( "Successfully automatically enabled custom puppet" )
}
2020-07-27 12:05:42 +02:00
func ( user * User ) sendMarkdownBridgeAlert ( formatString string , args ... interface { } ) {
2022-01-25 13:26:24 +01:00
if user . bridge . Config . Bridge . DisableBridgeAlerts {
return
}
2020-07-27 12:05:42 +02:00
notice := fmt . Sprintf ( formatString , args ... )
content := format . RenderMarkdown ( notice , true , false )
_ , err := user . bridge . Bot . SendMessageEvent ( user . GetManagementRoom ( ) , event . EventMessage , content )
if err != nil {
user . log . Warnf ( "Failed to send bridge alert \"%s\": %v" , notice , err )
}
}
2021-11-03 09:56:16 +01:00
const callEventMaxAge = 15 * time . Minute
func ( user * User ) handleCallStart ( sender types . JID , id , callType string , ts time . Time ) {
if ! user . bridge . Config . Bridge . CallStartNotices || ts . Add ( callEventMaxAge ) . Before ( time . Now ( ) ) {
2021-11-02 14:46:31 +01:00
return
}
portal := user . GetPortalByJID ( sender )
text := "Incoming call"
if callType != "" {
2021-11-02 14:52:12 +01:00
text = fmt . Sprintf ( "Incoming %s call" , callType )
2021-11-02 14:46:31 +01:00
}
portal . messages <- PortalMessage {
fake : & fakeMessage {
2021-11-09 16:49:34 +01:00
Sender : sender ,
Text : text ,
ID : id ,
Time : ts ,
Important : true ,
2021-11-02 14:46:31 +01:00
} ,
source : user ,
}
}
2022-01-25 13:26:24 +01:00
const PhoneDisconnectWarningTime = 12 * 24 * time . Hour // 12 days
2022-02-18 11:12:15 +01:00
const PhoneDisconnectPingTime = 10 * 24 * time . Hour
const PhoneMinPingInterval = 24 * time . Hour
func ( user * User ) sendHackyPhonePing ( ) {
user . PhoneLastPinged = time . Now ( )
2022-05-16 10:22:41 +02:00
msgID := whatsmeow . GenerateMessageID ( )
keyIDs := make ( [ ] * waProto . AppStateSyncKeyId , 0 , 1 )
lastKeyID , err := user . GetLastAppStateKeyID ( )
if lastKeyID != nil {
keyIDs = append ( keyIDs , & waProto . AppStateSyncKeyId {
KeyId : lastKeyID ,
} )
} else {
user . log . Warnfln ( "Failed to get last app state key ID to send hacky phone ping: %v - sending empty request" , err )
}
2022-02-18 11:12:15 +01:00
ts , err := user . Client . SendMessage ( user . JID . ToNonAD ( ) , msgID , & waProto . Message {
2022-05-16 10:22:41 +02:00
ProtocolMessage : & waProto . ProtocolMessage {
Type : waProto . ProtocolMessage_APP_STATE_SYNC_KEY_REQUEST . Enum ( ) ,
AppStateSyncKeyRequest : & waProto . AppStateSyncKeyRequest {
KeyIds : keyIDs ,
} ,
} ,
2022-02-18 11:12:15 +01:00
} )
if err != nil {
user . log . Warnfln ( "Failed to send hacky phone ping: %v" , err )
} else {
2022-05-16 10:22:41 +02:00
user . log . Debugfln ( "Sent hacky phone ping %s/%s because phone has been offline for >10 days" , msgID , ts . Unix ( ) )
2022-02-18 11:12:15 +01:00
user . PhoneLastPinged = ts
user . Update ( )
}
}
2022-01-25 13:26:24 +01:00
2022-02-18 11:12:15 +01:00
func ( user * User ) PhoneRecentlySeen ( doPing bool ) bool {
if doPing && ! user . PhoneLastSeen . IsZero ( ) && user . PhoneLastSeen . Add ( PhoneDisconnectPingTime ) . Before ( time . Now ( ) ) && user . PhoneLastPinged . Add ( PhoneMinPingInterval ) . Before ( time . Now ( ) ) {
// Over 10 days since the phone was seen and over a day since the last somewhat hacky ping, send a new ping.
go user . sendHackyPhonePing ( )
}
2022-01-25 13:26:24 +01:00
return user . PhoneLastSeen . IsZero ( ) || user . PhoneLastSeen . Add ( PhoneDisconnectWarningTime ) . After ( time . Now ( ) )
}
// phoneSeen records a timestamp when the user's main device was seen online.
// The stored timestamp can later be used to warn the user if the main device is offline for too long.
func ( user * User ) phoneSeen ( ts time . Time ) {
if user . PhoneLastSeen . Add ( 1 * time . Hour ) . After ( ts ) {
// The last seen timestamp isn't going to be perfectly accurate in any case,
// so don't spam the database with an update every time there's an event.
return
2022-05-04 10:17:34 +02:00
} else if ! user . PhoneRecentlySeen ( false ) {
if user . GetPrevBridgeState ( ) . Error == WAPhoneOffline && user . IsConnected ( ) {
user . log . Debugfln ( "Saw phone after current bridge state said it has been offline, switching state back to connected" )
go user . sendBridgeState ( BridgeState { StateEvent : StateConnected } )
} else {
user . log . Debugfln ( "Saw phone after current bridge state said it has been offline, not sending new bridge state (prev: %s, connected: %t)" , user . GetPrevBridgeState ( ) . Error , user . IsConnected ( ) )
}
2022-01-25 13:26:24 +01:00
}
user . PhoneLastSeen = ts
go user . Update ( )
}
func formatDisconnectTime ( dur time . Duration ) string {
days := int ( math . Floor ( dur . Hours ( ) / 24 ) )
hours := int ( dur . Hours ( ) ) % 24
if hours == 0 {
return fmt . Sprintf ( "%d days" , days )
} else if hours == 1 {
return fmt . Sprintf ( "%d days and 1 hour" , days )
} else {
return fmt . Sprintf ( "%d days and %d hours" , days , hours )
}
}
func ( user * User ) sendPhoneOfflineWarning ( ) {
if user . lastPhoneOfflineWarning . Add ( 12 * time . Hour ) . After ( time . Now ( ) ) {
// Don't spam the warning too much
return
}
user . lastPhoneOfflineWarning = time . Now ( )
timeSinceSeen := time . Now ( ) . Sub ( user . PhoneLastSeen )
user . sendMarkdownBridgeAlert ( "Your phone hasn't been seen in %s. The server will force the bridge to log out if the phone is not active at least every 2 weeks." , formatDisconnectTime ( timeSinceSeen ) )
}
2021-02-17 00:21:30 +01:00
func ( user * User ) HandleEvent ( event interface { } ) {
switch v := event . ( type ) {
2021-10-22 19:14:34 +02:00
case * events . LoggedOut :
2022-02-17 13:09:40 +01:00
go user . handleLoggedOut ( v . OnConnect , v . Reason )
2021-10-22 19:14:34 +02:00
case * events . Connected :
user . bridge . Metrics . TrackConnectionState ( user . JID , true )
user . bridge . Metrics . TrackLoginState ( user . JID , true )
2021-11-08 12:04:54 +01:00
if len ( user . Client . Store . PushName ) > 0 {
go func ( ) {
err := user . Client . SendPresence ( user . lastPresence )
if err != nil {
user . log . Warnln ( "Failed to send initial presence:" , err )
}
} ( )
}
2021-10-25 17:31:37 +02:00
go user . tryAutomaticDoublePuppeting ( )
2022-04-19 19:54:05 +02:00
if user . bridge . Config . Bridge . HistorySync . Backfill && ! user . historySyncLoopsStarted {
go user . handleHistorySyncsLoop ( )
user . historySyncLoopsStarted = true
}
2022-01-25 13:26:24 +01:00
case * events . OfflineSyncPreview :
user . log . Infofln ( "Server says it's going to send %d messages and %d receipts that were missed during downtime" , v . Messages , v . Receipts )
2022-04-06 16:45:45 +02:00
go user . sendBridgeState ( BridgeState {
StateEvent : StateBackfilling ,
Message : fmt . Sprintf ( "backfilling %d messages and %d receipts" , v . Messages , v . Receipts ) ,
} )
2022-01-25 13:26:24 +01:00
case * events . OfflineSyncCompleted :
2022-02-18 11:12:15 +01:00
if ! user . PhoneRecentlySeen ( true ) {
2022-01-25 13:26:24 +01:00
user . log . Infofln ( "Offline sync completed, but phone last seen date is still %s - sending phone offline bridge status" , user . PhoneLastSeen )
go user . sendBridgeState ( BridgeState { StateEvent : StateTransientDisconnect , Error : WAPhoneOffline } )
2022-02-18 12:18:29 +01:00
} else {
2022-04-06 16:45:45 +02:00
if user . GetPrevBridgeState ( ) . StateEvent == StateBackfilling {
user . log . Infoln ( "Offline sync completed" )
}
2022-01-25 13:26:24 +01:00
go user . sendBridgeState ( BridgeState { StateEvent : StateConnected } )
}
2021-10-26 16:01:10 +02:00
case * events . AppStateSyncComplete :
if len ( user . Client . Store . PushName ) > 0 && v . Name == appstate . WAPatchCriticalBlock {
2021-11-08 12:04:54 +01:00
err := user . Client . SendPresence ( user . lastPresence )
2021-10-26 16:01:10 +02:00
if err != nil {
user . log . Warnln ( "Failed to send presence after app state sync:" , err )
}
2021-11-08 12:04:39 +01:00
} else if v . Name == appstate . WAPatchCriticalUnblockLow {
2021-11-08 19:57:04 +01:00
go func ( ) {
err := user . ResyncContacts ( )
if err != nil {
user . log . Errorln ( "Failed to resync puppets: %v" , err )
}
} ( )
2021-10-26 16:01:10 +02:00
}
case * events . PushNameSetting :
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
2021-11-08 12:04:54 +01:00
err := user . Client . SendPresence ( user . lastPresence )
2021-10-26 16:01:10 +02:00
if err != nil {
user . log . Warnln ( "Failed to send presence after push name update:" , err )
}
2021-10-22 19:14:34 +02:00
case * events . PairSuccess :
2022-01-25 13:26:24 +01:00
user . PhoneLastSeen = time . Now ( )
2021-10-29 15:50:29 +02:00
user . Session = user . Client . Store
2021-10-22 19:14:34 +02:00
user . JID = v . ID
user . addToJIDMap ( )
user . Update ( )
2022-02-17 13:09:40 +01:00
case * events . StreamError :
var message string
if v . Code != "" {
message = fmt . Sprintf ( "Unknown stream error with code %s" , v . Code )
} else if children := v . Raw . GetChildren ( ) ; len ( children ) > 0 {
message = fmt . Sprintf ( "Unknown stream error (contains %s node)" , children [ 0 ] . Tag )
} else {
message = "Unknown stream error"
}
go user . sendBridgeState ( BridgeState { StateEvent : StateUnknownError , Message : message } )
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
case * events . ConnectFailure :
go user . sendBridgeState ( BridgeState { StateEvent : StateUnknownError , Message : fmt . Sprintf ( "Unknown connection failure: %s" , v . Reason ) } )
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
2022-02-17 14:33:31 +01:00
case * events . ClientOutdated :
user . log . Errorfln ( "Got a client outdated connect failure. The bridge is likely out of date, please update immediately." )
go user . sendBridgeState ( BridgeState { StateEvent : StateUnknownError , Message : "Connect failure: 405 client outdated" } )
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
2022-02-17 13:09:40 +01:00
case * events . TemporaryBan :
go user . sendBridgeState ( BridgeState { StateEvent : StateBadCredentials , Message : v . String ( ) } )
2021-10-22 19:14:34 +02:00
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
case * events . Disconnected :
2022-03-07 21:45:19 +01:00
// Don't send the normal transient disconnect state if we're already in a different transient disconnect state.
// TODO remove this if/when the phone offline state is moved to a sub-state of CONNECTED
2022-03-08 11:51:24 +01:00
if user . GetPrevBridgeState ( ) . Error != WAPhoneOffline && user . PhoneRecentlySeen ( false ) {
2022-03-07 21:45:19 +01:00
go user . sendBridgeState ( BridgeState { StateEvent : StateTransientDisconnect , Message : "Disconnected from WhatsApp. Trying to reconnect." } )
}
2021-10-22 19:14:34 +02:00
user . bridge . Metrics . TrackConnectionState ( user . JID , false )
case * events . Contact :
2021-11-08 12:04:39 +01:00
go user . syncPuppet ( v . JID , "contact event" )
2021-10-22 19:14:34 +02:00
case * events . PushName :
2021-11-08 12:04:39 +01:00
go user . syncPuppet ( v . JID , "push name event" )
2021-10-28 11:59:22 +02:00
case * events . GroupInfo :
2022-03-01 19:25:46 +01:00
user . groupListCache = nil
2021-10-28 11:59:22 +02:00
go user . handleGroupUpdate ( v )
2021-10-31 18:59:23 +01:00
case * events . JoinedGroup :
2022-03-01 19:25:46 +01:00
user . groupListCache = nil
2021-10-31 18:59:23 +01:00
go user . handleGroupCreate ( v )
2021-10-28 11:59:22 +02:00
case * events . Picture :
go user . handlePictureUpdate ( v )
2021-10-22 19:14:34 +02:00
case * events . Receipt :
2022-01-25 13:26:24 +01:00
if v . IsFromMe && v . Sender . Device == 0 {
user . phoneSeen ( v . Timestamp )
}
2021-10-22 19:14:34 +02:00
go user . handleReceipt ( v )
2021-10-27 18:30:34 +02:00
case * events . ChatPresence :
go user . handleChatPresence ( v )
2021-10-22 19:14:34 +02:00
case * events . Message :
2021-12-25 19:50:36 +01:00
portal := user . GetPortalByMessageSource ( v . Info . MessageSource )
2021-10-27 17:31:33 +02:00
portal . messages <- PortalMessage { evt : v , source : user }
2022-02-10 18:18:49 +01:00
case * events . MediaRetry :
user . phoneSeen ( v . Timestamp )
portal := user . GetPortalByJID ( v . ChatID )
portal . mediaRetries <- PortalMediaRetry { evt : v , source : user }
2021-11-02 14:46:31 +01:00
case * events . CallOffer :
2021-11-03 09:56:16 +01:00
user . handleCallStart ( v . CallCreator , v . CallID , "" , v . Timestamp )
2021-11-02 14:46:31 +01:00
case * events . CallOfferNotice :
2021-11-03 09:56:16 +01:00
user . handleCallStart ( v . CallCreator , v . CallID , v . Type , v . Timestamp )
2021-11-09 16:49:34 +01:00
case * events . IdentityChange :
puppet := user . bridge . GetPuppetByJID ( v . JID )
portal := user . GetPortalByJID ( v . JID )
2021-11-09 21:57:36 +01:00
if len ( portal . MXID ) > 0 && user . bridge . Config . Bridge . IdentityChangeNotices {
2021-11-09 22:12:10 +01:00
text := fmt . Sprintf ( "Your security code with %s changed." , puppet . Displayname )
if v . Implicit {
text = fmt . Sprintf ( "Your security code with %s (device #%d) changed." , puppet . Displayname , v . JID . Device )
}
2021-11-09 16:49:34 +01:00
portal . messages <- PortalMessage {
fake : & fakeMessage {
Sender : v . JID ,
2021-11-09 22:12:10 +01:00
Text : text ,
2021-11-09 16:49:34 +01:00
ID : strconv . FormatInt ( v . Timestamp . Unix ( ) , 10 ) ,
Time : v . Timestamp ,
Important : false ,
} ,
source : user ,
}
}
2021-11-02 14:46:31 +01:00
case * events . CallTerminate , * events . CallRelayLatency , * events . CallAccept , * events . UnknownCallEvent :
// ignore
2021-10-27 17:31:33 +02:00
case * events . UndecryptableMessage :
2021-12-25 19:50:36 +01:00
portal := user . GetPortalByMessageSource ( v . Info . MessageSource )
2021-10-27 17:31:33 +02:00
portal . messages <- PortalMessage { undecryptable : v , source : user }
2021-10-26 16:01:10 +02:00
case * events . HistorySync :
2022-04-06 17:00:48 +02:00
if user . bridge . Config . Bridge . HistorySync . Backfill {
user . historySyncs <- v
}
2021-10-22 19:14:34 +02:00
case * events . Mute :
2021-10-29 15:50:29 +02:00
portal := user . GetPortalByJID ( v . JID )
2021-04-19 21:14:32 +02:00
if portal != nil {
2021-10-22 19:14:34 +02:00
var mutedUntil time . Time
if v . Action . GetMuted ( ) {
mutedUntil = time . Unix ( v . Action . GetMuteEndTimestamp ( ) , 0 )
}
go user . updateChatMute ( nil , portal , mutedUntil )
2021-04-19 21:14:32 +02:00
}
2021-10-22 19:14:34 +02:00
case * events . Archive :
2021-10-29 15:50:29 +02:00
portal := user . GetPortalByJID ( v . JID )
2021-04-19 21:14:32 +02:00
if portal != nil {
2021-10-22 19:14:34 +02:00
go user . updateChatTag ( nil , portal , user . bridge . Config . Bridge . ArchiveTag , v . Action . GetArchived ( ) )
2021-04-20 15:32:23 +02:00
}
2021-10-22 19:14:34 +02:00
case * events . Pin :
2021-10-29 15:50:29 +02:00
portal := user . GetPortalByJID ( v . JID )
2021-04-20 15:32:23 +02:00
if portal != nil {
2021-10-22 19:14:34 +02:00
go user . updateChatTag ( nil , portal , user . bridge . Config . Bridge . PinnedTag , v . Action . GetPinned ( ) )
2021-04-19 21:14:32 +02:00
}
2021-10-27 17:31:33 +02:00
case * events . AppState :
// Ignore
2022-05-11 13:04:59 +02:00
case * events . KeepAliveTimeout :
go user . sendBridgeState ( BridgeState { StateEvent : StateTransientDisconnect , Error : WAKeepaliveTimeout } )
case * events . KeepAliveRestored :
user . log . Infof ( "Keepalive restored after timeouts, sending connected event" )
go user . sendBridgeState ( BridgeState { StateEvent : StateConnected } )
2022-05-18 18:40:29 +02:00
case * events . MarkChatAsRead :
if user . bridge . Config . Bridge . SyncManualMarkedUnread {
user . markUnread ( user . GetPortalByJID ( v . JID ) , ! v . Action . GetRead ( ) )
}
2021-02-17 00:21:30 +01:00
default :
user . log . Debugfln ( "Unknown type of event in HandleEvent: %T" , v )
}
}
2021-10-22 19:14:34 +02:00
func ( user * User ) updateChatMute ( intent * appservice . IntentAPI , portal * Portal , mutedUntil time . Time ) {
2021-04-19 21:14:32 +02:00
if len ( portal . MXID ) == 0 || ! user . bridge . Config . Bridge . MuteBridging {
return
} else if intent == nil {
doublePuppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if doublePuppet == nil || doublePuppet . CustomIntent ( ) == nil {
return
}
intent = doublePuppet . CustomIntent ( )
}
var err error
2021-10-22 19:14:34 +02:00
if mutedUntil . IsZero ( ) && mutedUntil . Before ( time . Now ( ) ) {
2021-11-15 13:42:06 +01:00
user . log . Debugfln ( "Portal %s is muted until %s, unmuting..." , portal . MXID , mutedUntil )
2021-04-19 21:14:32 +02:00
err = intent . DeletePushRule ( "global" , pushrules . RoomRule , string ( portal . MXID ) )
} else {
2021-11-15 13:42:06 +01:00
user . log . Debugfln ( "Portal %s is muted until %s, muting..." , portal . MXID , mutedUntil )
2021-04-19 21:14:32 +02:00
err = intent . PutPushRule ( "global" , pushrules . RoomRule , string ( portal . MXID ) , & mautrix . ReqPutPushRule {
Actions : [ ] pushrules . PushActionType { pushrules . ActionDontNotify } ,
} )
}
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
user . log . Warnfln ( "Failed to update push rule for %s through double puppet: %v" , portal . MXID , err )
}
}
2021-04-20 15:32:23 +02:00
type CustomTagData struct {
Order json . Number ` json:"order" `
2021-12-15 10:51:26 +01:00
DoublePuppet string ` json:"fi.mau.double_puppet_source" `
2021-04-20 15:32:23 +02:00
}
type CustomTagEventContent struct {
Tags map [ string ] CustomTagData ` json:"tags" `
}
func ( user * User ) updateChatTag ( intent * appservice . IntentAPI , portal * Portal , tag string , active bool ) {
if len ( portal . MXID ) == 0 || len ( tag ) == 0 {
2021-04-19 21:14:32 +02:00
return
} else if intent == nil {
doublePuppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if doublePuppet == nil || doublePuppet . CustomIntent ( ) == nil {
return
}
intent = doublePuppet . CustomIntent ( )
}
2021-04-20 15:32:23 +02:00
var existingTags CustomTagEventContent
err := intent . GetTagsWithCustomData ( portal . MXID , & existingTags )
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
user . log . Warnfln ( "Failed to get tags of %s: %v" , portal . MXID , err )
}
currentTag , ok := existingTags . Tags [ tag ]
if active && ! ok {
user . log . Debugln ( "Adding tag" , tag , "to" , portal . MXID )
2021-12-15 12:51:20 +01:00
data := CustomTagData { "0.5" , doublePuppetValue }
2021-04-20 15:32:23 +02:00
err = intent . AddTagWithCustomData ( portal . MXID , tag , & data )
2021-12-15 12:51:20 +01:00
} else if ! active && ok && currentTag . DoublePuppet == doublePuppetValue {
2021-04-20 15:32:23 +02:00
user . log . Debugln ( "Removing tag" , tag , "from" , portal . MXID )
err = intent . RemoveTag ( portal . MXID , tag )
2021-04-19 21:14:32 +02:00
} else {
2021-04-20 15:32:23 +02:00
err = nil
2021-04-19 21:14:32 +02:00
}
if err != nil {
2021-04-20 15:32:23 +02:00
user . log . Warnfln ( "Failed to update tag %s for %s through double puppet: %v" , tag , portal . MXID , err )
2021-04-19 21:14:32 +02:00
}
}
2021-05-18 14:23:19 +02:00
type CustomReadReceipt struct {
2022-01-07 13:38:44 +01:00
Timestamp int64 ` json:"ts,omitempty" `
DoublePuppetSource string ` json:"fi.mau.double_puppet_source,omitempty" `
2021-05-18 14:23:19 +02:00
}
2022-01-17 21:56:18 +01:00
type CustomReadMarkers struct {
mautrix . ReqSetReadMarkers
ReadExtra CustomReadReceipt ` json:"com.beeper.read.extra" `
FullyReadExtra CustomReadReceipt ` json:"com.beeper.fully_read.extra" `
}
2021-10-28 11:59:22 +02:00
func ( user * User ) syncChatDoublePuppetDetails ( portal * Portal , justCreated bool ) {
doublePuppet := portal . bridge . GetPuppetByCustomMXID ( user . MXID )
if doublePuppet == nil {
return
}
2021-10-22 19:14:34 +02:00
if doublePuppet == nil || doublePuppet . CustomIntent ( ) == nil || len ( portal . MXID ) == 0 {
2021-04-19 21:14:32 +02:00
return
}
2021-04-29 10:57:05 +02:00
if justCreated || ! user . bridge . Config . Bridge . TagOnlyOnCreate {
2021-10-22 19:14:34 +02:00
chat , err := user . Client . Store . ChatSettings . GetChatSettings ( portal . Key . JID )
if err != nil {
user . log . Warnfln ( "Failed to get settings of %s: %v" , portal . Key . JID , err )
2021-06-01 14:28:15 +02:00
return
}
2021-10-28 11:59:22 +02:00
intent := doublePuppet . CustomIntent ( )
2022-04-27 11:45:11 +02:00
if portal . Key . JID == types . StatusBroadcastJID && justCreated {
if user . bridge . Config . Bridge . MuteStatusBroadcast {
user . updateChatMute ( intent , portal , time . Now ( ) . Add ( 365 * 24 * time . Hour ) )
}
if len ( user . bridge . Config . Bridge . StatusBroadcastTag ) > 0 {
user . updateChatTag ( intent , portal , user . bridge . Config . Bridge . StatusBroadcastTag , true )
}
2021-11-15 13:42:06 +01:00
return
2021-11-15 13:06:31 +01:00
} else if ! chat . Found {
return
}
2021-10-22 19:14:34 +02:00
user . updateChatMute ( intent , portal , chat . MutedUntil )
user . updateChatTag ( intent , portal , user . bridge . Config . Bridge . ArchiveTag , chat . Archived )
user . updateChatTag ( intent , portal , user . bridge . Config . Bridge . PinnedTag , chat . Pinned )
}
}
2020-08-22 12:07:55 +02:00
func ( user * User ) getDirectChats ( ) map [ id . UserID ] [ ] id . RoomID {
res := make ( map [ id . UserID ] [ ] id . RoomID )
2021-10-22 19:14:34 +02:00
privateChats := user . bridge . DB . Portal . FindPrivateChats ( user . JID . ToNonAD ( ) )
2020-08-22 12:07:55 +02:00
for _ , portal := range privateChats {
if len ( portal . MXID ) > 0 {
res [ user . bridge . FormatPuppetMXID ( portal . Key . JID ) ] = [ ] id . RoomID { portal . MXID }
}
}
return res
}
func ( user * User ) UpdateDirectChats ( chats map [ id . UserID ] [ ] id . RoomID ) {
if ! user . bridge . Config . Bridge . SyncDirectChatList {
return
}
puppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if puppet == nil || puppet . CustomIntent ( ) == nil {
return
}
intent := puppet . CustomIntent ( )
method := http . MethodPatch
if chats == nil {
chats = user . getDirectChats ( )
method = http . MethodPut
}
user . log . Debugln ( "Updating m.direct list on homeserver" )
var err error
if user . bridge . Config . Homeserver . Asmux {
2022-04-17 12:09:54 +02:00
urlPath := intent . BuildClientURL ( "unstable" , "com.beeper.asmux" , "dms" )
2021-03-04 18:35:07 +01:00
_ , err = intent . MakeFullRequest ( mautrix . FullRequest {
Method : method ,
URL : urlPath ,
Headers : http . Header { "X-Asmux-Auth" : { user . bridge . AS . Registration . AppToken } } ,
RequestJSON : chats ,
} )
2020-08-22 12:07:55 +02:00
} else {
existingChats := make ( map [ id . UserID ] [ ] id . RoomID )
err = intent . GetAccountData ( event . AccountDataDirectChats . Type , & existingChats )
if err != nil {
user . log . Warnln ( "Failed to get m.direct list to update it:" , err )
return
}
for userID , rooms := range existingChats {
if _ , ok := user . bridge . ParsePuppetMXID ( userID ) ; ! ok {
// This is not a ghost user, include it in the new list
chats [ userID ] = rooms
} else if _ , ok := chats [ userID ] ; ! ok && method == http . MethodPatch {
// This is a ghost user, but we're not replacing the whole list, so include it too
chats [ userID ] = rooms
}
}
err = intent . SetAccountData ( event . AccountDataDirectChats . Type , & chats )
}
if err != nil {
user . log . Warnln ( "Failed to update m.direct list:" , err )
}
}
2022-02-17 13:09:40 +01:00
func ( user * User ) handleLoggedOut ( onConnect bool , reason events . ConnectFailureReason ) {
2022-03-15 15:04:10 +01:00
errorCode := WAUnknownLogout
if reason == events . ConnectFailureLoggedOut {
errorCode = WALoggedOut
2022-04-28 19:09:42 +02:00
} else if reason == events . ConnectFailureMainDeviceGone {
errorCode = WAMainDeviceGone
2022-03-15 15:04:10 +01:00
}
user . removeFromJIDMap ( BridgeState { StateEvent : StateBadCredentials , Error : errorCode } )
2022-03-10 21:20:10 +01:00
user . DeleteConnection ( )
user . Session = nil
2021-10-22 19:14:34 +02:00
user . JID = types . EmptyJID
user . Update ( )
2021-10-29 15:50:29 +02:00
if onConnect {
2022-02-17 13:09:40 +01:00
user . sendMarkdownBridgeAlert ( "Connecting to WhatsApp failed as the device was unlinked (error %s). Please link the bridge to your phone again." , reason )
2021-10-29 15:50:29 +02:00
} else {
user . sendMarkdownBridgeAlert ( "You were logged out from another device. Please link the bridge to your phone again." )
}
2018-08-16 14:59:18 +02:00
}
2021-12-25 19:50:36 +01:00
func ( user * User ) GetPortalByMessageSource ( ms types . MessageSource ) * Portal {
jid := ms . Chat
if ms . IsIncomingBroadcast ( ) {
if ms . IsFromMe {
jid = ms . BroadcastListOwner . ToNonAD ( )
} else {
jid = ms . Sender . ToNonAD ( )
}
if jid . IsEmpty ( ) {
return nil
}
}
return user . bridge . GetPortalByJID ( database . NewPortalKey ( jid , user . JID ) )
}
2021-10-22 19:14:34 +02:00
func ( user * User ) GetPortalByJID ( jid types . JID ) * Portal {
2021-10-29 15:50:29 +02:00
return user . bridge . GetPortalByJID ( database . NewPortalKey ( jid , user . JID ) )
2018-08-28 23:40:54 +02:00
}
2021-11-08 12:04:39 +01:00
func ( user * User ) syncPuppet ( jid types . JID , reason string ) {
2022-05-13 10:34:45 +02:00
user . bridge . GetPuppetByJID ( jid ) . SyncContact ( user , false , false , reason )
2021-11-08 12:04:39 +01:00
}
2021-11-08 19:57:04 +01:00
func ( user * User ) ResyncContacts ( ) error {
2021-11-08 12:04:39 +01:00
contacts , err := user . Client . Store . Contacts . GetAllContacts ( )
if err != nil {
2021-11-08 19:57:04 +01:00
return fmt . Errorf ( "failed to get cached contacts: %w" , err )
2021-11-08 12:04:39 +01:00
}
2021-11-08 19:57:04 +01:00
user . log . Infofln ( "Resyncing displaynames with %d contacts" , len ( contacts ) )
2021-11-08 12:04:39 +01:00
for jid , contact := range contacts {
puppet := user . bridge . GetPuppetByJID ( jid )
2022-02-17 13:09:40 +01:00
if puppet != nil {
puppet . Sync ( user , contact )
} else {
user . log . Warnfln ( "Got a nil puppet for %s while syncing contacts" , jid )
}
2021-11-08 12:04:39 +01:00
}
2021-11-08 19:57:04 +01:00
return nil
}
func ( user * User ) ResyncGroups ( createPortals bool ) error {
groups , err := user . Client . GetJoinedGroups ( )
if err != nil {
return fmt . Errorf ( "failed to get group list from server: %w" , err )
}
for _ , group := range groups {
portal := user . GetPortalByJID ( group . JID )
if len ( portal . MXID ) == 0 {
if createPortals {
2022-03-25 07:15:52 +01:00
err = portal . CreateMatrixRoom ( user , group , true , true )
2021-11-08 19:57:04 +01:00
if err != nil {
return fmt . Errorf ( "failed to create room for %s: %w" , group . JID , err )
}
}
} else {
portal . UpdateMatrixRoom ( user , group )
}
}
return nil
2021-10-22 19:14:34 +02:00
}
2021-10-27 18:30:34 +02:00
const WATypingTimeout = 15 * time . Second
func ( user * User ) handleChatPresence ( presence * events . ChatPresence ) {
puppet := user . bridge . GetPuppetByJID ( presence . Sender )
portal := user . GetPortalByJID ( presence . Chat )
if puppet == nil || portal == nil || len ( portal . MXID ) == 0 {
return
}
if presence . State == types . ChatPresenceComposing {
if puppet . typingIn != "" && puppet . typingAt . Add ( WATypingTimeout ) . Before ( time . Now ( ) ) {
if puppet . typingIn == portal . MXID {
return
}
_ , _ = puppet . IntentFor ( portal ) . UserTyping ( puppet . typingIn , false , 0 )
}
_ , _ = puppet . IntentFor ( portal ) . UserTyping ( portal . MXID , true , WATypingTimeout . Milliseconds ( ) )
puppet . typingIn = portal . MXID
puppet . typingAt = time . Now ( )
} else {
_ , _ = puppet . IntentFor ( portal ) . UserTyping ( portal . MXID , false , 0 )
puppet . typingIn = ""
}
}
2021-10-22 19:14:34 +02:00
func ( user * User ) handleReceipt ( receipt * events . Receipt ) {
2021-12-07 14:30:08 +01:00
if receipt . Type != events . ReceiptTypeRead && receipt . Type != events . ReceiptTypeReadSelf {
2019-05-30 16:00:36 +02:00
return
}
2021-12-25 19:50:36 +01:00
portal := user . GetPortalByMessageSource ( receipt . MessageSource )
2021-10-22 19:14:34 +02:00
if portal == nil || len ( portal . MXID ) == 0 {
2019-05-30 16:00:36 +02:00
return
}
2022-05-02 14:35:47 +02:00
portal . messages <- PortalMessage { receipt : receipt , source : user }
2021-10-22 19:14:34 +02:00
}
2022-01-17 21:56:18 +01:00
func makeReadMarkerContent ( eventID id . EventID , doublePuppet bool ) CustomReadMarkers {
var extra CustomReadReceipt
if doublePuppet {
extra . DoublePuppetSource = doublePuppetValue
}
return CustomReadMarkers {
ReqSetReadMarkers : mautrix . ReqSetReadMarkers {
Read : eventID ,
FullyRead : eventID ,
} ,
ReadExtra : extra ,
FullyReadExtra : extra ,
}
}
2021-10-28 11:59:22 +02:00
func ( user * User ) markSelfReadFull ( portal * Portal ) {
puppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if puppet == nil || puppet . CustomIntent ( ) == nil {
return
}
lastMessage := user . bridge . DB . Message . GetLastInChat ( portal . Key )
if lastMessage == nil {
return
}
2021-11-30 15:38:37 +01:00
user . SetLastReadTS ( portal . Key , lastMessage . Timestamp )
2022-01-17 21:56:18 +01:00
err := puppet . CustomIntent ( ) . SetReadMarkers ( portal . MXID , makeReadMarkerContent ( lastMessage . MXID , true ) )
2021-10-28 11:59:22 +02:00
if err != nil {
2021-11-10 19:26:28 +01:00
user . log . Warnfln ( "Failed to mark %s (last message) in %s as read: %v" , lastMessage . MXID , portal . MXID , err )
2022-01-17 09:38:44 +01:00
} else {
user . log . Debugfln ( "Marked %s (last message) in %s as read" , lastMessage . MXID , portal . MXID )
2021-10-28 11:59:22 +02:00
}
}
2022-05-18 18:40:29 +02:00
func ( user * User ) markUnread ( portal * Portal , unread bool ) {
puppet := user . bridge . GetPuppetByCustomMXID ( user . MXID )
if puppet == nil || puppet . CustomIntent ( ) == nil {
return
}
err := puppet . CustomIntent ( ) . SetRoomAccountData ( portal . MXID , "m.marked_unread" ,
map [ string ] bool { "unread" : unread } )
if err != nil {
user . log . Warnfln ( "Failed to mark %s as unread via m.marked_unread: %v" , portal . MXID , err )
} else {
user . log . Debugfln ( "Marked %s as unread via m.marked_unread: %v" , portal . MXID , err )
}
err = puppet . CustomIntent ( ) . SetRoomAccountData ( portal . MXID , "com.famedly.marked_unread" ,
map [ string ] bool { "unread" : unread } )
if err != nil {
user . log . Warnfln ( "Failed to mark %s as unread via com.famedly.marked_unread: %v" , portal . MXID , err )
} else {
user . log . Debugfln ( "Marked %s as unread via com.famedly.marked_unread: %v" , portal . MXID , err )
}
}
2021-10-31 18:59:23 +01:00
func ( user * User ) handleGroupCreate ( evt * events . JoinedGroup ) {
portal := user . GetPortalByJID ( evt . JID )
if len ( portal . MXID ) == 0 {
2022-03-25 07:15:52 +01:00
err := portal . CreateMatrixRoom ( user , & evt . GroupInfo , true , true )
2021-10-31 18:59:23 +01:00
if err != nil {
user . log . Errorln ( "Failed to create Matrix room after join notification: %v" , err )
}
} else {
2021-11-01 10:28:52 +01:00
portal . UpdateMatrixRoom ( user , & evt . GroupInfo )
2021-10-31 18:59:23 +01:00
}
}
2021-10-28 11:59:22 +02:00
func ( user * User ) handleGroupUpdate ( evt * events . GroupInfo ) {
portal := user . GetPortalByJID ( evt . JID )
if portal == nil || len ( portal . MXID ) == 0 {
user . log . Debugfln ( "Ignoring group info update in chat with no portal: %+v" , evt )
return
}
switch {
case evt . Announce != nil :
portal . RestrictMessageSending ( evt . Announce . IsAnnounce )
case evt . Locked != nil :
portal . RestrictMetadataChanges ( evt . Locked . IsLocked )
case evt . Name != nil :
portal . UpdateName ( evt . Name . Name , evt . Name . NameSetBy , true )
case evt . Topic != nil :
portal . UpdateTopic ( evt . Topic . Topic , evt . Topic . TopicSetBy , true )
case evt . Leave != nil :
if evt . Sender != nil && ! evt . Sender . IsEmpty ( ) {
portal . HandleWhatsAppKick ( user , * evt . Sender , evt . Leave )
}
case evt . Join != nil :
portal . HandleWhatsAppInvite ( user , evt . Sender , evt . Join )
case evt . Promote != nil :
portal . ChangeAdminStatus ( evt . Promote , true )
case evt . Demote != nil :
portal . ChangeAdminStatus ( evt . Demote , false )
2022-01-07 14:05:09 +01:00
case evt . Ephemeral != nil :
portal . UpdateGroupDisappearingMessages ( evt . Sender , evt . Timestamp , evt . Ephemeral . DisappearingTimer )
2021-10-28 11:59:22 +02:00
}
}
func ( user * User ) handlePictureUpdate ( evt * events . Picture ) {
if evt . JID . Server == types . DefaultUserServer {
puppet := user . bridge . GetPuppetByJID ( evt . JID )
2021-11-08 12:04:39 +01:00
user . log . Debugfln ( "Received picture update for puppet %s (current: %s, new: %s)" , evt . JID , puppet . Avatar , evt . PictureID )
2021-10-28 11:59:22 +02:00
if puppet . Avatar != evt . PictureID {
puppet . UpdateAvatar ( user )
}
2021-11-08 12:04:39 +01:00
} else if portal := user . GetPortalByJID ( evt . JID ) ; portal != nil {
user . log . Debugfln ( "Received picture update for portal %s (current: %s, new: %s)" , evt . JID , portal . Avatar , evt . PictureID )
if portal . Avatar != evt . PictureID {
2021-10-28 11:59:22 +02:00
portal . UpdateAvatar ( user , evt . Author , true )
}
}
}
2022-02-17 14:14:53 +01:00
func ( user * User ) StartPM ( jid types . JID , reason string ) ( * Portal , * Puppet , bool , error ) {
user . log . Debugln ( "Starting PM with" , jid , "from" , reason )
puppet := user . bridge . GetPuppetByJID ( jid )
2022-05-13 10:34:45 +02:00
puppet . SyncContact ( user , true , false , reason )
2022-02-17 14:14:53 +01:00
portal := user . GetPortalByJID ( puppet . JID )
if len ( portal . MXID ) > 0 {
ok := portal . ensureUserInvited ( user )
if ! ok {
portal . log . Warnfln ( "ensureUserInvited(%s) returned false, creating new portal" , user . MXID )
portal . MXID = ""
} else {
return portal , puppet , false , nil
}
}
2022-03-25 07:15:52 +01:00
err := portal . CreateMatrixRoom ( user , nil , false , true )
2022-02-17 14:14:53 +01:00
return portal , puppet , true , err
}
2022-03-01 19:25:46 +01:00
const groupListCacheMaxAge = 24 * time . Hour
func ( user * User ) getCachedGroupList ( ) ( [ ] * types . GroupInfo , error ) {
user . groupListCacheLock . Lock ( )
defer user . groupListCacheLock . Unlock ( )
if user . groupListCache != nil && user . groupListCacheTime . Add ( groupListCacheMaxAge ) . After ( time . Now ( ) ) {
return user . groupListCache , nil
}
var err error
user . groupListCache , err = user . Client . GetJoinedGroups ( )
user . groupListCacheTime = time . Now ( )
return user . groupListCache , err
}