#!/usr/bin/env python3 import sys import json import signal import threading import queue import pickle import hashlib import fbchat from fbchat.models import * # ---- MESSAGE TYPES ---- # ezbr -> external CONFIGURE = "configure" GET_USER = "get_user" SET_USER_INFO = "set_user_info" SET_ROOM_INFO = "set_room_info" JOIN = "join" INVITE = "invite" LEAVE = "leave" SEND = "send" CLOSE = "close" # external -> ezbr JOINED = "joined" LEFT = "left" USER_INFO_UPDATED = "user_info_updated" ROOM_INFO_UPDATED = "room_info_updated" EVENT = "event" CACHE_PUT = "cache_put" CACHE_GET = "cache_get" # reply messages # ezbr -> external: all must wait for a reply! # external -> ezbr: only CACHE_GET produces a reply REP_OK = "rep_ok" REP_ERROR = "rep_error" # Event types EVENT_JOIN = "join" EVENT_LEAVE = "leave" EVENT_MESSAGE = "message" EVENT_ACTION = "action" # ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ---- def mediaObjectOfURL(url): return { "filename": url.split("?")[0].split("/")[-1], "url": url, } # class MessengerBridgeClient(fbchat.Client): # def __init__(self, bridge, *args, **kwargs): # super(MessengerBridgeClient, self).__init__(*args, **kwargs) # # # TODO: handle events class InitialSyncThread(threading.Thread): def __init__(self, client, bridge, *args, **kwargs): super(InitialSyncThread, self).__init__(*args, **kwargs) self.client = client self.bridge = bridge def run(self): threads = self.client.fetchThreadList() sys.stderr.write("fb thread list: {}\n".format(threads)) for thread in threads: sys.stderr.write("fb thread: {}\n".format(thread)) if thread.type == ThreadType.GROUP: members = self.client.fetchAllUsersFromThreads([thread]) self.bridge.write({ "_type": JOINED, "room": thread.uid, }) self.send_room_info(thread, members) self.send_room_members(thread, members) elif thread.type == ThreadType.USER: self.bridge.getUserId(thread) self.backlog_room(thread) def send_room_info(self, thread, members): room_info = {} if thread.name is not None: room_info["name"] = thread.name else: who = [m for m in members if m.uid != self.client.uid] if len(who) > 3: room_info["name"] = ", ".join([self.bridge.getUserShortName(m) for m in who[:3]] + ["..."]) else: room_info["name"] = ", ".join([self.bridge.getUserShortName(m) for m in who]) if thread.photo is not None: room_info["picture"] = mediaObjectOfURL(thread.photo) else: for m in members: if m.uid != self.client.uid and m.photo is not None: room_info["picture"] = mediaObjectOfURL(m.photo) break self.bridge.write({ "_type": ROOM_INFO_UPDATED, "room": thread.uid, "data": room_info, }) def send_room_members(self, thread, members): for member in members: sys.stderr.write("fb thread member: {}\n".format(member)) self.bridge.write({ "_type": EVENT, "data": { "type": EVENT_JOIN, "author": self.bridge.getUserId(member), "room": thread.uid, } }) user_info = { "display_name": member.name, } if member.photo is not None: user_info["avatar"] = mediaObjectOfURL(member.photo) self.bridge.write({ "_type": USER_INFO_UPDATED, "user": self.bridge.getUserId(member), "data": user_info, }) def backlog_room(self, thread): pass # TODO # ---- MAIN LOOP THAT HANDLES REQUESTS FROM BRIDGE ---- class MessengerBridge: def __init__(self): self.rev_uid = {} def getUserId(self, user): if user.url is not None and not "?" in user.url: user_id = user.url.split("/")[-1] self.rev_uid[user_id] = user.uid return user_id else: return user.uid def revUserId(self, user_id): if user_id in self.rev_uid: return self.rev_uid[user_id] else: return user_id def getUserShortName(self, user): if user.first_name != None: return user.first_name else: return user.name def run(self): self.client = None self.keep_running = True self.cache_gets = {} self.num = 0 while self.keep_running: line = sys.stdin.readline() sys.stderr.write("(python) reading {}\n".format(line.strip())) cmd = json.loads(line) try: rep = self.handle_cmd(cmd) if rep is None: rep = {} if "_type" not in rep: rep["_type"] = REP_OK except Exception as e: rep = { "_type": REP_ERROR, "error": "{}".format(e) } rep["_id"] = cmd["_id"] self.write(rep) def write(self, msg): msgstr = json.dumps(msg) sys.stderr.write("(python) writing {}\n".format(msgstr)) sys.stdout.write(msgstr + "\n") sys.stdout.flush() def handle_cmd(self, cmd): ty = cmd["_type"] if ty == CONFIGURE: client_file = "/tmp/fbclient_" + hashlib.sha224(cmd["data"]["email"].encode("utf-8")).hexdigest() try: f = open(client_file, "rb") self.client = pickle.load(f) f.close() sys.stderr.write("(python messenger) using previous client: {}\n".format(client_file)) except: self.client = None if self.client is None: email, password = cmd["data"]["email"], cmd["data"]["password"] self.client = fbchat.Client(email=email, password=password, max_tries=1) if self.client.isLoggedIn(): try: f = open(client_file, "wb") pickle.dump(self.client, f) f.close() except: pass InitialSyncThread(self.client, self).start() elif ty == CLOSE: self.keep_running = False elif ty == GET_USER: return {"_type": REP_OK, "user": self.client.uid} elif ty == INVITE and cmd["room"] == "": return {"_type": REP_OK} elif ty == SEND: event = cmd["data"] if event["type"] in [EVENT_MESSAGE, EVENT_ACTION]: # TODO: attachments msg = Message(event["text"]) if event["type"] == EVENT_ACTION: msg.text = "* " + event["text"] if event["room"] != "": msg_id = self.client.send(msg, thread_id=event["room"], thread_type=ThreadType.GROUP) elif event["recipient"] != "": uid = self.revUserId(event["recipient"]) msg_id = self.client.send(msg, thread_id=uid, thread_type=ThreadType.USER) else: return {"_type": REP_ERROR, "error": "Invalid message"} return {"_type": REP_OK, "event_id": msg_id} elif ty == REP_OK and cmd["_id"] in self.cache_gets: self.cache_gets[cmd["_id"]].put(cmd["value"]) else: return {"_type": REP_ERROR, "error": "Not implemented"} def cache_get(self, key): self.num += 1 num = self.num q = queue.Queue(1) self.cache_gets[num] = q self.write({"_type": CACHE_GET, "_id": num, "key": key}) rep = q.get(block=True, timeout=30) del self.cache_gets[num] return rep if __name__ == "__main__": bridge = MessengerBridge() bridge.run()