2020-02-29 17:30:43 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import json
|
2020-02-29 19:47:44 +00:00
|
|
|
import signal
|
|
|
|
import threading
|
2020-03-01 12:08:32 +00:00
|
|
|
import queue
|
|
|
|
import pickle
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
import hashlib
|
2020-02-29 19:47:44 +00:00
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
import fbchat
|
2020-02-29 19:47:44 +00:00
|
|
|
from fbchat.models import *
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
# ---- 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"
|
|
|
|
|
2020-02-29 19:47:44 +00:00
|
|
|
# Event types
|
|
|
|
EVENT_JOIN = "join"
|
|
|
|
EVENT_LEAVE = "leave"
|
|
|
|
EVENT_MESSAGE = "message"
|
|
|
|
EVENT_ACTION = "action"
|
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
# ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ----
|
|
|
|
|
2020-02-29 19:47:44 +00:00
|
|
|
|
|
|
|
def mediaObjectOfURL(url):
|
|
|
|
return {
|
|
|
|
"filename": url.split("?")[0].split("/")[-1],
|
|
|
|
"url": url,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
# class MessengerBridgeClient(fbchat.Client):
|
|
|
|
# def __init__(self, bridge, *args, **kwargs):
|
|
|
|
# super(MessengerBridgeClient, self).__init__(*args, **kwargs)
|
|
|
|
#
|
|
|
|
# # TODO: handle events
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
|
2020-02-29 19:47:44 +00:00
|
|
|
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))
|
2020-03-01 12:08:32 +00:00
|
|
|
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))
|
2020-02-29 19:47:44 +00:00
|
|
|
self.bridge.write({
|
2020-03-01 12:08:32 +00:00
|
|
|
"_type": EVENT,
|
|
|
|
"data": {
|
|
|
|
"type": EVENT_JOIN,
|
|
|
|
"author": self.bridge.getUserId(member),
|
|
|
|
"room": thread.uid,
|
|
|
|
}
|
2020-02-29 19:47:44 +00:00
|
|
|
})
|
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
user_info = {
|
|
|
|
"display_name": member.name,
|
2020-02-29 19:47:44 +00:00
|
|
|
}
|
2020-03-01 12:08:32 +00:00
|
|
|
if member.photo is not None:
|
|
|
|
user_info["avatar"] = mediaObjectOfURL(member.photo)
|
2020-02-29 19:47:44 +00:00
|
|
|
self.bridge.write({
|
2020-03-01 12:08:32 +00:00
|
|
|
"_type": USER_INFO_UPDATED,
|
|
|
|
"user": self.bridge.getUserId(member),
|
|
|
|
"data": user_info,
|
2020-02-29 19:47:44 +00:00
|
|
|
})
|
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
def backlog_room(self, thread):
|
|
|
|
pass # TODO
|
2020-02-29 19:47:44 +00:00
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
# ---- MAIN LOOP THAT HANDLES REQUESTS FROM BRIDGE ----
|
|
|
|
|
|
|
|
class MessengerBridge:
|
2020-02-29 19:47:44 +00:00
|
|
|
def __init__(self):
|
2020-03-01 12:08:32 +00:00
|
|
|
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
|
2020-02-29 19:47:44 +00:00
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
def run(self):
|
|
|
|
self.client = None
|
|
|
|
self.keep_running = True
|
2020-03-01 12:08:32 +00:00
|
|
|
self.cache_gets = {}
|
|
|
|
self.num = 0
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
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:
|
2020-03-01 12:08:32 +00:00
|
|
|
client_file = "/tmp/fbclient_" + hashlib.sha224(cmd["data"]["email"].encode("utf-8")).hexdigest()
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
try:
|
2020-03-01 12:08:32 +00:00
|
|
|
f = open(client_file, "rb")
|
|
|
|
self.client = pickle.load(f)
|
2020-02-29 17:30:43 +00:00
|
|
|
f.close()
|
2020-03-01 12:08:32 +00:00
|
|
|
sys.stderr.write("(python messenger) using previous client: {}\n".format(client_file))
|
2020-02-29 17:30:43 +00:00
|
|
|
except:
|
2020-03-01 12:08:32 +00:00
|
|
|
self.client = None
|
2020-02-29 17:30:43 +00:00
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
if self.client is None:
|
|
|
|
email, password = cmd["data"]["email"], cmd["data"]["password"]
|
|
|
|
self.client = fbchat.Client(email=email, password=password, max_tries=1)
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
if self.client.isLoggedIn():
|
|
|
|
try:
|
2020-03-01 12:08:32 +00:00
|
|
|
f = open(client_file, "wb")
|
|
|
|
pickle.dump(self.client, f)
|
2020-02-29 17:30:43 +00:00
|
|
|
f.close()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2020-02-29 19:47:44 +00:00
|
|
|
InitialSyncThread(self.client, self).start()
|
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
elif ty == CLOSE:
|
|
|
|
self.keep_running = False
|
|
|
|
|
2020-02-29 19:47:44 +00:00
|
|
|
elif ty == GET_USER:
|
|
|
|
return {"_type": REP_OK, "user": self.client.uid}
|
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
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"])
|
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
else:
|
|
|
|
return {"_type": REP_ERROR, "error": "Not implemented"}
|
|
|
|
|
2020-03-01 12:08:32 +00:00
|
|
|
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
|
|
|
|
|
2020-02-29 17:30:43 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
bridge = MessengerBridge()
|
|
|
|
bridge.run()
|
|
|
|
|