diff --git a/connector/connector.go b/connector/connector.go index 55e0e34..0819bd7 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -71,6 +71,10 @@ type Connector interface { // in which case the backend is free to re-use the ID or select a new one. 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() } diff --git a/connector/external/external.go b/connector/external/external.go index 9aae0f1..92339b9 100644 --- a/connector/external/external.go +++ b/connector/external/external.go @@ -51,10 +51,12 @@ const ( 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" @@ -249,7 +251,7 @@ func (m *extMessageWithData) UnmarshalJSON(jj []byte) error { } m.Data = sr.Data 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 default: 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 { case SAVE_CONFIG: ext.handler.SaveConfig(msg.Data.(Configuration)) + case SYSTEM_MESSAGE: + ext.handler.SystemMessage(msg.Value) case JOINED: ext.handler.Joined(msg.Room) case LEFT: @@ -475,3 +479,10 @@ func (ext *External) Send(event *Event) (string, error) { } return rep.EventId, nil } + +func (ext *External) UserCommand(cm string) { + ext.cmd(extMessage{ + MsgType: USER_COMMAND, + Value: cm, + }, nil) +} diff --git a/connector/irc/irc.go b/connector/irc/irc.go index 77388e7..1327eaa 100644 --- a/connector/irc/irc.go +++ b/connector/irc/irc.go @@ -271,6 +271,10 @@ func (irc *IRC) Send(event *Event) (string, error) { return "", nil } +func (irc *IRC) UserCommand(cm string) { + irc.handler.SystemMessage("Command not supported.") +} + func (irc *IRC) Close() { conn := irc.conn irc.conn = nil diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go index 9f8bbaf..86bf6b5 100644 --- a/connector/mattermost/mattermost.go +++ b/connector/mattermost/mattermost.go @@ -334,6 +334,10 @@ func (mm *Mattermost) Send(event *Event) (string, error) { return created_post.Id, nil } +func (mm *Mattermost) UserCommand(cm string) { + mm.handler.SystemMessage("Command not supported.") +} + func (mm *Mattermost) Close() { if mm.conn != nil { mm.conn.WsQuit = true diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go index 64c53d9..2d0260c 100644 --- a/connector/xmpp/xmpp.go +++ b/connector/xmpp/xmpp.go @@ -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() { if xm.conn != nil { xm.conn.Close() diff --git a/external/messenger.py b/external/messenger.py index 76ca90d..9ac7d26 100755 --- a/external/messenger.py +++ b/external/messenger.py @@ -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 = "--") 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 (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(): diff --git a/server.go b/server.go index e81f0b3..2ed598e 100644 --- a/server.go +++ b/server.go @@ -259,6 +259,7 @@ func handleSystemMessage(mxid string, msg string) { ezbrSystemSend(mxid, "- join : join public chat room") ezbrSystemSend(mxid, "- talk : open private conversation to contact") ezbrSystemSend(mxid, "- search : search for users by name") + ezbrSystemSend(mxid, "- cmd : send special command to account") case "list", "account", "accounts": one := false if accts, ok := registeredAccounts[mxid]; ok { @@ -332,6 +333,18 @@ func handleSystemMessage(mxid string, msg string) { } else { ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1]) } + case "cmd": + if len(cmd) < 3 { + ezbrSystemSendf(mxid, "Usage: %s ", 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: ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!") }