From 1460b2f42137d14267a5de8b4b2a529b7b65b68d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 16:33:44 +0100 Subject: [PATCH 01/12] Version up, thanks to @oneblue and @darylkell --- fbchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 91bacb4..59bb975 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -11,7 +11,7 @@ from .models import * from .client import * __title__ = "fbchat" -__version__ = "1.6.3" +__version__ = "1.6.4" __description__ = "Facebook Chat (Messenger) for Python" __copyright__ = "Copyright 2015 - 2019 by Taehoon Kim" From 4f0f126e4837120795d3a68146af686815b51050 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 10 Mar 2019 16:45:16 +0100 Subject: [PATCH 02/12] Make Github Releases deploy in the published state --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4a15895..3a53852 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ jobs: file_glob: true file: dist/* skip_cleanup: true - draft: true + draft: false on: tags: true From d7a5d0043945972fa05af8777d48568e66b4386b Mon Sep 17 00:00:00 2001 From: LukasOndrejka Date: Tue, 12 Mar 2019 18:15:11 +0100 Subject: [PATCH 03/12] Add new colors (#393) Color names are from https://www.htmlcsscolor.com/ --- fbchat/_thread.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 6b4641e..c01673f 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -41,6 +41,11 @@ class ThreadColor(Enum): CAMEO = "#d4a88c" BRILLIANT_ROSE = "#ff5ca1" BILOBA_FLOWER = "#a695c7" + TICKLE_ME_PINK = "#ff7ca8" + MALACHITE = "#1adb5b" + RUBY = "#f01d6a" + DARK_TANGERINE = "#ff9c19" + BRIGHT_TURQUOISE = "#0edcde" @attr.s(cmp=False, init=False) From a3efa7702a0b2f54722b78032f78bfa4c05ea610 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 23 Mar 2019 21:26:43 +0100 Subject: [PATCH 04/12] Add possibility to reply to messages and to (partly) fetch the replied messages --- fbchat/_client.py | 17 ++++++++++------- fbchat/_graphql.py | 2 ++ fbchat/_message.py | 4 ++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index a4a363d..3cd0c3b 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -390,9 +390,9 @@ class Client(object): if "home" in r.url: return r - del (data["approvals_code"]) - del (data["submit[Submit Code]"]) - del (data["codes_submitted"]) + del data["approvals_code"] + del data["submit[Submit Code]"] + del data["codes_submitted"] data["name_action_selected"] = "save_device" data["submit[Continue]"] = "Continue" @@ -404,7 +404,7 @@ class Client(object): if "home" in r.url: return r - del (data["name_action_selected"]) + del data["name_action_selected"] log.info( "Starting Facebook checkup flow." ) # At this stage, we have dtsg, nh, submit[Continue] @@ -413,7 +413,7 @@ class Client(object): if "home" in r.url: return r - del (data["submit[Continue]"]) + del data["submit[Continue]"] data["submit[This was me]"] = "This Was Me" log.info( "Verifying login attempt." @@ -423,7 +423,7 @@ class Client(object): if "home" in r.url: return r - del (data["submit[This was me]"]) + del data["submit[This was me]"] data["submit[Continue]"] = "Continue" data["name_action_selected"] = "save_device" log.info( @@ -1084,7 +1084,7 @@ class Client(object): j = self.graphql_request( GraphQL( - doc_id="1386147188135407", + doc_id="1860982147341344", params={ "id": thread_id, "message_limit": limit, @@ -1379,6 +1379,9 @@ class Client(object): xmd["quick_replies"] = xmd["quick_replies"][0] data["platform_xmd"] = json.dumps(xmd) + if message.reply_to_id: + data["replied_to_message_id"] = message.reply_to_id + return data def _doSendRequest(self, data, get_thread_id=False): diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 4bf8cab..ea8c56a 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -401,6 +401,8 @@ def graphql_to_message(message): rtn.unsent = True elif attachment: rtn.attachments.append(attachment) + if message.get("replied_to_message") is not None: + rtn.replied_to = graphql_to_message(message["replied_to_message"]["message"]) return rtn diff --git a/fbchat/_message.py b/fbchat/_message.py index 3f6f03d..3203778 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -68,6 +68,10 @@ class Message(object): quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x) #: Whether the message is unsent (deleted for everyone) unsent = attr.ib(False, init=False) + #: Message ID you want to reply to + reply_to_id = attr.ib(None) + #: Replied message + replied_to = attr.ib(None, init=False) @classmethod def formatMentions(cls, text, *args, **kwargs): From a4ce45e9b0ff5788ec7160c5b1c76dcd7f969029 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Fri, 29 Mar 2019 21:09:19 +0100 Subject: [PATCH 05/12] Add detecting replied messages while listening --- fbchat/_client.py | 18 ++++++++++++++++++ fbchat/_graphql.py | 42 ++++++++++++++++++++++++++++++++++++++++++ fbchat/graphql.py | 1 + 3 files changed, 61 insertions(+) diff --git a/fbchat/_client.py b/fbchat/_client.py index 3cd0c3b..8e0c8df 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3040,6 +3040,24 @@ class Client(object): msg=m, ) + elif d.get("deltaMessageReply"): + i = d["deltaMessageReply"] + metadata = i["message"]["messageMetadata"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + message = graphql_to_message_reply(i["message"]) + message.replied_to = graphql_to_message_reply(i["repliedToMessage"]) + self.onMessage( + mid=message.uid, + author_id=message.author, + message=message.text, + message_object=message, + thread_id=thread_id, + thread_type=thread_type, + ts=message.timestamp, + metadata=metadata, + msg=m, + ) + # New message elif delta.get("class") == "NewMessage": mentions = [] diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index ea8c56a..3c1bac8 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -406,6 +406,48 @@ def graphql_to_message(message): return rtn +def graphql_to_message_reply(message): + rtn = Message( + text=message.get("body"), + mentions=[ + Mention(m.get("i"), offset=m.get("o"), length=m.get("l")) + for m in json.loads(message.get("data", {}).get("prng", "[]")) + ], + emoji_size=get_emojisize_from_tags(message["messageMetadata"].get("tags")), + ) + metadata = message.get("messageMetadata", {}) + rtn.uid = metadata.get("messageId") + rtn.author = str(metadata.get("actorFbId")) + rtn.timestamp = metadata.get("timestamp") + rtn.unsent = False + if message.get("data", {}).get("platform_xmd"): + quick_replies = json.loads(message["data"]["platform_xmd"]).get("quick_replies") + if isinstance(quick_replies, list): + rtn.quick_replies = [graphql_to_quick_reply(q) for q in quick_replies] + elif isinstance(quick_replies, dict): + rtn.quick_replies = [ + graphql_to_quick_reply(quick_replies, is_response=True) + ] + if message.get("attachments") is not None: + for attachment in message["attachments"]: + attachment = json.loads(attachment["mercuryJSON"]) + if attachment.get("blob_attachment"): + rtn.attachments.append( + graphql_to_attachment(attachment["blob_attachment"]) + ) + if attachment.get("extensible_attachment"): + extensible_attachment = graphql_to_extensible_attachment( + attachment["extensible_attachment"] + ) + if isinstance(extensible_attachment, UnsentMessage): + rtn.unsent = True + else: + rtn.attachments.append(extensible_attachment) + if attachment.get("sticker_attachment"): + rtn.sticker = graphql_to_sticker(attachment["sticker_attachment"]) + return rtn + + def graphql_to_user(user): if user.get("profile_picture") is None: user["profile_picture"] = {} diff --git a/fbchat/graphql.py b/fbchat/graphql.py index aeef27c..251e036 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -20,6 +20,7 @@ from ._graphql import ( graphql_to_plan, graphql_to_quick_reply, graphql_to_message, + graphql_to_message_reply, graphql_to_user, graphql_to_thread, graphql_to_group, From 070f57fcc4dc1445508c8fbabf03de63812e4d2b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sat, 30 Mar 2019 21:20:20 +0100 Subject: [PATCH 06/12] Refactor `_graphql` away --- fbchat/_client.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index a4a363d..66581c9 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -166,6 +166,7 @@ class Client(object): timeout=30, fix_request=False, as_json=False, + as_graphql=False, error_retries=3, ): payload = self._generatePayload(query) @@ -179,7 +180,11 @@ class Client(object): if not fix_request: return r try: - return check_request(r, as_json=as_json) + if as_graphql: + content = check_request(r, as_json=False) + return graphql_response_to_json(content) + else: + return check_request(r, as_json=as_json) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): return self._post( @@ -188,21 +193,11 @@ class Client(object): timeout=timeout, fix_request=fix_request, as_json=as_json, + as_graphql=as_graphql, error_retries=error_retries - 1, ) raise e - def _graphql(self, payload, error_retries=3): - content = self._post( - self.req_url.GRAPHQL, payload, fix_request=True, as_json=False - ) - try: - return graphql_response_to_json(content) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): - return self._graphql(payload, error_retries=error_retries - 1) - raise e - def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True): return self._session.get( url, @@ -274,12 +269,13 @@ class Client(object): """ return tuple( - self._graphql( + self._post( { "method": "GET", "response_format": "json", "queries": graphql_queries_to_json(*queries), - } + }, + as_graphql=True, ) ) From 24e238c425f747d43591324eafcd64c4c4eedf69 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Mar 2019 00:02:49 +0100 Subject: [PATCH 07/12] Remove superfluous whitespace --- fbchat/_client.py | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 66581c9..351062c 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -60,7 +60,6 @@ class Client(object): :type logging_level: int :raises: FBchatException on failed login """ - self.sticky, self.pool = (None, None) self._session = requests.session() self.req_counter = 1 @@ -267,7 +266,6 @@ class Client(object): :return: A tuple containing json graphql queries :rtype: tuple """ - return tuple( self._post( { @@ -455,7 +453,6 @@ class Client(object): :return: False if `session_cookies` does not contain proper cookies :rtype: bool """ - # Quick check to see if session_cookies is formatted properly if not session_cookies or "c_user" not in session_cookies: return False @@ -521,7 +518,6 @@ class Client(object): :return: True if the action was successful :rtype: bool """ - if not hasattr(self, "fb_h"): h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {"pmid": "4"}) self.fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1) @@ -681,7 +677,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - data = {"viewer": self.uid} j = self._post( self.req_url.ALL_USERS, query=data, fix_request=True, as_json=True @@ -708,7 +703,6 @@ class Client(object): gender=GENDERS.get(k.get("gender")), ) ) - return users def searchForUsers(self, name, limit=10): @@ -721,7 +715,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( GraphQL(query=GraphQL.SEARCH_USER, params={"search": name, "limit": limit}) ) @@ -737,7 +730,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( GraphQL(query=GraphQL.SEARCH_PAGE, params={"search": name, "limit": limit}) ) @@ -754,7 +746,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( GraphQL(query=GraphQL.SEARCH_GROUP, params={"search": name, "limit": limit}) ) @@ -771,7 +762,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( GraphQL( query=GraphQL.SEARCH_THREAD, params={"search": name, "limit": limit} @@ -872,7 +862,6 @@ class Client(object): j = self._post( self.req_url.SEARCH_MESSAGES, data, fix_request=True, as_json=True ) - result = j["payload"]["search_snippets"][query] if fetch_messages: @@ -939,7 +928,6 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - threads = self.fetchThreadInfo(*user_ids) users = {} for k in threads: @@ -962,7 +950,6 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - threads = self.fetchThreadInfo(*page_ids) pages = {} for k in threads: @@ -982,7 +969,6 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - threads = self.fetchThreadInfo(*group_ids) groups = {} for k in threads: @@ -1005,7 +991,6 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - queries = [] for thread_id in thread_ids: queries.append( @@ -1075,7 +1060,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - thread_id, thread_type = self._getThread(thread_id, None) j = self.graphql_request( @@ -1126,7 +1110,6 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - if offset is not None: log.warning( "Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead" @@ -1171,13 +1154,11 @@ class Client(object): "last_action_timestamp": now() - 60 * 1000 # 'last_action_timestamp': 0 } - j = self._post( self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True ) payload = j["payload"]["unread_thread_fbids"][0] - return payload["thread_fbids"] + payload["other_user_fbids"] def fetchUnseen(self): @@ -1193,7 +1174,6 @@ class Client(object): ) payload = j["payload"]["unseen_thread_fbids"][0] - return payload["thread_fbids"] + payload["other_user_fbids"] def fetchImageUrl(self, image_id): @@ -1239,11 +1219,9 @@ class Client(object): :raises: FBchatException if request failed """ data = {"question_id": poll_id} - j = self._post( self.req_url.GET_POLL_OPTIONS, data, fix_request=True, as_json=True ) - return [graphql_to_poll_option(m) for m in j["payload"]] def fetchPlanInfo(self, plan_id): @@ -1421,7 +1399,6 @@ class Client(object): data = self._getSendData( message=message, thread_id=thread_id, thread_type=thread_type ) - return self._doSendRequest(data) def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): @@ -1815,11 +1792,9 @@ class Client(object): :param thread_id: Group ID to remove people from. See :ref:`intro_threads` :raises: FBchatException if request failed """ - thread_id, thread_type = self._getThread(thread_id, None) data = {"uid": user_id, "tid": thread_id} - j = self._post(self.req_url.REMOVE_USER, data, fix_request=True, as_json=True) def _adminStatus(self, admin_ids, admin, thread_id=None): @@ -1865,7 +1840,6 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} - j = self._post(self.req_url.APPROVAL_MODE, data, fix_request=True, as_json=True) def _usersApproval(self, user_ids, approve, thread_id=None): @@ -1917,7 +1891,6 @@ class Client(object): :param thread_id: User/Group ID to change image. See :ref:`intro_threads` :raises: FBchatException if request failed """ - thread_id, thread_type = self._getThread(thread_id, None) data = {"thread_image_id": image_id, "thread_id": thread_id} @@ -1933,7 +1906,6 @@ class Client(object): :param thread_id: User/Group ID to change image. See :ref:`intro_threads` :raises: FBchatException if request failed """ - (image_id, mimetype), = self._upload(get_files_from_urls([image_url])) return self._changeGroupImage(image_id, thread_id) @@ -1945,7 +1917,6 @@ class Client(object): :param thread_id: User/Group ID to change image. See :ref:`intro_threads` :raises: FBchatException if request failed """ - with get_files_from_paths([image_path]) as files: (image_id, mimetype), = self._upload(files) @@ -1962,7 +1933,6 @@ class Client(object): :type thread_type: models.ThreadType :raises: FBchatException if request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) if thread_type == ThreadType.USER: @@ -1972,7 +1942,6 @@ class Client(object): ) data = {"thread_name": title, "thread_id": thread_id} - j = self._post(self.req_url.THREAD_NAME, data, fix_request=True, as_json=True) def changeNickname( @@ -1995,7 +1964,6 @@ class Client(object): "participant_id": user_id, "thread_or_other_fbid": thread_id, } - j = self._post( self.req_url.THREAD_NICKNAME, data, fix_request=True, as_json=True ) @@ -2015,7 +1983,6 @@ class Client(object): "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", "thread_or_other_fbid": thread_id, } - j = self._post(self.req_url.THREAD_COLOR, data, fix_request=True, as_json=True) def changeThreadEmoji(self, emoji, thread_id=None): @@ -2031,7 +1998,6 @@ class Client(object): thread_id, thread_type = self._getThread(thread_id, None) data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} - j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) def reactToMessage(self, message_id, reaction): @@ -2110,7 +2076,6 @@ class Client(object): ] }, } - j = self._post( self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True ) @@ -2131,7 +2096,6 @@ class Client(object): ] }, } - j = self._post( self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True ) @@ -2153,7 +2117,6 @@ class Client(object): ] }, } - j = self._post( self.req_url.PLAN_PARTICIPATION, full_data, fix_request=True, as_json=True ) @@ -2236,7 +2199,6 @@ class Client(object): "to": thread_id if thread_type == ThreadType.USER else "", "source": "mercury-chat", } - j = self._post(self.req_url.TYPING, data, fix_request=True, as_json=True) """ @@ -2508,7 +2470,6 @@ class Client(object): def _pullMessage(self): """Call pull api with seq value to get message data.""" - data = { "msgs_recv": 0, "sticky_token": self.sticky, @@ -2516,9 +2477,7 @@ class Client(object): "clientid": self.client_id, "state": "active" if self._markAlive else "offline", } - j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) - self.seq = j.get("seq", "0") return j From 6302d5fb8bfd97cb9c7f0c9f05d5225794dfcfa1 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Mar 2019 00:07:29 +0100 Subject: [PATCH 08/12] Split overly nested calls --- fbchat/_client.py | 232 +++++++++++++++++----------------------------- 1 file changed, 84 insertions(+), 148 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 351062c..58d7b13 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -20,6 +20,13 @@ except ImportError: from urlparse import urlparse, parse_qs +_ACONTEXT = { + "action_history": [ + {"surface": "messenger_chat_tab", "mechanism": "messenger_composer"} + ] +} + + class Client(object): """A client for the Facebook Chat (Messenger). @@ -266,16 +273,12 @@ class Client(object): :return: A tuple containing json graphql queries :rtype: tuple """ - return tuple( - self._post( - { - "method": "GET", - "response_format": "json", - "queries": graphql_queries_to_json(*queries), - }, - as_graphql=True, - ) - ) + data = { + "method": "GET", + "response_format": "json", + "queries": graphql_queries_to_json(*queries), + } + return tuple(self._post(data, as_graphql=True)) def graphql_request(self, query): """ @@ -578,15 +581,8 @@ class Client(object): """ def _forcedFetch(self, thread_id, mid): - j = self.graphql_request( - GraphQL( - doc_id="1768656253222505", - params={ - "thread_and_message_id": {"thread_id": thread_id, "message_id": mid} - }, - ) - ) - return j + params = {"thread_and_message_id": {"thread_id": thread_id, "message_id": mid}} + return self.graphql_request(GraphQL(doc_id="1768656253222505", params=params)) def fetchThreads(self, thread_location, before=None, after=None, limit=None): """ @@ -715,9 +711,8 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( - GraphQL(query=GraphQL.SEARCH_USER, params={"search": name, "limit": limit}) - ) + params = {"search": name, "limit": limit} + j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_USER, params=params)) return [graphql_to_user(node) for node in j[name]["users"]["nodes"]] @@ -730,9 +725,8 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( - GraphQL(query=GraphQL.SEARCH_PAGE, params={"search": name, "limit": limit}) - ) + params = {"search": name, "limit": limit} + j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_PAGE, params=params)) return [graphql_to_page(node) for node in j[name]["pages"]["nodes"]] @@ -746,9 +740,8 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( - GraphQL(query=GraphQL.SEARCH_GROUP, params={"search": name, "limit": limit}) - ) + params = {"search": name, "limit": limit} + j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_GROUP, params=params)) return [graphql_to_group(node) for node in j["viewer"]["groups"]["nodes"]] @@ -762,11 +755,8 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - j = self.graphql_request( - GraphQL( - query=GraphQL.SEARCH_THREAD, params={"search": name, "limit": limit} - ) - ) + params = {"search": name, "limit": limit} + j = self.graphql_request(GraphQL(query=GraphQL.SEARCH_THREAD, params=params)) rtn = [] for node in j[name]["threads"]["nodes"]: @@ -993,18 +983,14 @@ class Client(object): """ queries = [] for thread_id in thread_ids: - queries.append( - GraphQL( - doc_id="2147762685294928", - params={ - "id": thread_id, - "message_limit": 0, - "load_messages": False, - "load_read_receipts": False, - "before": None, - }, - ) - ) + params = { + "id": thread_id, + "message_limit": 0, + "load_messages": False, + "load_read_receipts": False, + "before": None, + } + queries.append(GraphQL(doc_id="2147762685294928", params=params)) j = self.graphql_requests(*queries) @@ -1062,30 +1048,24 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) - j = self.graphql_request( - GraphQL( - doc_id="1386147188135407", - params={ - "id": thread_id, - "message_limit": limit, - "load_messages": True, - "load_read_receipts": True, - "before": before, - }, - ) - ) + params = { + "id": thread_id, + "message_limit": limit, + "load_messages": True, + "load_read_receipts": True, + "before": before, + } + j = self.graphql_request(GraphQL(doc_id="1386147188135407", params=params)) if j.get("message_thread") is None: raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j)) - messages = list( - reversed( - [ - graphql_to_message(message) - for message in j["message_thread"]["messages"]["nodes"] - ] - ) - ) + messages = [ + graphql_to_message(message) + for message in j["message_thread"]["messages"]["nodes"] + ] + messages.reverse() + read_receipts = j["message_thread"]["read_receipts"]["nodes"] for message in messages: @@ -1123,18 +1103,14 @@ class Client(object): else: raise FBchatUserError('"thread_location" must be a value of ThreadLocation') - j = self.graphql_request( - GraphQL( - doc_id="1349387578499440", - params={ - "limit": limit, - "tags": [loc_str], - "before": before, - "includeDeliveryReceipts": True, - "includeSeqID": False, - }, - ) - ) + params = { + "limit": limit, + "tags": [loc_str], + "before": before, + "includeDeliveryReceipts": True, + "includeSeqID": False, + } + j = self.graphql_request(GraphQL(doc_id="1349387578499440", params=params)) return [ graphql_to_thread(node) for node in j["viewer"]["message_threads"]["nodes"] @@ -1186,8 +1162,9 @@ class Client(object): :raises: FBchatException if request failed """ image_id = str(image_id) - j = check_request( - self._get(ReqUrl.ATTACHMENT_PHOTO, query={"photo_id": str(image_id)}) + data = {"photo_id": str(image_id)} + j = self._get( + ReqUrl.ATTACHMENT_PHOTO, query=data, fix_request=True, as_json=True ) url = get_jsmods_require(j, 3) @@ -1847,20 +1824,16 @@ class Client(object): user_ids = list(require_list(user_ids)) + data = { + "client_mutation_id": "0", + "actor_id": self.uid, + "thread_fbid": thread_id, + "user_ids": user_ids, + "response": "ACCEPT" if approve else "DENY", + "surface": "ADMIN_MODEL_APPROVAL_CENTER", + } j = self.graphql_request( - GraphQL( - doc_id="1574519202665847", - params={ - "data": { - "client_mutation_id": "0", - "actor_id": self.uid, - "thread_fbid": thread_id, - "user_ids": user_ids, - "response": "ACCEPT" if approve else "DENY", - "surface": "ADMIN_MODEL_APPROVAL_CENTER", - } - }, - ) + GraphQL(doc_id="1574519202665847", params={"data": data}) ) def acceptUsersToGroup(self, user_ids, thread_id=None): @@ -2010,19 +1983,13 @@ class Client(object): :raises: FBchatException if request failed """ data = { - "doc_id": 1491398900900362, - "variables": json.dumps( - { - "data": { - "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", - "client_mutation_id": "1", - "actor_id": self.uid, - "message_id": str(message_id), - "reaction": reaction.value if reaction else None, - } - } - ), + "action": "ADD_REACTION" if reaction else "REMOVE_REACTION", + "client_mutation_id": "1", + "actor_id": self.uid, + "message_id": str(message_id), + "reaction": reaction.value if reaction else None, } + data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})} self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) def createPlan(self, plan, thread_id=None): @@ -2036,23 +2003,16 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) - full_data = { + data = { "event_type": "EVENT", "event_time": plan.time, "title": plan.title, "thread_id": thread_id, "location_id": plan.location_id or "", "location_name": plan.location or "", - "acontext": { - "action_history": [ - {"surface": "messenger_chat_tab", "mechanism": "messenger_composer"} - ] - }, + "acontext": _ACONTEXT, } - - j = self._post( - self.req_url.PLAN_CREATE, full_data, fix_request=True, as_json=True - ) + j = self._post(self.req_url.PLAN_CREATE, data, fix_request=True, as_json=True) def editPlan(self, plan, new_plan): """ @@ -2063,22 +2023,16 @@ class Client(object): :type plan: models.Plan :raises: FBchatException if request failed """ - full_data = { + data = { "event_reminder_id": plan.uid, "delete": "false", "date": new_plan.time, "location_name": new_plan.location or "", "location_id": new_plan.location_id or "", "title": new_plan.title, - "acontext": { - "action_history": [ - {"surface": "messenger_chat_tab", "mechanism": "reminder_banner"} - ] - }, + "acontext": _ACONTEXT, } - j = self._post( - self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True - ) + j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) def deletePlan(self, plan): """ @@ -2087,18 +2041,8 @@ class Client(object): :param plan: Plan to delete :raises: FBchatException if request failed """ - full_data = { - "event_reminder_id": plan.uid, - "delete": "true", - "acontext": { - "action_history": [ - {"surface": "messenger_chat_tab", "mechanism": "reminder_banner"} - ] - }, - } - j = self._post( - self.req_url.PLAN_CHANGE, full_data, fix_request=True, as_json=True - ) + data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": _ACONTEXT} + j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) def changePlanParticipation(self, plan, take_part=True): """ @@ -2108,29 +2052,21 @@ class Client(object): :param take_part: Whether to take part in the plan :raises: FBchatException if request failed """ - full_data = { + data = { "event_reminder_id": plan.uid, "guest_state": "GOING" if take_part else "DECLINED", - "acontext": { - "action_history": [ - {"surface": "messenger_chat_tab", "mechanism": "reminder_banner"} - ] - }, + "acontext": _ACONTEXT, } j = self._post( - self.req_url.PLAN_PARTICIPATION, full_data, fix_request=True, as_json=True + self.req_url.PLAN_PARTICIPATION, data, fix_request=True, as_json=True ) def eventReminder(self, thread_id, time, title, location="", location_id=""): """ Deprecated. Use :func:`fbchat.Client.createPlan` instead """ - self.createPlan( - plan=Plan( - time=time, title=title, location=location, location_id=location_id - ), - thread_id=thread_id, - ) + plan = Plan(time=time, title=title, location=location, location_id=location_id) + self.createPlan(plan=plan, thread_id=thread_id) def createPoll(self, poll, thread_id=None): """ From bc27f756ed13564c326697043357885a2704d986 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Mar 2019 00:11:20 +0100 Subject: [PATCH 09/12] Split long strings, use format when creating strings --- fbchat/_client.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 58d7b13..550067e 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -311,7 +311,7 @@ class Client(object): if self.uid is None: raise FBchatException("Could not find c_user cookie") self.uid = str(self.uid) - self.user_channel = "p_" + self.uid + self.user_channel = "p_{}".format(self.uid) self.ttstamp = "" r = self._get(self.req_url.BASE) @@ -508,9 +508,8 @@ class Client(object): break else: raise FBchatUserError( - "Login failed. Check email/password. (Failed on url: {})".format( - login_url - ) + "Login failed. Check email/password. " + "(Failed on url: {})".format(login_url) ) def logout(self): @@ -659,8 +658,6 @@ class Client(object): and user_id not in users_to_fetch ): users_to_fetch.append(user_id) - else: - pass for user_id, user in self.fetchUserInfo(*users_to_fetch).items(): users.append(user) return users @@ -772,9 +769,7 @@ class Client(object): pass else: log.warning( - "Unknown __typename: {} in {}".format( - repr(node["__typename"]), node - ) + "Unknown type {} in {}".format(repr(node["__typename"]), node) ) return rtn @@ -1092,7 +1087,9 @@ class Client(object): """ if offset is not None: log.warning( - "Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead" + "Using `offset` in `fetchThreadList` is no longer supported, " + "since Facebook migrated to the use of GraphQL in this request. " + "Use `before` instead." ) if limit > 20 or limit < 1: @@ -1184,8 +1181,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) message_info = self._forcedFetch(thread_id, mid).get("message") - message = graphql_to_message(message_info) - return message + return graphql_to_message(message_info) def fetchPollOptions(self, poll_id): """ @@ -1212,8 +1208,7 @@ class Client(object): """ data = {"event_reminder_id": plan_id} j = self._post(self.req_url.PLAN_INFO, data, fix_request=True, as_json=True) - plan = graphql_to_plan(j["payload"]) - return plan + return graphql_to_plan(j["payload"]) def _getPrivateData(self): j = self.graphql_request(GraphQL(doc_id="1868889766468115")) @@ -1272,7 +1267,7 @@ class Client(object): timestamp = now() data = { "client": self.client, - "author": "fbid:" + str(self.uid), + "author": "fbid:{}".format(self.uid), "timestamp": timestamp, "source": "source:chat:web", "offline_threading_id": messageAndOTID, @@ -1355,9 +1350,8 @@ class Client(object): return message_ids[0][0] except (KeyError, IndexError, TypeError) as e: raise FBchatException( - "Error when sending message: No message IDs could be found: {}".format( - j - ) + "Error when sending message: " + "No message IDs could be found: {}".format(j) ) def send(self, message, thread_id=None, thread_type=ThreadType.USER): @@ -1756,8 +1750,8 @@ class Client(object): ) else: data[ - "log_message_data[added_participants][" + str(i) + "]" - ] = "fbid:" + str(user_id) + "log_message_data[added_participants][{}]".format(i) + ] = "fbid:{}".format(user_id) return self._doSendRequest(data) @@ -1782,7 +1776,7 @@ class Client(object): admin_ids = require_list(admin_ids) for i, admin_id in enumerate(admin_ids): - data["admin_ids[" + str(i) + "]"] = str(admin_id) + data["admin_ids[{}]".format(i)] = str(admin_id) j = self._post(self.req_url.SAVE_ADMINS, data, fix_request=True, as_json=True) From 1eeae78a9f187945b1483a1aed079a07009cab11 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 31 Mar 2019 00:16:36 +0100 Subject: [PATCH 10/12] Small refactoring The `muteX` methods return values are now checked using `check_request`, `seq` is now parsed in `_parseMessage` and a few other things. --- fbchat/_client.py | 77 ++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 550067e..9d5c7ad 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -850,19 +850,14 @@ class Client(object): result = j["payload"]["search_snippets"][query] if fetch_messages: - return { - thread_id: self.searchForMessages( - query, limit=message_limit, thread_id=thread_id - ) - for thread_id in result - } + search_method = self.searchForMessages else: - return { - thread_id: self.searchForMessageIDs( - query, limit=message_limit, thread_id=thread_id - ) - for thread_id in result - } + search_method = self.searchForMessageIDs + + return { + thread_id: search_method(query, limit=message_limit, thread_id=thread_id) + for thread_id in result + } def _fetchInfo(self, *ids): data = {"ids[{}]".format(i): _id for i, _id in enumerate(ids)} @@ -915,11 +910,11 @@ class Client(object): """ threads = self.fetchThreadInfo(*user_ids) users = {} - for k in threads: - if threads[k].type == ThreadType.USER: - users[k] = threads[k] + for id_, thread in threads.items(): + if thread.type == ThreadType.USER: + users[id_] = thread else: - raise FBchatUserError("Thread {} was not a user".format(threads[k])) + raise FBchatUserError("Thread {} was not a user".format(thread)) return users @@ -937,11 +932,11 @@ class Client(object): """ threads = self.fetchThreadInfo(*page_ids) pages = {} - for k in threads: - if threads[k].type == ThreadType.PAGE: - pages[k] = threads[k] + for id_, thread in threads.items(): + if thread.type == ThreadType.PAGE: + pages[id_] = thread else: - raise FBchatUserError("Thread {} was not a page".format(threads[k])) + raise FBchatUserError("Thread {} was not a page".format(thread)) return pages @@ -956,11 +951,11 @@ class Client(object): """ threads = self.fetchThreadInfo(*group_ids) groups = {} - for k in threads: - if threads[k].type == ThreadType.GROUP: - groups[k] = threads[k] + for id_, thread in threads.items(): + if thread.type == ThreadType.GROUP: + groups[id_] = thread else: - raise FBchatUserError("Thread {} was not a group".format(threads[k])) + raise FBchatUserError("Thread {} was not a group".format(thread)) return groups @@ -1662,19 +1657,15 @@ class Client(object): Deprecated. Use :func:`fbchat.Client._sendFiles` instead """ if is_gif: - return self._sendFiles( - files=[(image_id, "image/png")], - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) + mimetype = "image/gif" else: - return self._sendFiles( - files=[(image_id, "image/gif")], - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) + mimetype = "image/png" + return self._sendFiles( + files=[(image_id, mimetype)], + message=message, + thread_id=thread_id, + thread_type=thread_type, + ) def sendRemoteImage( self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER @@ -2329,8 +2320,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"mute_settings": str(mute_time), "thread_fbid": thread_id} - r = self._post(self.req_url.MUTE_THREAD, data) - r.raise_for_status() + content = self._post(self.req_url.MUTE_THREAD, data, fix_request=True) def unmuteThread(self, thread_id=None): """ @@ -2349,8 +2339,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} - r = self._post(self.req_url.MUTE_REACTIONS, data) - r.raise_for_status() + r = self._post(self.req_url.MUTE_REACTIONS, data, fix_request=True) def unmuteThreadReactions(self, thread_id=None): """ @@ -2369,8 +2358,7 @@ class Client(object): """ thread_id, thread_type = self._getThread(thread_id, None) data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} - r = self._post(self.req_url.MUTE_MENTIONS, data) - r.raise_for_status() + r = self._post(self.req_url.MUTE_MENTIONS, data, fix_request=True) def unmuteThreadMentions(self, thread_id=None): """ @@ -2407,9 +2395,7 @@ class Client(object): "clientid": self.client_id, "state": "active" if self._markAlive else "offline", } - j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) - self.seq = j.get("seq", "0") - return j + return self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) def _parseDelta(self, m): def getThreadIdAndThreadType(msg_metadata): @@ -3014,6 +3000,7 @@ class Client(object): def _parseMessage(self, content): """Get message and author name from content. May contain multiple messages in the content.""" + self.seq = j.get("seq", "0") if "lb_info" in content: self.sticky = content["lb_info"]["sticky"] From 48e7203ca6121e3b425531b2d2566efca3e08e8e Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 17 Apr 2019 20:47:49 +0200 Subject: [PATCH 11/12] Rename internal variable --- fbchat/_client.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 9d5c7ad..d9ec1e6 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -20,7 +20,7 @@ except ImportError: from urlparse import urlparse, parse_qs -_ACONTEXT = { +ACONTEXT = { "action_history": [ {"surface": "messenger_chat_tab", "mechanism": "messenger_composer"} ] @@ -1995,7 +1995,7 @@ class Client(object): "thread_id": thread_id, "location_id": plan.location_id or "", "location_name": plan.location or "", - "acontext": _ACONTEXT, + "acontext": ACONTEXT, } j = self._post(self.req_url.PLAN_CREATE, data, fix_request=True, as_json=True) @@ -2015,7 +2015,7 @@ class Client(object): "location_name": new_plan.location or "", "location_id": new_plan.location_id or "", "title": new_plan.title, - "acontext": _ACONTEXT, + "acontext": ACONTEXT, } j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) @@ -2026,7 +2026,7 @@ class Client(object): :param plan: Plan to delete :raises: FBchatException if request failed """ - data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": _ACONTEXT} + data = {"event_reminder_id": plan.uid, "delete": "true", "acontext": ACONTEXT} j = self._post(self.req_url.PLAN_CHANGE, data, fix_request=True, as_json=True) def changePlanParticipation(self, plan, take_part=True): @@ -2040,7 +2040,7 @@ class Client(object): data = { "event_reminder_id": plan.uid, "guest_state": "GOING" if take_part else "DECLINED", - "acontext": _ACONTEXT, + "acontext": ACONTEXT, } j = self._post( self.req_url.PLAN_PARTICIPATION, data, fix_request=True, as_json=True From 8e65074b1153259adb5e4efc759b20ae4e234c77 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 17 Apr 2019 21:56:50 +0200 Subject: [PATCH 12/12] Hotfixes Fix mistakes introduced in #415 --- fbchat/_client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index f4a812f..c72c88d 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -278,7 +278,9 @@ class Client(object): "response_format": "json", "queries": graphql_queries_to_json(*queries), } - return tuple(self._post(data, as_graphql=True)) + return tuple( + self._post(self.req_url.GRAPHQL, data, fix_request=True, as_graphql=True) + ) def graphql_request(self, query): """ @@ -3021,7 +3023,7 @@ class Client(object): def _parseMessage(self, content): """Get message and author name from content. May contain multiple messages in the content.""" - self.seq = j.get("seq", "0") + self.seq = content.get("seq", "0") if "lb_info" in content: self.sticky = content["lb_info"]["sticky"]