@ -29,10 +29,12 @@ INVITE = "invite"
LEAVE = " leave "
SEARCH = " search "
SEND = " send "
USER_COMMAND = " user_command "
CLOSE = " close "
# external -> ezbr
SAVE_CONFIG = " save_config "
SYSTEM_MESSAGE = " system_message "
JOINED = " joined "
LEFT = " left "
USER_INFO_UPDATED = " user_info_updated "
@ -71,9 +73,9 @@ def stripFbLinkPrefix(url):
# ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ----
class MessengerBridgeClient ( fbchat . Client ) :
def __init__ ( self , * args , * * kwargs ) :
def __init__ ( self , bridge , * args , * * kwargs ) :
self . bridge = bridge
super ( MessengerBridgeClient , self ) . __init__ ( * args , * * kwargs )
self . bridge = None
def setBridge ( self , bridge ) :
self . bridge = bridge
@ -87,9 +89,20 @@ class MessengerBridgeClient(fbchat.Client):
self . bridge . onPersonRemoved ( * args , * * kwargs )
def onTitleChange ( self , * args , * * kwargs ) :
self . bridge . onTitleChange ( * args , * * kwargs )
def on2FACode ( self , * args , * * kwargs ) :
return self . bridge . on2FACode ( * args , * * kwargs )
# ---- SEPARATE THREADS FOR INITIAL SYNC & CLIENT LISTEN ----
class LoginThread ( threading . Thread ) :
def __init__ ( self , bridge , * args , * * kwargs ) :
super ( LoginThread , self ) . __init__ ( * args , * * kwargs )
self . bridge = bridge
def run ( self ) :
self . bridge . processLogin ( )
class SyncerThread ( threading . Thread ) :
def __init__ ( self , bridge , thread_queue , * args , * * kwargs ) :
super ( SyncerThread , self ) . __init__ ( * args , * * kwargs )
@ -121,6 +134,9 @@ class MessengerBridge:
def __init__ ( self ) :
self . init_backlog_length = 100
self . config = None
self . login_in_progress = None
# We cache maps between two kinds of identifiers:
# - facebook uids of users
# - identifiers for the bridge, which are the username when defined (otherwise equal to above)
@ -135,6 +151,9 @@ class MessengerBridge:
# caches for the people that are in rooms so that we don't send JOINED every time (map keys = "<userId>--<threadId>")
self . others_joined_map = { }
# queue for thread syncing
self . sync_thread_queue = queue . Queue ( 100 )
def getUserId ( self , user ) :
retval = None
if user . url is not None and not " ? " in user . url :
@ -222,48 +241,21 @@ class MessengerBridge:
sys . stdout . write ( msgstr + " \n " )
sys . stdout . flush ( )
def system_message ( self , msg ) :
self . write ( {
" _type " : SYSTEM_MESSAGE ,
" value " : msg ,
} )
def handle_cmd ( self , cmd ) :
ty = cmd [ " _type " ]
if ty == CONFIGURE :
self . init_backlog_length = int ( cmd [ " data " ] [ " initial_backlog " ] )
has_pickle = " client_pickle " in cmd [ " data " ] and len ( cmd [ " data " ] [ " client_pickle " ] ) > 0
if has_pickle :
data = base64 . b64decode ( cmd [ " data " ] [ " client_pickle " ] )
data = zlib . decompress ( data )
self . client = pickle . loads ( data )
if self . login_in_progress is None :
self . config = cmd [ " data " ]
self . login_in_progress = queue . Queue ( 1 )
LoginThread ( self ) . start ( )
else :
email , password = cmd [ " data " ] [ " email " ] , cmd [ " data " ] [ " password " ]
self . client = MessengerBridgeClient ( email = email , password = password , max_tries = 1 )
## TODO: save client in new client_pickle config value
if not self . client . isLoggedIn ( ) :
return { " _type " : REP_ERROR , " error " : " Unable to login (invalid pickle?) " }
if not has_pickle :
new_config = cmd [ " data " ]
data = pickle . dumps ( self . client )
data = zlib . compress ( data )
new_config [ " client_pickle " ] = base64 . b64encode ( data ) . decode ( ' ascii ' )
self . write ( { " _type " : SAVE_CONFIG , " data " : new_config } )
self . client . setBridge ( self )
self . my_user_id = self . getUserIdFromUid ( self . client . uid )
threads = self . client . fetchThreadList ( limit = 10 )
# ensure we have a correct mapping for bridged user IDs to fb uids
# (this should be fast)
for thread in threads :
if thread . type == ThreadType . USER :
self . getUserId ( thread )
self . sync_thread_queue = queue . Queue ( 100 )
SyncerThread ( self , self . sync_thread_queue ) . start ( )
for thread in reversed ( threads ) :
self . sync_thread_queue . put ( thread )
ClientListenThread ( self . client ) . start ( )
return { " _type " : REP_ERROR , " error " : " Already logging in (CONFIGURE sent twice) " }
elif ty == CLOSE :
self . close ( )
@ -272,7 +264,10 @@ class MessengerBridge:
return { " _type " : REP_OK , " user " : self . my_user_id }
elif ty == JOIN :
self . ensure_i_joined ( cmd [ " room " ] )
if self . client is None :
pass
else :
self . ensure_i_joined ( cmd [ " room " ] )
elif ty == LEAVE :
thread_id = cmd [ " room " ]
@ -331,6 +326,9 @@ class MessengerBridge:
elif ty == REP_OK and cmd [ " _id " ] in self . cache_gets :
self . cache_gets [ cmd [ " _id " ] ] . put ( cmd [ " value " ] )
elif ty == USER_COMMAND :
self . handleUserCommand ( cmd [ " value " ] )
else :
return { " _type " : REP_ERROR , " error " : " Not implemented " }
@ -354,6 +352,51 @@ class MessengerBridge:
def cache_put ( self , key , value ) :
self . write ( { " _type " : CACHE_PUT , " key " : key , " value " : value } )
# ---- Process login (called from separate thread) ----
def processLogin ( self ) :
self . init_backlog_length = int ( self . config [ " initial_backlog " ] )
has_pickle = " client_pickle " in self . config and len ( self . config [ " client_pickle " ] ) > 0
if has_pickle :
data = base64 . b64decode ( self . config [ " client_pickle " ] )
data = zlib . decompress ( data )
self . client = pickle . loads ( data )
else :
email , password = self . config [ " email " ] , self . config [ " password " ]
self . client = MessengerBridgeClient ( bridge = self , email = email , password = password , max_tries = 1 )
if not self . client . isLoggedIn ( ) :
self . system_message ( " Unable to login (invalid pickle? dunno) " )
else :
self . system_message ( " Login complete, will now sync threads. " )
if not has_pickle :
self . client . setBridge ( None )
data = pickle . dumps ( self . client )
data = zlib . compress ( data )
self . config [ " client_pickle " ] = base64 . b64encode ( data ) . decode ( ' ascii ' )
self . write ( { " _type " : SAVE_CONFIG , " data " : self . config } )
self . client . setBridge ( self )
self . my_user_id = self . getUserIdFromUid ( self . client . uid )
threads = self . client . fetchThreadList ( limit = 10 )
# ensure we have a correct mapping for bridged user IDs to fb uids
# (this should be fast)
for thread in threads :
if thread . type == ThreadType . USER :
self . getUserId ( thread )
SyncerThread ( self , self . sync_thread_queue ) . start ( )
for thread in reversed ( threads ) :
self . sync_thread_queue . put ( thread )
ClientListenThread ( self . client ) . start ( )
self . login_in_progress = None
# ---- Info sync ----
def ensure_i_joined ( self , thread_id ) :
@ -566,6 +609,25 @@ class MessengerBridge:
" data " : { " name " : new_title } ,
} )
def on2FACode ( self , * args , * * kwargs ) :
if self . login_in_progress is None :
self . system_message ( " Facebook messenger requests 2 factor authentication, but we have a bug so that won ' t work. " )
return None
else :
self . system_message ( " Facebook messenger requests 2 factor authentication. Enter it by saying: cmd messenger 2fa <your code> (replace messenger by your account name if you have several messenger accounts) " )
uc = self . login_in_progress . get ( block = True )
return uc [ " 2fa_code " ]
def handleUserCommand ( self , cmd ) :
cmd = cmd . split ( ' ' )
if cmd [ 0 ] == " 2fa " :
if self . login_in_progress is not None :
self . login_in_progress . put ( { " 2fa_code " : cmd [ 1 ] } )
else :
self . system_message ( " 2FA code not required at this point. " )
else :
self . system_message ( " Invalid user command. " )
# ---- CLI ----
def createClientPickle ( ) :