From ea518ba4c91e9587f827ef18fee9924e1e10a7e3 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 4 Jan 2020 16:23:35 +0100 Subject: [PATCH] Add initial MQTT helper --- fbchat/_mqtt.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ fbchat/_util.py | 12 ++++++ pyproject.toml | 1 + 3 files changed, 121 insertions(+) create mode 100644 fbchat/_mqtt.py diff --git a/fbchat/_mqtt.py b/fbchat/_mqtt.py new file mode 100644 index 0000000..f01d6d2 --- /dev/null +++ b/fbchat/_mqtt.py @@ -0,0 +1,108 @@ +import attr +import random +import paho.mqtt.client +from . import _util, _graphql + + +@attr.s(slots=True) +class Mqtt: + _state = attr.ib() + _mqtt = attr.ib() + + @classmethod + def connect(cls, state, foreground): + mqtt = paho.mqtt.client.Client( + client_id="mqttwsclient", + clean_session=True, + protocol=paho.mqtt.client.MQTTv31, + transport="websockets", + ) + mqtt.enable_logger() + + # Generate a random session ID between 1 and 9007199254740991 + session_id = random.randint(1, 2 ** 53) + + username = { + # The user ID + "u": state.user_id, + # Session ID + "s": session_id, + # Active status setting + "chat_on": True, + # foreground_state - Whether the window is focused + "fg": foreground, + # Can be any random ID + "d": state._client_id, + # Application ID, taken from facebook.com + "aid": 219994525426954, + # MQTT extension by FB, allows making a SUBSCRIBE while CONNECTing + "st": [ + # TODO: Investigate the result from these + # "/inbox", + # "/mercury", + # "/messaging_events", + # "/orca_message_notifications", + # "/pp", + # "/t_p", + # "/t_rtc", + # "/webrtc_response", + "/legacy_web", + "/webrtc", + "/onevc", + # Things that happen in chats (e.g. messages) + "/t_ms", + # Group typing notifications + "/thread_typing", + # Private chat typing notifications + "/orca_typing_notifications", + "/notify_disconnect", + # Active notifications + "/orca_presence", + "/br_sr", + "/sr_res", + ], + # MQTT extension by FB, allows making a PUBLISH while CONNECTing + "pm": [], + # Unknown parameters + "cp": 3, + "ecp": 10, + "ct": "websocket", + "mqtt_sid": "", + "dc": "", + "no_auto_fg": True, + "gas": None, + "pack": [], + } + mqtt.username_pw_set(_util.json_minimal(username)) + + headers = { + "Cookie": _util.get_cookie_header(state._session, "edge-chat.facebook.com"), + "User-Agent": state._session.headers["User-Agent"], + "Origin": "https://www.facebook.com", + "Host": "edge-chat.facebook.com", + } + + # TODO: Is region (lla | atn | odn | others?) important? + mqtt.ws_set_options(path="/chat?sid={}".format(session_id), headers=headers) + mqtt.tls_set() + response_code = mqtt.connect("edge-chat.facebook.com", 443, keepalive=10) + # TODO: Handle response code + + return cls(state=state, mqtt=mqtt) + + def listen(self, on_message): + def real_on_message(client, userdata, message): + on_message(message.topic, message.payload) + + self._mqtt.on_message = real_on_message + + self._mqtt.loop_forever() # TODO: retry_first_connection=True? + + def disconnect(self): + self._mqtt.disconnect() + + def set_foreground(self, state): + payload = _util.json_minimal({"foreground": state}) + info = self._mqtt.publish("/foreground_state", payload=payload, qos=1) + # TODO: We can't wait for this, since the loop is running with .loop_forever() + # info.wait_for_publish() diff --git a/fbchat/_util.py b/fbchat/_util.py index af06d42..6228989 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -57,6 +57,11 @@ def now(): return int(time() * 1000) +def json_minimal(data): + """Get JSON data in minimal form.""" + return json.dumps(data, separators=(",", ":")) + + def strip_json_cruft(text): """Removes `for(;;);` (and other cruft) that preceeds JSON responses.""" try: @@ -65,6 +70,13 @@ def strip_json_cruft(text): raise FBchatException("No JSON object found: {!r}".format(text)) +def get_cookie_header(session, host): + """Extract a cookie header from a requests session.""" + return requests.cookies.get_cookie_header( + session.cookies, requests.Request("GET", host), + ) + + def get_decoded_r(r): return get_decoded(r._content) diff --git a/pyproject.toml b/pyproject.toml index 136268d..29cbc28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ requires = [ "attrs>=18.2", "requests~=2.19", "beautifulsoup4~=4.0", + "paho-mqtt~=1.5", ] description-file = "README.rst" classifiers = [