2023-07-02 14:21:55 +00:00
// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
// Copyright (C) 2023 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 (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
2023-07-16 22:13:46 +00:00
"sync/atomic"
2023-07-02 14:21:55 +00:00
"time"
"github.com/rs/zerolog"
2023-08-02 11:37:01 +00:00
"google.golang.org/protobuf/proto"
2023-07-02 14:21:55 +00:00
"maunium.net/go/maulogger/v2"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
2024-02-22 21:05:00 +00:00
"maunium.net/go/mautrix/bridge/commands"
2023-07-02 14:21:55 +00:00
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/pushrules"
"go.mau.fi/mautrix-gmessages/database"
"go.mau.fi/mautrix-gmessages/libgm"
"go.mau.fi/mautrix-gmessages/libgm/events"
2023-07-17 13:51:31 +00:00
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
2023-07-02 14:21:55 +00:00
)
type User struct {
* database . User
Client * libgm . Client
bridge * GMBridge
zlog zerolog . Logger
// Deprecated
log maulogger . Logger
Admin bool
Whitelisted bool
PermissionLevel bridgeconfig . PermissionLevel
mgmtCreateLock sync . Mutex
spaceCreateLock sync . Mutex
connLock sync . Mutex
2024-02-22 21:05:00 +00:00
BridgeState * bridge . BridgeStateQueue
CommandState * commands . CommandState
2023-07-02 14:21:55 +00:00
spaceMembershipChecked bool
2023-10-05 09:56:42 +00:00
longPollingError error
browserInactiveType status . BridgeStateErrorCode
2023-12-13 23:18:08 +00:00
switchedToGoogleLogin bool
2023-10-05 09:56:42 +00:00
batteryLow bool
mobileData bool
phoneResponding bool
ready bool
sessionID string
batteryLowAlertSent time . Time
pollErrorAlertSent bool
phoneNotRespondingAlertSent bool
2023-07-15 12:02:03 +00:00
2024-02-22 21:05:00 +00:00
loginInProgress atomic . Bool
pairSuccessChan chan struct { }
ongoingLoginChan <- chan qrChannelItem
lastQRCode string
cancelLogin func ( )
2023-07-03 13:14:04 +00:00
2024-02-23 14:11:10 +00:00
googleAsyncPairErrChan atomic . Pointer [ chan error ]
2023-07-16 12:55:30 +00:00
DoublePuppetIntent * appservice . IntentAPI
2023-07-02 14:21:55 +00:00
}
2024-02-22 21:05:00 +00:00
func ( user * User ) GetCommandState ( ) * commands . CommandState {
return user . CommandState
}
func ( user * User ) SetCommandState ( state * commands . CommandState ) {
user . CommandState = state
}
var _ commands . CommandingUser = ( * User ) ( nil )
2023-07-02 14:21:55 +00:00
func ( br * GMBridge ) getUserByMXID ( userID id . UserID , onlyIfExists bool ) * User {
_ , isPuppet := br . ParsePuppetMXID ( userID )
if isPuppet || userID == br . Bot . UserID {
return nil
}
br . usersLock . Lock ( )
defer br . usersLock . Unlock ( )
user , ok := br . usersByMXID [ userID ]
if ! ok {
userIDPtr := & userID
if onlyIfExists {
userIDPtr = nil
}
dbUser , err := br . DB . User . GetByMXID ( context . TODO ( ) , userID )
if err != nil {
br . ZLog . Err ( err ) .
Str ( "user_id" , userID . String ( ) ) .
Msg ( "Failed to load user from database" )
return nil
}
return br . loadDBUser ( dbUser , userIDPtr )
}
return user
}
func ( br * GMBridge ) GetUserByMXID ( userID id . UserID ) * User {
return br . getUserByMXID ( userID , false )
}
func ( br * GMBridge ) GetIUser ( userID id . UserID , create bool ) bridge . User {
u := br . getUserByMXID ( userID , ! create )
if u == nil {
return nil
}
return u
}
func ( user * User ) GetPuppetByID ( id , phone string ) * Puppet {
return user . bridge . GetPuppetByKey ( database . Key { Receiver : user . RowID , ID : id } , phone )
}
func ( user * User ) GetPortalByID ( id string ) * Portal {
return user . bridge . GetPortalByKey ( database . Key { Receiver : user . RowID , ID : id } )
}
func ( user * User ) GetIDoublePuppet ( ) bridge . DoublePuppet {
return user
}
func ( user * User ) GetIGhost ( ) bridge . Ghost {
return nil
}
func ( user * User ) GetPermissionLevel ( ) bridgeconfig . PermissionLevel {
return user . PermissionLevel
}
func ( user * User ) GetManagementRoomID ( ) id . RoomID {
return user . ManagementRoom
}
func ( user * User ) GetMXID ( ) id . UserID {
return user . MXID
}
func ( br * GMBridge ) GetUserByMXIDIfExists ( userID id . UserID ) * User {
return br . getUserByMXID ( userID , true )
}
func ( br * GMBridge ) GetAllUsersWithSession ( ) [ ] * User {
return br . loadManyUsers ( br . DB . User . GetAllWithSession )
}
func ( br * GMBridge ) GetAllUsersWithDoublePuppet ( ) [ ] * User {
return br . loadManyUsers ( br . DB . User . GetAllWithDoublePuppet )
}
func ( br * GMBridge ) loadManyUsers ( query func ( ctx context . Context ) ( [ ] * database . User , error ) ) [ ] * User {
br . usersLock . Lock ( )
defer br . usersLock . Unlock ( )
dbUsers , err := query ( context . TODO ( ) )
if err != nil {
br . ZLog . Err ( err ) . Msg ( "Failed to all load users from database" )
return [ ] * User { }
}
output := make ( [ ] * User , len ( dbUsers ) )
for index , dbUser := range dbUsers {
user , ok := br . usersByMXID [ dbUser . MXID ]
if ! ok {
user = br . loadDBUser ( dbUser , nil )
}
output [ index ] = user
}
return output
}
func ( br * GMBridge ) loadDBUser ( dbUser * database . User , mxid * id . UserID ) * User {
if dbUser == nil {
if mxid == nil {
return nil
}
dbUser = br . DB . User . New ( )
dbUser . MXID = * mxid
err := dbUser . Insert ( context . TODO ( ) )
if err != nil {
br . ZLog . Err ( err ) .
Str ( "user_id" , mxid . String ( ) ) .
Msg ( "Failed to insert user to database" )
return nil
}
}
user := br . NewUser ( dbUser )
br . usersByMXID [ user . MXID ] = user
if len ( user . ManagementRoom ) > 0 {
br . managementRooms [ user . ManagementRoom ] = user
}
return user
}
func ( br * GMBridge ) NewUser ( dbUser * database . User ) * User {
user := & User {
User : dbUser ,
bridge : br ,
zlog : br . ZLog . With ( ) . Str ( "user_id" , dbUser . MXID . String ( ) ) . Logger ( ) ,
}
user . log = maulogadapt . ZeroAsMau ( & user . zlog )
2023-07-15 23:11:25 +00:00
user . longPollingError = errors . New ( "not connected" )
2023-07-19 21:58:39 +00:00
user . phoneResponding = true
2023-07-02 14:21:55 +00:00
user . PermissionLevel = user . bridge . Config . Bridge . Permissions . Get ( user . MXID )
user . Whitelisted = user . PermissionLevel >= bridgeconfig . PermissionLevelUser
user . Admin = user . PermissionLevel >= bridgeconfig . PermissionLevelAdmin
user . BridgeState = br . NewBridgeStateQueue ( user )
return user
}
func ( user * User ) ensureInvited ( intent * appservice . IntentAPI , roomID id . RoomID , isDirect bool ) ( ok bool ) {
extraContent := make ( map [ string ] any )
if isDirect {
extraContent [ "is_direct" ] = true
}
if user . DoublePuppetIntent != nil {
extraContent [ "fi.mau.will_auto_accept" ] = true
}
_ , err := intent . InviteUser ( roomID , & mautrix . ReqInviteUser { UserID : user . MXID } , extraContent )
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
return
} else if err != nil {
user . zlog . Warn ( ) . Err ( err ) . Str ( "room_id" , roomID . String ( ) ) . Msg ( "Failed to invite user to room" )
} else {
ok = true
}
if user . DoublePuppetIntent != nil {
err = user . DoublePuppetIntent . EnsureJoined ( roomID , appservice . EnsureJoinedParams { IgnoreCache : true } )
if err != nil {
user . zlog . Warn ( ) . Err ( err ) . Str ( "room_id" , roomID . String ( ) ) . Msg ( "Failed to auto-join room" )
ok = false
} else {
ok = true
}
}
return
}
func ( user * User ) GetSpaceRoom ( ) id . RoomID {
if ! user . bridge . Config . Bridge . PersonalFilteringSpaces {
return ""
}
if len ( user . SpaceRoom ) == 0 {
user . spaceCreateLock . Lock ( )
defer user . spaceCreateLock . Unlock ( )
if len ( user . SpaceRoom ) > 0 {
return user . SpaceRoom
}
resp , err := user . bridge . Bot . CreateRoom ( & mautrix . ReqCreateRoom {
Visibility : "private" ,
Name : "Google Messages" ,
Topic : "Your Google Messages 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 ,
} ,
PowerLevelOverride : & event . PowerLevelsEventContent {
Users : map [ id . UserID ] int {
user . bridge . Bot . UserID : 9001 ,
user . MXID : 50 ,
} ,
} ,
} )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to auto-create space room" )
} else {
user . SpaceRoom = resp . RoomID
err = user . Update ( context . TODO ( ) )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to update database after creating space room" )
}
user . ensureInvited ( user . bridge . Bot , user . SpaceRoom , false )
}
} else if ! user . spaceMembershipChecked && ! user . bridge . StateStore . IsInRoom ( user . SpaceRoom , user . MXID ) {
user . ensureInvited ( user . bridge . Bot , user . SpaceRoom , false )
}
user . spaceMembershipChecked = true
return user . SpaceRoom
}
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
}
creationContent := make ( map [ string ] interface { } )
if ! user . bridge . Config . Bridge . FederateRooms {
creationContent [ "m.federate" ] = false
}
resp , err := user . bridge . Bot . CreateRoom ( & mautrix . ReqCreateRoom {
Topic : "Google Messages bridge notices" ,
IsDirect : true ,
CreationContent : creationContent ,
} )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to auto-create management room" )
} else {
user . SetManagementRoom ( resp . RoomID )
}
}
return user . ManagementRoom
}
func ( user * User ) SetManagementRoom ( roomID id . RoomID ) {
log := user . zlog . With ( ) .
Str ( "management_room_id" , roomID . String ( ) ) .
Str ( "action" , "SetManagementRoom" ) .
Logger ( )
existingUser , ok := user . bridge . managementRooms [ roomID ]
if ok {
existingUser . ManagementRoom = ""
err := existingUser . Update ( context . TODO ( ) )
if err != nil {
log . Err ( err ) .
Str ( "prev_user_id" , existingUser . MXID . String ( ) ) .
Msg ( "Failed to clear management room from previous user" )
}
}
user . ManagementRoom = roomID
user . bridge . managementRooms [ user . ManagementRoom ] = user
err := user . Update ( context . TODO ( ) )
if err != nil {
log . Err ( err ) . Msg ( "Failed to update database with management room ID" )
}
}
var ErrAlreadyLoggedIn = errors . New ( "already logged in" )
2023-07-16 12:55:30 +00:00
var ErrLoginInProgress = errors . New ( "login already in progress" )
var ErrLoginTimeout = errors . New ( "login timed out" )
2023-07-02 14:21:55 +00:00
2023-07-16 12:55:30 +00:00
func ( user * User ) createClient ( sess * libgm . AuthData ) {
user . Client = libgm . NewClient ( sess , user . zlog . With ( ) . Str ( "component" , "libgm" ) . Logger ( ) )
2023-07-10 22:20:50 +00:00
user . Client . SetEventHandler ( user . syncHandleEvent )
}
2023-07-16 12:55:30 +00:00
type qrChannelItem struct {
success bool
qr string
err error
}
2023-07-16 22:13:46 +00:00
func ( qci qrChannelItem ) IsEmpty ( ) bool {
return ! qci . success && qci . qr == "" && qci . err == nil
}
2023-07-18 11:51:43 +00:00
func ( user * User ) Login ( maxAttempts int ) ( <- chan qrChannelItem , error ) {
2023-07-02 14:21:55 +00:00
user . connLock . Lock ( )
defer user . connLock . Unlock ( )
if user . Session != nil {
return nil , ErrAlreadyLoggedIn
2023-07-16 22:13:46 +00:00
} else if ! user . loginInProgress . CompareAndSwap ( false , true ) {
return user . ongoingLoginChan , ErrLoginInProgress
}
if user . Client != nil {
2023-07-02 14:21:55 +00:00
user . unlockedDeleteConnection ( )
}
2023-07-16 12:55:30 +00:00
pairSuccessChan := make ( chan struct { } )
user . pairSuccessChan = pairSuccessChan
user . createClient ( libgm . NewAuthData ( ) )
qr , err := user . Client . StartLogin ( )
2023-07-02 14:21:55 +00:00
if err != nil {
2023-08-24 10:37:21 +00:00
user . unlockedDeleteConnection ( )
2023-07-16 12:55:30 +00:00
user . pairSuccessChan = nil
2023-07-16 22:13:46 +00:00
user . loginInProgress . Store ( false )
2023-07-02 14:21:55 +00:00
return nil , fmt . Errorf ( "failed to connect to Google Messages: %w" , err )
}
2023-10-09 12:03:58 +00:00
Analytics . Track ( user . MXID , "$login_start" )
2023-07-16 12:55:30 +00:00
ch := make ( chan qrChannelItem , maxAttempts + 2 )
2023-07-18 11:51:43 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
user . cancelLogin = cancel
2023-07-16 22:13:46 +00:00
user . ongoingLoginChan = ch
2023-07-16 12:55:30 +00:00
ch <- qrChannelItem { qr : qr }
2023-07-18 11:51:43 +00:00
user . lastQRCode = qr
2023-07-16 12:55:30 +00:00
go func ( ) {
ticker := time . NewTicker ( 30 * time . Second )
success := false
defer func ( ) {
ticker . Stop ( )
if ! success {
user . zlog . Debug ( ) . Msg ( "Deleting connection as login wasn't successful" )
user . DeleteConnection ( )
}
user . pairSuccessChan = nil
2023-07-16 22:13:46 +00:00
user . ongoingLoginChan = nil
2023-07-18 11:51:43 +00:00
user . lastQRCode = ""
2023-07-16 12:55:30 +00:00
close ( ch )
2023-07-16 22:13:46 +00:00
user . loginInProgress . Store ( false )
2023-07-18 11:51:43 +00:00
cancel ( )
user . cancelLogin = nil
2023-07-16 12:55:30 +00:00
} ( )
2023-07-16 22:13:46 +00:00
for {
maxAttempts --
2023-07-16 12:55:30 +00:00
select {
case <- ctx . Done ( ) :
user . zlog . Debug ( ) . Err ( ctx . Err ( ) ) . Msg ( "Login context cancelled" )
return
case <- ticker . C :
2023-07-16 22:13:46 +00:00
if maxAttempts <= 0 {
ch <- qrChannelItem { err : ErrLoginTimeout }
return
}
2023-07-16 12:55:30 +00:00
qr , err := user . Client . RefreshPhoneRelay ( )
if err != nil {
ch <- qrChannelItem { err : fmt . Errorf ( "failed to refresh QR code: %w" , err ) }
return
}
ch <- qrChannelItem { qr : qr }
2023-07-18 11:51:43 +00:00
user . lastQRCode = qr
2023-07-16 12:55:30 +00:00
case <- pairSuccessChan :
ch <- qrChannelItem { success : true }
success = true
return
}
}
} ( )
return ch , nil
2023-07-02 14:21:55 +00:00
}
2024-02-23 14:11:10 +00:00
func ( user * User ) AsyncLoginGoogleStart ( cookies map [ string ] string ) ( outEmoji string , outErr error ) {
errChan := make ( chan error , 1 )
if ! user . googleAsyncPairErrChan . CompareAndSwap ( nil , & errChan ) {
close ( errChan )
outErr = fmt . Errorf ( "login already in progress" )
return
}
var callbackDone bool
var initialWait sync . WaitGroup
initialWait . Add ( 1 )
callback := func ( emoji string ) {
callbackDone = true
outEmoji = emoji
initialWait . Done ( )
}
go func ( ) {
err := user . LoginGoogle ( cookies , callback )
if ! callbackDone {
initialWait . Done ( )
outErr = err
close ( errChan )
user . googleAsyncPairErrChan . Store ( nil )
} else {
errChan <- err
}
} ( )
initialWait . Wait ( )
return
}
func ( user * User ) AsyncLoginGoogleWait ( ) error {
ch := user . googleAsyncPairErrChan . Swap ( nil )
if ch == nil {
return fmt . Errorf ( "no login in progress" )
}
return <- * ch
}
2024-02-22 21:05:00 +00:00
func ( user * User ) LoginGoogle ( cookies map [ string ] string , emojiCallback func ( string ) ) error {
user . connLock . Lock ( )
defer user . connLock . Unlock ( )
if user . Session != nil {
return ErrAlreadyLoggedIn
} else if ! user . loginInProgress . CompareAndSwap ( false , true ) {
return ErrLoginInProgress
}
if user . Client != nil {
user . unlockedDeleteConnection ( )
}
pairSuccessChan := make ( chan struct { } )
user . pairSuccessChan = pairSuccessChan
authData := libgm . NewAuthData ( )
authData . Cookies = cookies
user . createClient ( authData )
Analytics . Track ( user . MXID , "$login_start" )
err := user . Client . DoGaiaPairing ( emojiCallback )
if err != nil {
user . unlockedDeleteConnection ( )
user . pairSuccessChan = nil
user . loginInProgress . Store ( false )
return fmt . Errorf ( "failed to connect to Google Messages: %w" , err )
}
return nil
}
2023-07-02 14:21:55 +00:00
func ( user * User ) Connect ( ) bool {
user . connLock . Lock ( )
defer user . connLock . Unlock ( )
if user . Client != nil {
return true
} else if user . Session == nil {
return false
}
2023-07-02 20:47:31 +00:00
if len ( user . AccessToken ) == 0 {
user . tryAutomaticDoublePuppeting ( )
}
2023-07-02 14:21:55 +00:00
user . zlog . Debug ( ) . Msg ( "Connecting to Google Messages" )
2023-07-03 21:03:36 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnecting , Error : GMConnecting } )
2023-07-16 12:55:30 +00:00
user . createClient ( user . Session )
2023-07-09 11:16:52 +00:00
err := user . Client . Connect ( )
2023-07-02 14:21:55 +00:00
if err != nil {
user . zlog . Err ( err ) . Msg ( "Error connecting to Google Messages" )
2023-09-04 11:24:45 +00:00
if errors . Is ( err , events . ErrRequestedEntityNotFound ) {
go user . Logout ( status . BridgeState {
StateEvent : status . StateBadCredentials ,
2023-09-04 11:33:08 +00:00
Error : GMUnpaired404 ,
2023-09-04 11:24:45 +00:00
Info : map [ string ] any {
"go_error" : err . Error ( ) ,
} ,
} , false )
} else {
user . BridgeState . Send ( status . BridgeState {
StateEvent : status . StateUnknownError ,
Error : GMConnectionFailed ,
Info : map [ string ] interface { } {
"go_error" : err . Error ( ) ,
} ,
} )
}
2023-07-02 14:21:55 +00:00
return false
}
return true
}
func ( user * User ) unlockedDeleteConnection ( ) {
if user . Client == nil {
return
}
user . Client . Disconnect ( )
user . Client . SetEventHandler ( nil )
user . Client = nil
}
func ( user * User ) DeleteConnection ( ) {
user . connLock . Lock ( )
defer user . connLock . Unlock ( )
user . unlockedDeleteConnection ( )
2023-07-15 23:11:25 +00:00
user . longPollingError = errors . New ( "not connected" )
2023-07-19 22:54:30 +00:00
user . phoneResponding = true
2024-01-16 12:17:18 +00:00
user . batteryLow = false
user . switchedToGoogleLogin = false
user . ready = false
user . browserInactiveType = ""
2023-07-02 14:21:55 +00:00
}
func ( user * User ) HasSession ( ) bool {
return user . Session != nil
}
func ( user * User ) DeleteSession ( ) {
user . Session = nil
2023-07-19 18:15:24 +00:00
user . SelfParticipantIDs = [ ] string { }
2023-07-02 14:21:55 +00:00
err := user . Update ( context . TODO ( ) )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to delete session from database" )
}
}
func ( user * User ) IsConnected ( ) bool {
return user . Client != nil && user . Client . IsConnected ( )
}
func ( user * User ) IsLoggedIn ( ) bool {
return user . IsConnected ( ) && user . Client . IsLoggedIn ( )
}
2023-07-19 20:41:17 +00:00
func ( user * User ) sendMarkdownBridgeAlert ( important bool , formatString string , args ... interface { } ) {
2023-07-02 14:21:55 +00:00
if user . bridge . Config . Bridge . DisableBridgeAlerts {
return
}
notice := fmt . Sprintf ( formatString , args ... )
content := format . RenderMarkdown ( notice , true , false )
2023-07-19 20:41:17 +00:00
if ! important {
content . MsgType = event . MsgNotice
}
2023-07-02 14:21:55 +00:00
_ , err := user . bridge . Bot . SendMessageEvent ( user . GetManagementRoom ( ) , event . EventMessage , content )
if err != nil {
user . zlog . Warn ( ) . Err ( err ) . Str ( "notice" , notice ) . Msg ( "Failed to send bridge alert" )
}
}
2023-08-10 12:40:43 +00:00
func ( user * User ) syncHandleEvent ( event any ) {
2023-07-02 14:21:55 +00:00
switch v := event . ( type ) {
2023-07-03 21:03:36 +00:00
case * events . ListenFatalError :
2023-08-10 12:40:43 +00:00
go user . Logout ( status . BridgeState {
2023-08-10 08:39:18 +00:00
StateEvent : status . StateUnknownError ,
2023-07-03 21:03:36 +00:00
Error : GMFatalError ,
2023-07-15 12:02:03 +00:00
Info : map [ string ] any { "go_error" : v . Error . Error ( ) } ,
2023-07-15 23:11:25 +00:00
} , false )
2023-07-19 20:41:17 +00:00
go user . sendMarkdownBridgeAlert ( true , "Fatal error while listening to Google Messages: %v - Log in again to continue using the bridge" , v . Error )
2023-07-03 21:03:36 +00:00
case * events . ListenTemporaryError :
2023-07-15 12:02:03 +00:00
user . longPollingError = v . Error
2023-07-03 21:03:36 +00:00
user . BridgeState . Send ( status . BridgeState {
StateEvent : status . StateTransientDisconnect ,
Error : GMListenError ,
2023-07-15 12:02:03 +00:00
Info : map [ string ] any { "go_error" : v . Error . Error ( ) } ,
2023-07-03 21:03:36 +00:00
} )
2023-07-19 20:41:17 +00:00
if ! user . pollErrorAlertSent {
go user . sendMarkdownBridgeAlert ( false , "Temporary error while listening to Google Messages: %v" , v . Error )
user . pollErrorAlertSent = true
}
2023-07-03 21:03:36 +00:00
case * events . ListenRecovered :
2023-07-15 12:02:03 +00:00
user . longPollingError = nil
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
2023-07-19 20:41:17 +00:00
if user . pollErrorAlertSent {
go user . sendMarkdownBridgeAlert ( false , "Reconnected to Google Messages" )
user . pollErrorAlertSent = false
}
2023-07-19 21:58:39 +00:00
case * events . PhoneNotResponding :
user . phoneResponding = false
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
2023-10-05 09:56:42 +00:00
// TODO make this properly configurable
if user . zlog . Trace ( ) . Enabled ( ) && ! user . phoneNotRespondingAlertSent {
go user . sendMarkdownBridgeAlert ( false , "Phone is not responding" )
user . phoneNotRespondingAlertSent = true
}
2023-07-19 21:58:39 +00:00
case * events . PhoneRespondingAgain :
user . phoneResponding = true
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
2023-10-05 09:56:42 +00:00
if user . phoneNotRespondingAlertSent {
go user . sendMarkdownBridgeAlert ( false , "Phone is responding again" )
user . phoneNotRespondingAlertSent = false
}
2023-09-04 11:24:45 +00:00
case * events . PingFailed :
if errors . Is ( v . Error , events . ErrRequestedEntityNotFound ) {
go user . Logout ( status . BridgeState {
StateEvent : status . StateBadCredentials ,
2023-09-04 11:33:08 +00:00
Error : GMUnpaired404 ,
2023-09-04 11:24:45 +00:00
Info : map [ string ] any {
"go_error" : v . Error . Error ( ) ,
} ,
} , false )
2023-11-06 14:05:21 +00:00
} else if v . ErrorCount > 1 {
2023-09-04 11:24:45 +00:00
user . BridgeState . Send ( status . BridgeState {
StateEvent : status . StateUnknownError ,
Error : GMPingFailed ,
Info : map [ string ] any { "go_error" : v . Error . Error ( ) } ,
} )
2023-11-06 14:05:21 +00:00
} else {
user . zlog . Debug ( ) . Msg ( "Not sending unknown error for first ping fail" )
2023-09-04 11:24:45 +00:00
}
2023-07-02 14:21:55 +00:00
case * events . PairSuccessful :
2023-07-16 12:55:30 +00:00
user . Session = user . Client . AuthData
2023-08-10 12:55:33 +00:00
if user . PhoneID != "" && user . PhoneID != v . GetMobile ( ) . GetSourceID ( ) {
user . zlog . Warn ( ) .
Str ( "old_phone_id" , user . PhoneID ) .
Str ( "new_phone_id" , v . GetMobile ( ) . GetSourceID ( ) ) .
Msg ( "Phone ID changed, resetting state" )
user . ResetState ( )
}
2023-07-19 17:32:01 +00:00
user . PhoneID = v . GetMobile ( ) . GetSourceID ( )
2023-07-02 14:21:55 +00:00
err := user . Update ( context . TODO ( ) )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to update session in database" )
}
2023-07-16 12:55:30 +00:00
if ch := user . pairSuccessChan ; ch != nil {
close ( ch )
}
2023-08-10 12:40:43 +00:00
go user . tryAutomaticDoublePuppeting ( )
2023-07-17 13:51:31 +00:00
case * gmproto . RevokePairData :
2023-07-15 23:41:34 +00:00
user . zlog . Info ( ) . Any ( "revoked_device" , v . GetRevokedDevice ( ) ) . Msg ( "Got pair revoked event" )
2023-08-10 12:40:43 +00:00
go user . Logout ( status . BridgeState {
2023-07-15 23:41:34 +00:00
StateEvent : status . StateBadCredentials ,
Error : GMUnpaired ,
} , false )
2023-08-10 12:40:43 +00:00
go user . sendMarkdownBridgeAlert ( true , "Unpaired from Google Messages. Log in again to continue using the bridge." )
2023-07-09 11:16:52 +00:00
case * events . AuthTokenRefreshed :
2023-08-10 12:40:43 +00:00
go func ( ) {
err := user . Update ( context . TODO ( ) )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to update session in database" )
}
} ( )
2023-07-17 13:51:31 +00:00
case * gmproto . Conversation :
2023-08-10 12:40:43 +00:00
go user . syncConversation ( v , "event" )
2023-08-24 11:48:03 +00:00
//case *gmproto.Message:
case * libgm . WrappedMessage :
2023-08-24 11:34:45 +00:00
user . zlog . Debug ( ) .
Str ( "conversation_id" , v . GetConversationID ( ) ) .
Str ( "participant_id" , v . GetParticipantID ( ) ) .
Str ( "message_id" , v . GetMessageID ( ) ) .
Str ( "message_status" , v . GetMessageStatus ( ) . GetStatus ( ) . String ( ) ) .
Int64 ( "message_ts" , v . GetTimestamp ( ) ) .
Str ( "tmp_id" , v . GetTmpID ( ) ) .
2023-09-07 14:29:48 +00:00
Bool ( "is_old" , v . IsOld ) .
2023-08-24 11:34:45 +00:00
Msg ( "Received message" )
2023-07-09 11:16:52 +00:00
portal := user . GetPortalByID ( v . GetConversationID ( ) )
2023-08-24 11:48:03 +00:00
portal . messages <- PortalMessage { evt : v . Message , source : user , raw : v . Data }
2023-07-17 13:51:31 +00:00
case * gmproto . UserAlertEvent :
2023-07-15 12:02:03 +00:00
user . handleUserAlert ( v )
2023-08-30 16:35:02 +00:00
case * gmproto . Settings :
user . handleSettings ( v )
2023-12-13 23:32:36 +00:00
case * events . AccountChange :
2023-12-13 23:18:08 +00:00
user . handleAccountChange ( v )
2023-07-02 14:21:55 +00:00
default :
2023-07-10 22:20:50 +00:00
user . zlog . Trace ( ) . Any ( "data" , v ) . Type ( "data_type" , v ) . Msg ( "Unknown event" )
2023-07-02 14:21:55 +00:00
}
}
2023-08-10 12:55:33 +00:00
func ( user * User ) ResetState ( ) {
portals := user . bridge . GetAllPortalsForUser ( user . RowID )
user . zlog . Debug ( ) . Int ( "portal_count" , len ( portals ) ) . Msg ( "Deleting portals" )
for _ , portal := range portals {
portal . Delete ( )
}
user . bridge . DeleteAllPuppetsForUser ( user . RowID )
user . PhoneID = ""
go func ( ) {
user . zlog . Debug ( ) . Msg ( "Cleaning up portal rooms in background" )
for _ , portal := range portals {
portal . Cleanup ( )
}
user . zlog . Debug ( ) . Msg ( "Finished cleaning up portals" )
} ( )
}
2023-07-19 20:04:28 +00:00
func ( user * User ) aggressiveSetActive ( ) {
2023-07-19 21:58:39 +00:00
sleepTimes := [ ] int { 5 , 10 , 30 }
for i := 0 ; i < 3 ; i ++ {
2023-07-19 20:04:28 +00:00
sleep := time . Duration ( sleepTimes [ i ] ) * time . Second
user . zlog . Info ( ) .
Int ( "sleep_seconds" , int ( sleep . Seconds ( ) ) ) .
2023-07-19 21:58:39 +00:00
Msg ( "Aggressively reactivating bridge session after sleep" )
2023-07-19 20:04:28 +00:00
time . Sleep ( sleep )
2023-07-19 21:58:39 +00:00
if user . browserInactiveType == "" {
user . zlog . Info ( ) . Msg ( "Bridge session became active on its own, not reactivating" )
return
}
user . zlog . Info ( ) . Msg ( "Now reactivating bridge session" )
2023-07-19 20:04:28 +00:00
err := user . Client . SetActiveSession ( )
if err != nil {
user . zlog . Warn ( ) . Err ( err ) . Msg ( "Failed to set self as active session" )
} else {
break
}
}
}
2023-07-19 20:30:27 +00:00
func ( user * User ) fetchAndSyncConversations ( ) {
2023-07-19 20:33:45 +00:00
user . zlog . Info ( ) . Msg ( "Fetching conversation list" )
resp , err := user . Client . ListConversations ( user . bridge . Config . Bridge . InitialChatSyncCount , gmproto . ListConversationsRequest_INBOX )
2023-07-19 20:30:27 +00:00
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to get conversation list" )
return
}
2023-07-19 20:33:45 +00:00
user . zlog . Info ( ) . Int ( "count" , len ( resp . GetConversations ( ) ) ) . Msg ( "Syncing conversations" )
2023-07-19 20:30:27 +00:00
for _ , conv := range resp . GetConversations ( ) {
2023-08-02 11:54:08 +00:00
user . syncConversation ( conv , "sync" )
2023-07-19 20:30:27 +00:00
}
}
2023-12-13 23:32:36 +00:00
func ( user * User ) handleAccountChange ( v * events . AccountChange ) {
2023-12-13 23:18:08 +00:00
user . zlog . Debug ( ) .
Str ( "account" , v . GetAccount ( ) ) .
Bool ( "enabled" , v . GetEnabled ( ) ) .
2023-12-13 23:32:36 +00:00
Bool ( "fake" , v . IsFake ) .
2023-12-13 23:18:08 +00:00
Msg ( "Got account change event" )
2023-12-13 23:32:36 +00:00
user . switchedToGoogleLogin = v . GetEnabled ( ) || v . IsFake
if ! v . IsFake {
if user . switchedToGoogleLogin {
go user . sendMarkdownBridgeAlert ( true , "The bridge will not work when the account-based pairing method is enabled in the Google Messages app. Unlink other devices and switch back to the QR code method to continue using the bridge." )
} else {
go user . sendMarkdownBridgeAlert ( false , "Switched back to QR pairing, bridge should work now" )
2023-12-13 23:36:43 +00:00
// Assume connection is ready now even if it wasn't before
user . ready = true
2023-12-13 23:32:36 +00:00
}
2023-12-13 23:18:08 +00:00
}
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
}
2023-07-17 13:51:31 +00:00
func ( user * User ) handleUserAlert ( v * gmproto . UserAlertEvent ) {
2023-07-19 20:30:27 +00:00
user . zlog . Debug ( ) . Str ( "alert_type" , v . GetAlertType ( ) . String ( ) ) . Msg ( "Got user alert event" )
2023-07-19 20:04:28 +00:00
becameInactive := false
2023-07-15 12:02:03 +00:00
switch v . GetAlertType ( ) {
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_BROWSER_INACTIVE :
2023-07-15 12:02:03 +00:00
user . browserInactiveType = GMBrowserInactive
2023-07-19 20:04:28 +00:00
becameInactive = true
2023-07-19 20:30:27 +00:00
case gmproto . AlertType_BROWSER_ACTIVE :
2023-08-02 12:07:02 +00:00
wasInactive := user . browserInactiveType != "" || ! user . ready
2023-07-19 20:41:17 +00:00
user . pollErrorAlertSent = false
2023-07-19 20:30:27 +00:00
user . browserInactiveType = ""
user . ready = true
2023-08-02 12:07:02 +00:00
newSessionID := user . Client . CurrentSessionID ( )
if user . sessionID != newSessionID || wasInactive {
user . zlog . Debug ( ) .
Str ( "old_session_id" , user . sessionID ) .
Str ( "new_session_id" , newSessionID ) .
Msg ( "Session ID changed for browser active event, resyncing" )
user . sessionID = newSessionID
go user . fetchAndSyncConversations ( )
2023-08-10 12:40:43 +00:00
go user . sendMarkdownBridgeAlert ( false , "Connected to Google Messages" )
2023-08-02 12:07:02 +00:00
} else {
user . zlog . Debug ( ) .
Str ( "session_id" , user . sessionID ) .
Msg ( "Session ID didn't change for browser active event, not resyncing" )
}
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_BROWSER_INACTIVE_FROM_TIMEOUT :
2023-07-15 12:02:03 +00:00
user . browserInactiveType = GMBrowserInactiveTimeout
2023-07-19 20:04:28 +00:00
becameInactive = true
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_BROWSER_INACTIVE_FROM_INACTIVITY :
2023-07-15 12:02:03 +00:00
user . browserInactiveType = GMBrowserInactiveInactivity
2023-07-19 20:04:28 +00:00
becameInactive = true
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_MOBILE_DATA_CONNECTION :
2023-07-15 12:02:03 +00:00
user . mobileData = true
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_MOBILE_WIFI_CONNECTION :
2023-07-15 12:02:03 +00:00
user . mobileData = false
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_MOBILE_BATTERY_LOW :
2023-07-15 12:02:03 +00:00
user . batteryLow = true
2023-07-19 20:41:17 +00:00
if time . Since ( user . batteryLowAlertSent ) > 30 * time . Minute {
go user . sendMarkdownBridgeAlert ( true , "Your phone's battery is low" )
user . batteryLowAlertSent = time . Now ( )
}
2023-07-17 13:51:31 +00:00
case gmproto . AlertType_MOBILE_BATTERY_RESTORED :
2023-07-15 12:02:03 +00:00
user . batteryLow = false
2023-07-19 20:41:17 +00:00
if ! user . batteryLowAlertSent . IsZero ( ) {
go user . sendMarkdownBridgeAlert ( false , "Phone battery restored" )
user . batteryLowAlertSent = time . Time { }
}
2023-07-15 12:02:03 +00:00
default :
return
}
2023-07-19 20:04:28 +00:00
if becameInactive {
if user . bridge . Config . GoogleMessages . AggressiveReconnect {
go user . aggressiveSetActive ( )
} else {
2023-07-19 20:41:17 +00:00
go user . sendMarkdownBridgeAlert ( true , "Google Messages was opened in another browser. Use `set-active` to reconnect the bridge." )
2023-07-19 20:04:28 +00:00
}
}
2023-07-15 12:02:03 +00:00
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
}
2023-08-30 16:35:02 +00:00
func ( user * User ) handleSettings ( settings * gmproto . Settings ) {
if settings . SIMCards == nil {
return
}
ctx := context . TODO ( )
2023-08-30 17:45:14 +00:00
changed := user . SetSIMs ( settings . SIMCards )
newRCSSettings := settings . GetRCSSettings ( )
if user . Settings . RCSEnabled != newRCSSettings . GetIsEnabled ( ) ||
user . Settings . ReadReceipts != newRCSSettings . GetSendReadReceipts ( ) ||
user . Settings . TypingNotifications != newRCSSettings . GetShowTypingIndicators ( ) ||
2023-09-04 11:40:33 +00:00
user . Settings . IsDefaultSMSApp != newRCSSettings . GetIsDefaultSMSApp ( ) ||
! user . Settings . SettingsReceived {
2023-08-30 17:45:14 +00:00
user . Settings = database . Settings {
2023-09-04 11:40:33 +00:00
SettingsReceived : true ,
2023-08-30 17:45:14 +00:00
RCSEnabled : newRCSSettings . GetIsEnabled ( ) ,
ReadReceipts : newRCSSettings . GetSendReadReceipts ( ) ,
TypingNotifications : newRCSSettings . GetShowTypingIndicators ( ) ,
IsDefaultSMSApp : newRCSSettings . GetIsDefaultSMSApp ( ) ,
}
changed = true
}
if changed {
2023-08-30 16:35:02 +00:00
err := user . Update ( ctx )
if err != nil {
user . zlog . Err ( err ) . Msg ( "Failed to save SIM details" )
}
user . BridgeState . Send ( status . BridgeState { StateEvent : status . StateConnected } )
}
}
2023-07-15 12:02:03 +00:00
func ( user * User ) FillBridgeState ( state status . BridgeState ) status . BridgeState {
if state . Info == nil {
state . Info = make ( map [ string ] any )
}
if state . StateEvent == status . StateConnected {
2023-08-30 16:35:02 +00:00
state . Info [ "sims" ] = user . GetSIMsForBridgeState ( )
2023-08-30 17:45:14 +00:00
state . Info [ "settings" ] = user . Settings
2023-07-15 23:11:25 +00:00
state . Info [ "battery_low" ] = user . batteryLow
state . Info [ "mobile_data" ] = user . mobileData
state . Info [ "browser_active" ] = user . browserInactiveType == ""
2023-12-13 23:18:08 +00:00
state . Info [ "google_account_pairing" ] = user . switchedToGoogleLogin
2023-07-19 20:30:27 +00:00
if ! user . ready {
state . StateEvent = status . StateConnecting
state . Error = GMConnecting
}
2023-07-19 21:58:39 +00:00
if ! user . phoneResponding {
state . StateEvent = status . StateBadCredentials
state . Error = GMPhoneNotResponding
}
2023-12-13 23:18:08 +00:00
if user . switchedToGoogleLogin {
state . StateEvent = status . StateBadCredentials
state . Error = GMSwitchedToGoogleLogin
}
2023-07-15 12:02:03 +00:00
if user . longPollingError != nil {
state . StateEvent = status . StateTransientDisconnect
state . Error = GMListenError
state . Info [ "go_error" ] = user . longPollingError . Error ( )
}
if user . browserInactiveType != "" {
2023-07-19 21:58:39 +00:00
if user . bridge . Config . GoogleMessages . AggressiveReconnect {
state . StateEvent = status . StateTransientDisconnect
} else {
state . StateEvent = status . StateBadCredentials
}
2023-07-15 12:02:03 +00:00
state . Error = user . browserInactiveType
}
}
return state
}
2023-07-15 23:11:25 +00:00
func ( user * User ) Logout ( state status . BridgeState , unpair bool ) ( logoutOK bool ) {
if user . Client != nil && unpair {
_ , err := user . Client . Unpair ( )
if err != nil {
user . zlog . Debug ( ) . Err ( err ) . Msg ( "Error sending unpair request" )
} else {
logoutOK = true
}
}
2023-07-15 12:02:03 +00:00
user . DeleteConnection ( )
user . DeleteSession ( )
2023-08-10 12:55:33 +00:00
user . BridgeState . Send ( state )
2023-07-15 23:11:25 +00:00
return
2023-07-15 12:02:03 +00:00
}
2023-08-24 11:22:22 +00:00
func conversationDataIsSus ( portal * Portal , v * gmproto . Conversation ) bool {
if ! portal . IsPrivateChat ( ) {
// Group chats hopefully never get bad updates
return false
} else if v . IsGroupChat {
// Group chat update for a DM is always sus
return true
}
count := 0
mainName := ""
for _ , pcp := range v . Participants {
if ! pcp . IsMe {
if count == 0 {
mainName = pcp . FullName
count ++
} else if mainName != pcp . FullName {
count ++
}
}
}
// If there are multiple names in a DM, that's sus even if it's not marked as a group
return count > 1
}
2023-08-02 11:54:08 +00:00
func ( user * User ) syncConversation ( v * gmproto . Conversation , source string ) {
2023-07-11 22:57:07 +00:00
updateType := v . GetStatus ( )
2023-07-09 14:26:34 +00:00
portal := user . GetPortalByID ( v . GetConversationID ( ) )
2023-08-02 11:37:01 +00:00
convCopy := proto . Clone ( v ) . ( * gmproto . Conversation )
convCopy . LatestMessage = nil
2023-07-24 13:46:54 +00:00
log := portal . zlog . With ( ) .
Str ( "action" , "sync conversation" ) .
Str ( "conversation_status" , updateType . String ( ) ) .
2023-08-02 11:54:08 +00:00
Str ( "data_source" , source ) .
2023-08-02 11:37:01 +00:00
Interface ( "conversation_data" , convCopy ) .
2023-07-24 13:46:54 +00:00
Logger ( )
2023-08-21 16:42:57 +00:00
if cancel := portal . cancelCreation . Load ( ) ; cancel != nil {
if updateType == gmproto . ConversationStatus_SPAM_FOLDER || updateType == gmproto . ConversationStatus_BLOCKED_FOLDER {
( * cancel ) ( fmt . Errorf ( "conversation was moved to spam" ) )
2023-08-25 17:47:20 +00:00
} else if updateType == gmproto . ConversationStatus_DELETED {
( * cancel ) ( fmt . Errorf ( "conversation was deleted" ) )
portal . Delete ( )
2023-08-21 16:42:57 +00:00
} else {
log . Debug ( ) . Msg ( "Conversation creation is still pending, ignoring new sync event" )
return
}
}
2023-07-09 14:26:34 +00:00
if portal . MXID != "" {
2023-07-11 22:57:07 +00:00
switch updateType {
2023-07-24 13:46:54 +00:00
case gmproto . ConversationStatus_DELETED :
log . Info ( ) . Msg ( "Got delete event, cleaning up portal" )
2023-07-11 22:57:07 +00:00
portal . Delete ( )
2023-08-10 12:55:33 +00:00
portal . Cleanup ( )
2023-08-21 16:42:57 +00:00
case gmproto . ConversationStatus_SPAM_FOLDER , gmproto . ConversationStatus_BLOCKED_FOLDER :
log . Info ( ) . Msg ( "Got spam/block event, cleaning up portal" )
portal . Cleanup ( )
portal . RemoveMXID ( context . TODO ( ) )
2023-07-11 22:57:07 +00:00
default :
2023-07-24 13:46:54 +00:00
if v . Participants == nil {
log . Debug ( ) . Msg ( "Not syncing conversation with nil participants" )
return
2023-08-24 11:22:22 +00:00
} else if conversationDataIsSus ( portal , v ) {
log . Warn ( ) . Msg ( "Ignoring suspicious update for private chat" )
2023-08-21 09:11:56 +00:00
return
2023-07-24 13:46:54 +00:00
}
log . Debug ( ) . Msg ( "Syncing existing portal" )
2023-07-11 22:57:07 +00:00
portal . UpdateMetadata ( user , v )
2023-07-19 19:15:34 +00:00
user . syncChatDoublePuppetDetails ( portal , v , false )
2023-08-09 13:30:02 +00:00
go portal . missedForwardBackfill (
user ,
time . UnixMicro ( v . LastMessageTimestamp ) ,
v . LatestMessageID ,
! v . GetUnread ( ) ,
source == "event" ,
)
2023-07-11 22:57:07 +00:00
}
2023-07-24 13:46:54 +00:00
} else if updateType == gmproto . ConversationStatus_ACTIVE || updateType == gmproto . ConversationStatus_ARCHIVED {
if v . Participants == nil {
log . Debug ( ) . Msg ( "Not syncing conversation with nil participants" )
return
}
2023-08-21 16:42:57 +00:00
if source == "event" {
go func ( ) {
ctx , cancel := context . WithCancelCause ( context . TODO ( ) )
cancelPtr := & cancel
defer func ( ) {
portal . cancelCreation . CompareAndSwap ( cancelPtr , nil )
cancel ( nil )
} ( )
portal . cancelCreation . Store ( cancelPtr )
log . Debug ( ) . Msg ( "Creating portal for conversation in 5 seconds" )
select {
case <- time . After ( 5 * time . Second ) :
case <- ctx . Done ( ) :
log . Debug ( ) . Err ( ctx . Err ( ) ) . Msg ( "Portal creation was cancelled" )
return
}
err := portal . CreateMatrixRoom ( user , v , source == "sync" )
if err != nil {
log . Err ( err ) . Msg ( "Error creating Matrix room from conversation event" )
}
} ( )
} else {
log . Debug ( ) . Msg ( "Creating portal for conversation" )
err := portal . CreateMatrixRoom ( user , v , source == "sync" )
if err != nil {
log . Err ( err ) . Msg ( "Error creating Matrix room from conversation event" )
}
2023-07-09 14:26:34 +00:00
}
2023-07-11 22:57:07 +00:00
} else {
2023-07-24 13:46:54 +00:00
log . Debug ( ) . Msg ( "Not creating portal for conversation" )
2023-07-09 14:26:34 +00:00
}
}
2023-07-02 14:21:55 +00:00
func ( user * User ) updateChatMute ( portal * Portal , mutedUntil time . Time ) {
intent := user . DoublePuppetIntent
if intent == nil || len ( portal . MXID ) == 0 {
return
}
var err error
if mutedUntil . IsZero ( ) && mutedUntil . Before ( time . Now ( ) ) {
user . log . Debugfln ( "Portal %s is muted until %s, unmuting..." , portal . MXID , mutedUntil )
err = intent . DeletePushRule ( "global" , pushrules . RoomRule , string ( portal . MXID ) )
} else {
user . log . Debugfln ( "Portal %s is muted until %s, muting..." , portal . MXID , mutedUntil )
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 )
}
}
type CustomTagData struct {
Order json . Number ` json:"order" `
DoublePuppet string ` json:"fi.mau.double_puppet_source" `
}
type CustomTagEventContent struct {
Tags map [ string ] CustomTagData ` json:"tags" `
}
2023-07-19 19:35:19 +00:00
func ( user * User ) updateChatTag ( portal * Portal , tag string , active bool , existingTags CustomTagEventContent ) {
var err error
2023-07-02 14:21:55 +00:00
currentTag , ok := existingTags . Tags [ tag ]
if active && ! ok {
2023-07-19 19:35:19 +00:00
user . zlog . Debug ( ) . Str ( "tag" , tag ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Adding room tag" )
2023-07-02 14:21:55 +00:00
data := CustomTagData { Order : "0.5" , DoublePuppet : user . bridge . Name }
2023-07-19 19:35:19 +00:00
err = user . DoublePuppetIntent . AddTagWithCustomData ( portal . MXID , tag , & data )
2023-07-02 14:21:55 +00:00
} else if ! active && ok && currentTag . DoublePuppet == user . bridge . Name {
2023-07-19 19:35:19 +00:00
user . zlog . Debug ( ) . Str ( "tag" , tag ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Removing room tag" )
err = user . DoublePuppetIntent . RemoveTag ( portal . MXID , tag )
2023-07-02 14:21:55 +00:00
} else {
err = nil
}
if err != nil {
2023-07-19 19:35:19 +00:00
user . zlog . Warn ( ) . Err ( err ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Failed to update room tag" )
2023-07-02 14:21:55 +00:00
}
}
type CustomReadReceipt struct {
Timestamp int64 ` json:"ts,omitempty" `
DoublePuppetSource string ` json:"fi.mau.double_puppet_source,omitempty" `
}
type CustomReadMarkers struct {
mautrix . ReqSetReadMarkers
ReadExtra CustomReadReceipt ` json:"com.beeper.read.extra" `
FullyReadExtra CustomReadReceipt ` json:"com.beeper.fully_read.extra" `
}
2023-07-19 19:15:34 +00:00
func ( user * User ) markSelfReadFull ( portal * Portal , lastMessageID string ) {
2023-08-09 14:53:11 +00:00
if user . DoublePuppetIntent == nil || portal . lastUserReadID == lastMessageID {
2023-08-09 13:30:02 +00:00
return
}
2023-07-19 19:15:34 +00:00
ctx := context . TODO ( )
2023-08-14 11:32:12 +00:00
lastMessage , err := user . bridge . DB . Message . GetByID ( ctx , portal . Receiver , lastMessageID )
2023-08-10 08:30:17 +00:00
if err == nil && lastMessage == nil || lastMessage . IsFakeMXID ( ) {
lastMessage , err = user . bridge . DB . Message . GetLastInChatWithMXID ( ctx , portal . Key )
2023-07-19 19:15:34 +00:00
}
if err != nil {
user . zlog . Warn ( ) . Err ( err ) . Msg ( "Failed to get last message in chat to mark it as read" )
return
2023-08-09 14:53:11 +00:00
} else if lastMessage == nil || portal . lastUserReadID == lastMessage . ID {
2023-07-19 19:15:34 +00:00
return
}
log := user . zlog . With ( ) .
Str ( "conversation_id" , portal . ID ) .
Str ( "message_id" , lastMessage . ID ) .
Str ( "room_id" , portal . ID ) .
Str ( "event_id" , lastMessage . MXID . String ( ) ) .
Logger ( )
err = user . DoublePuppetIntent . SetReadMarkers ( portal . MXID , & CustomReadMarkers {
ReqSetReadMarkers : mautrix . ReqSetReadMarkers {
Read : lastMessage . MXID ,
FullyRead : lastMessage . MXID ,
} ,
ReadExtra : CustomReadReceipt { DoublePuppetSource : user . bridge . Name } ,
FullyReadExtra : CustomReadReceipt { DoublePuppetSource : user . bridge . Name } ,
} )
if err != nil {
log . Warn ( ) . Err ( err ) . Msg ( "Failed to mark last message in chat as read" )
} else {
log . Debug ( ) . Msg ( "Marked last message in chat as read" )
2023-08-09 14:53:11 +00:00
portal . lastUserReadID = lastMessage . ID
2023-07-19 19:15:34 +00:00
}
}
2023-07-17 13:51:31 +00:00
func ( user * User ) syncChatDoublePuppetDetails ( portal * Portal , conv * gmproto . Conversation , justCreated bool ) {
2023-07-02 14:21:55 +00:00
if user . DoublePuppetIntent == nil || len ( portal . MXID ) == 0 {
return
}
if justCreated || ! user . bridge . Config . Bridge . TagOnlyOnCreate {
2023-07-19 19:35:19 +00:00
var existingTags CustomTagEventContent
err := user . DoublePuppetIntent . GetTagsWithCustomData ( portal . MXID , & existingTags )
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
user . zlog . Warn ( ) . Err ( err ) . Str ( "room_id" , portal . MXID . String ( ) ) . Msg ( "Failed to get existing room tags" )
}
2023-07-24 13:46:54 +00:00
user . updateChatTag ( portal , user . bridge . Config . Bridge . ArchiveTag , conv . Status == gmproto . ConversationStatus_ARCHIVED || conv . Status == gmproto . ConversationStatus_KEEP_ARCHIVED , existingTags )
2023-07-19 19:35:19 +00:00
user . updateChatTag ( portal , user . bridge . Config . Bridge . PinnedTag , conv . Pinned , existingTags )
2023-07-02 14:21:55 +00:00
}
}
func ( user * User ) UpdateDirectChats ( chats map [ id . UserID ] [ ] id . RoomID ) {
if ! user . bridge . Config . Bridge . SyncDirectChatList || user . DoublePuppetIntent == nil {
return
}
intent := user . DoublePuppetIntent
method := http . MethodPatch
//if chats == nil {
// chats = user.getDirectChats()
// method = http.MethodPut
//}
user . zlog . Debug ( ) . Msg ( "Updating m.direct list on homeserver" )
var err error
if user . bridge . Config . Homeserver . Software == bridgeconfig . SoftwareAsmux {
urlPath := intent . BuildClientURL ( "unstable" , "com.beeper.asmux" , "dms" )
_ , err = intent . MakeFullRequest ( mautrix . FullRequest {
Method : method ,
URL : urlPath ,
Headers : http . Header { "X-Asmux-Auth" : { user . bridge . AS . Registration . AppToken } } ,
RequestJSON : chats ,
} )
} 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 )
}
}
func ( user * User ) markUnread ( portal * Portal , unread bool ) {
if user . DoublePuppetIntent == nil {
return
}
log := user . zlog . With ( ) . Str ( "room_id" , portal . MXID . String ( ) ) . Logger ( )
err := user . DoublePuppetIntent . SetRoomAccountData ( portal . MXID , "m.marked_unread" , map [ string ] bool { "unread" : unread } )
if err != nil {
log . Warn ( ) . Err ( err ) . Str ( "event_type" , "m.marked_unread" ) .
Msg ( "Failed to mark room as unread" )
} else {
log . Debug ( ) . Str ( "event_type" , "m.marked_unread" ) . Msg ( "Marked room as unread" )
}
err = user . DoublePuppetIntent . SetRoomAccountData ( portal . MXID , "com.famedly.marked_unread" , map [ string ] bool { "unread" : unread } )
if err != nil {
log . Warn ( ) . Err ( err ) . Str ( "event_type" , "com.famedly.marked_unread" ) .
Msg ( "Failed to mark room as unread" )
} else {
log . Debug ( ) . Str ( "event_type" , "com.famedly.marked_unread" ) . Msg ( "Marked room as unread" )
}
}