Looks like 2FA works in facebook messenger :o

This commit is contained in:
Alex 2020-10-04 21:20:22 +02:00
parent 7130c7b7d6
commit 07f9f640f0
7 changed files with 144 additions and 42 deletions

View file

@ -71,6 +71,10 @@ type Connector interface {
// in which case the backend is free to re-use the ID or select a new one. // in which case the backend is free to re-use the ID or select a new one.
Send(event *Event) (string, error) Send(event *Event) (string, error)
// Used to send user commands directly
// (first use case: receive 2-factor authentication codes)
UserCommand(string)
// Close the connection // Close the connection
Close() Close()
} }

View file

@ -51,10 +51,12 @@ const (
LEAVE = "leave" LEAVE = "leave"
SEARCH = "search" SEARCH = "search"
SEND = "send" SEND = "send"
USER_COMMAND = "user_command"
CLOSE = "close" CLOSE = "close"
// external -> ezbr // external -> ezbr
SAVE_CONFIG = "save_config" SAVE_CONFIG = "save_config"
SYSTEM_MESSAGE = "system_message"
JOINED = "joined" JOINED = "joined"
LEFT = "left" LEFT = "left"
USER_INFO_UPDATED = "user_info_updated" USER_INFO_UPDATED = "user_info_updated"
@ -249,7 +251,7 @@ func (m *extMessageWithData) UnmarshalJSON(jj []byte) error {
} }
m.Data = sr.Data m.Data = sr.Data
return nil return nil
case JOINED, LEFT, CACHE_PUT, CACHE_GET, REP_OK, REP_ERROR: case SYSTEM_MESSAGE, JOINED, LEFT, CACHE_PUT, CACHE_GET, REP_OK, REP_ERROR:
return nil return nil
default: default:
return fmt.Errorf("Invalid message type for message from external program: '%s'", c.MsgType) return fmt.Errorf("Invalid message type for message from external program: '%s'", c.MsgType)
@ -377,6 +379,8 @@ func (ext *External) handleCmd(msg *extMessageWithData) {
switch msg.MsgType { switch msg.MsgType {
case SAVE_CONFIG: case SAVE_CONFIG:
ext.handler.SaveConfig(msg.Data.(Configuration)) ext.handler.SaveConfig(msg.Data.(Configuration))
case SYSTEM_MESSAGE:
ext.handler.SystemMessage(msg.Value)
case JOINED: case JOINED:
ext.handler.Joined(msg.Room) ext.handler.Joined(msg.Room)
case LEFT: case LEFT:
@ -475,3 +479,10 @@ func (ext *External) Send(event *Event) (string, error) {
} }
return rep.EventId, nil return rep.EventId, nil
} }
func (ext *External) UserCommand(cm string) {
ext.cmd(extMessage{
MsgType: USER_COMMAND,
Value: cm,
}, nil)
}

View file

@ -271,6 +271,10 @@ func (irc *IRC) Send(event *Event) (string, error) {
return "", nil return "", nil
} }
func (irc *IRC) UserCommand(cm string) {
irc.handler.SystemMessage("Command not supported.")
}
func (irc *IRC) Close() { func (irc *IRC) Close() {
conn := irc.conn conn := irc.conn
irc.conn = nil irc.conn = nil

View file

@ -334,6 +334,10 @@ func (mm *Mattermost) Send(event *Event) (string, error) {
return created_post.Id, nil return created_post.Id, nil
} }
func (mm *Mattermost) UserCommand(cm string) {
mm.handler.SystemMessage("Command not supported.")
}
func (mm *Mattermost) Close() { func (mm *Mattermost) Close() {
if mm.conn != nil { if mm.conn != nil {
mm.conn.WsQuit = true mm.conn.WsQuit = true

View file

@ -367,6 +367,10 @@ func (xm *XMPP) Send(event *Event) (string, error) {
} }
} }
func (xm *XMPP) UserCommand(cmd string) {
xm.handler.SystemMessage("Command not supported.")
}
func (xm *XMPP) Close() { func (xm *XMPP) Close() {
if xm.conn != nil { if xm.conn != nil {
xm.conn.Close() xm.conn.Close()

144
external/messenger.py vendored
View file

@ -29,10 +29,12 @@ INVITE = "invite"
LEAVE = "leave" LEAVE = "leave"
SEARCH = "search" SEARCH = "search"
SEND = "send" SEND = "send"
USER_COMMAND = "user_command"
CLOSE = "close" CLOSE = "close"
# external -> ezbr # external -> ezbr
SAVE_CONFIG = "save_config" SAVE_CONFIG = "save_config"
SYSTEM_MESSAGE = "system_message"
JOINED = "joined" JOINED = "joined"
LEFT = "left" LEFT = "left"
USER_INFO_UPDATED = "user_info_updated" USER_INFO_UPDATED = "user_info_updated"
@ -71,9 +73,9 @@ def stripFbLinkPrefix(url):
# ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ---- # ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ----
class MessengerBridgeClient(fbchat.Client): class MessengerBridgeClient(fbchat.Client):
def __init__(self, *args, **kwargs): def __init__(self, bridge, *args, **kwargs):
self.bridge = bridge
super(MessengerBridgeClient, self).__init__(*args, **kwargs) super(MessengerBridgeClient, self).__init__(*args, **kwargs)
self.bridge = None
def setBridge(self, bridge): def setBridge(self, bridge):
self.bridge = bridge self.bridge = bridge
@ -87,9 +89,20 @@ class MessengerBridgeClient(fbchat.Client):
self.bridge.onPersonRemoved(*args, **kwargs) self.bridge.onPersonRemoved(*args, **kwargs)
def onTitleChange(self, *args, **kwargs): def onTitleChange(self, *args, **kwargs):
self.bridge.onTitleChange(*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 ---- # ---- 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): class SyncerThread(threading.Thread):
def __init__(self, bridge, thread_queue, *args, **kwargs): def __init__(self, bridge, thread_queue, *args, **kwargs):
super(SyncerThread, self).__init__(*args, **kwargs) super(SyncerThread, self).__init__(*args, **kwargs)
@ -121,6 +134,9 @@ class MessengerBridge:
def __init__(self): def __init__(self):
self.init_backlog_length = 100 self.init_backlog_length = 100
self.config = None
self.login_in_progress = None
# We cache maps between two kinds of identifiers: # We cache maps between two kinds of identifiers:
# - facebook uids of users # - facebook uids of users
# - identifiers for the bridge, which are the username when defined (otherwise equal to above) # - 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>") # 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 = {} self.others_joined_map = {}
# queue for thread syncing
self.sync_thread_queue = queue.Queue(100)
def getUserId(self, user): def getUserId(self, user):
retval = None retval = None
if user.url is not None and not "?" in user.url: if user.url is not None and not "?" in user.url:
@ -222,48 +241,21 @@ class MessengerBridge:
sys.stdout.write(msgstr + "\n") sys.stdout.write(msgstr + "\n")
sys.stdout.flush() sys.stdout.flush()
def system_message(self, msg):
self.write({
"_type": SYSTEM_MESSAGE,
"value": msg,
})
def handle_cmd(self, cmd): def handle_cmd(self, cmd):
ty = cmd["_type"] ty = cmd["_type"]
if ty == CONFIGURE: if ty == CONFIGURE:
self.init_backlog_length = int(cmd["data"]["initial_backlog"]) if self.login_in_progress is None:
self.config = cmd["data"]
has_pickle = "client_pickle" in cmd["data"] and len(cmd["data"]["client_pickle"]) > 0 self.login_in_progress = queue.Queue(1)
if has_pickle: LoginThread(self).start()
data = base64.b64decode(cmd["data"]["client_pickle"])
data = zlib.decompress(data)
self.client = pickle.loads(data)
else: else:
email, password = cmd["data"]["email"], cmd["data"]["password"] return {"_type": REP_ERROR, "error": "Already logging in (CONFIGURE sent twice)"}
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()
elif ty == CLOSE: elif ty == CLOSE:
self.close() self.close()
@ -272,7 +264,10 @@ class MessengerBridge:
return {"_type": REP_OK, "user": self.my_user_id} return {"_type": REP_OK, "user": self.my_user_id}
elif ty == JOIN: 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: elif ty == LEAVE:
thread_id = cmd["room"] thread_id = cmd["room"]
@ -331,6 +326,9 @@ class MessengerBridge:
elif ty == REP_OK and cmd["_id"] in self.cache_gets: elif ty == REP_OK and cmd["_id"] in self.cache_gets:
self.cache_gets[cmd["_id"]].put(cmd["value"]) self.cache_gets[cmd["_id"]].put(cmd["value"])
elif ty == USER_COMMAND:
self.handleUserCommand(cmd["value"])
else: else:
return {"_type": REP_ERROR, "error": "Not implemented"} return {"_type": REP_ERROR, "error": "Not implemented"}
@ -354,6 +352,51 @@ class MessengerBridge:
def cache_put(self, key, value): def cache_put(self, key, value):
self.write({"_type": CACHE_PUT, "key": key, "value": 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 ---- # ---- Info sync ----
def ensure_i_joined(self, thread_id): def ensure_i_joined(self, thread_id):
@ -566,6 +609,25 @@ class MessengerBridge:
"data": {"name": new_title}, "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 ---- # ---- CLI ----
def createClientPickle(): def createClientPickle():

View file

@ -259,6 +259,7 @@ func handleSystemMessage(mxid string, msg string) {
ezbrSystemSend(mxid, "- join <protocol or account> <room id>: join public chat room") ezbrSystemSend(mxid, "- join <protocol or account> <room id>: join public chat room")
ezbrSystemSend(mxid, "- talk <protocol or account> <user id>: open private conversation to contact") ezbrSystemSend(mxid, "- talk <protocol or account> <user id>: open private conversation to contact")
ezbrSystemSend(mxid, "- search <protocol or account> <name>: search for users by name") ezbrSystemSend(mxid, "- search <protocol or account> <name>: search for users by name")
ezbrSystemSend(mxid, "- cmd <protocol or account> <command>: send special command to account")
case "list", "account", "accounts": case "list", "account", "accounts":
one := false one := false
if accts, ok := registeredAccounts[mxid]; ok { if accts, ok := registeredAccounts[mxid]; ok {
@ -332,6 +333,18 @@ func handleSystemMessage(mxid string, msg string) {
} else { } else {
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
} }
case "cmd":
if len(cmd) < 3 {
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
return
}
account := findAccount(mxid, cmd[1])
if account != nil {
account.Conn.UserCommand(strings.Join(cmd[2:], " "))
} else {
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
}
default: default:
ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!") ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!")
} }