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 diff --git a/fbchat/__init__.py b/fbchat/__init__.py index e5acc44..59d1cbe 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -14,7 +14,7 @@ from ._graphql import graphql_queries_to_json, graphql_response_to_json, GraphQL from ._client import Client __title__ = "fbchat" -__version__ = "1.6.3" +__version__ = "1.6.4" __description__ = "Facebook Chat (Messenger) for Python" __copyright__ = "Copyright 2015 - 2019 by Taehoon Kim" diff --git a/fbchat/_client.py b/fbchat/_client.py index e73e413..fe68f9a 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). @@ -60,7 +67,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 @@ -166,6 +172,7 @@ class Client(object): timeout=30, fix_request=False, as_json=False, + as_graphql=False, error_retries=3, ): payload = self._generatePayload(query) @@ -179,7 +186,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 +199,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, @@ -272,15 +273,13 @@ class Client(object): :return: A tuple containing json graphql queries :rtype: tuple """ - + data = { + "method": "GET", + "response_format": "json", + "queries": graphql_queries_to_json(*queries), + } return tuple( - self._graphql( - { - "method": "GET", - "response_format": "json", - "queries": graphql_queries_to_json(*queries), - } - ) + self._post(self.req_url.GRAPHQL, data, fix_request=True, as_graphql=True) ) def graphql_request(self, query): @@ -314,7 +313,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) @@ -390,9 +389,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 +403,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 +412,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 +422,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( @@ -459,7 +458,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 @@ -512,9 +510,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): @@ -525,7 +522,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) @@ -586,15 +582,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): """ @@ -671,8 +660,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 @@ -685,7 +672,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 @@ -700,7 +686,6 @@ class Client(object): # Skip invalid users continue users.append(User._from_all_fetch(data)) - return users def searchForUsers(self, name, limit=10): @@ -713,10 +698,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 [User._from_graphql(node) for node in j[name]["users"]["nodes"]] @@ -729,10 +712,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 [Page._from_graphql(node) for node in j[name]["pages"]["nodes"]] @@ -746,10 +727,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 [Group._from_graphql(node) for node in j["viewer"]["groups"]["nodes"]] @@ -763,12 +742,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"]: @@ -784,9 +759,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 @@ -864,23 +837,17 @@ 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: - 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)} @@ -931,14 +898,13 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - 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 @@ -954,14 +920,13 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - 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 @@ -974,14 +939,13 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - 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 @@ -997,21 +961,16 @@ class Client(object): :rtype: dict :raises: FBchatException if request failed """ - 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) @@ -1067,33 +1026,26 @@ class Client(object): :rtype: list :raises: FBchatException if request failed """ - 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="1860982147341344", params=params)) if j.get("message_thread") is None: raise FBchatException("Could not fetch thread {}: {}".format(thread_id, j)) - messages = list( - reversed( - [ - Message._from_graphql(message) - for message in j["message_thread"]["messages"]["nodes"] - ] - ) - ) + messages = [ + Message._from_graphql(message) + for message in j["message_thread"]["messages"]["nodes"] + ] + messages.reverse() + read_receipts = j["message_thread"]["read_receipts"]["nodes"] for message in messages: @@ -1118,10 +1070,11 @@ 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" + "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: @@ -1132,18 +1085,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)) rtn = [] for node in j["viewer"]["message_threads"]["nodes"]: @@ -1172,13 +1121,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): @@ -1194,7 +1141,6 @@ class Client(object): ) payload = j["payload"]["unseen_thread_fbids"][0] - return payload["thread_fbids"] + payload["other_user_fbids"] def fetchImageUrl(self, image_id): @@ -1207,8 +1153,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) @@ -1239,11 +1186,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 [PollOption._from_graphql(m) for m in j["payload"]] def fetchPlanInfo(self, plan_id): @@ -1316,7 +1261,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, @@ -1374,6 +1319,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): @@ -1399,9 +1347,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): @@ -1420,7 +1367,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): @@ -1713,19 +1659,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 @@ -1801,8 +1743,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) @@ -1814,11 +1756,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): @@ -1829,7 +1769,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) @@ -1864,7 +1804,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): @@ -1872,20 +1811,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): @@ -1916,7 +1851,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} @@ -1932,7 +1866,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) @@ -1944,7 +1877,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) @@ -1961,7 +1893,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: @@ -1971,7 +1902,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( @@ -1994,7 +1924,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 ) @@ -2014,7 +1943,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): @@ -2030,7 +1958,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): @@ -2043,19 +1970,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): @@ -2069,23 +1990,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): """ @@ -2096,23 +2010,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): """ @@ -2121,19 +2028,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): """ @@ -2143,30 +2039,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): """ @@ -2235,7 +2122,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) """ @@ -2436,8 +2322,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): """ @@ -2456,8 +2341,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): """ @@ -2476,8 +2360,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): """ @@ -2507,7 +2390,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, @@ -2515,11 +2397,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): @@ -3027,6 +2905,24 @@ class Client(object): msg=m, ) + elif d.get("deltaMessageReply"): + i = d["deltaMessageReply"] + metadata = i["message"]["messageMetadata"] + thread_id, thread_type = getThreadIdAndThreadType(metadata) + message = Message._from_reply(i["message"]) + message.replied_to = Message._from_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": thread_id, thread_type = getThreadIdAndThreadType(metadata) @@ -3050,6 +2946,7 @@ class Client(object): def _parseMessage(self, content): """Get message and author name from content. May contain multiple messages in the content.""" + self.seq = content.get("seq", "0") if "lb_info" in content: self.sticky = content["lb_info"]["sticky"] diff --git a/fbchat/_message.py b/fbchat/_message.py index b5a9423..be3e328 100644 --- a/fbchat/_message.py +++ b/fbchat/_message.py @@ -86,6 +86,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): @@ -192,6 +196,56 @@ class Message(object): rtn.unsent = True elif attachment: rtn.attachments.append(attachment) + if data.get("replied_to_message") is not None: + rtn.replied_to = cls._from_graphql(data["replied_to_message"]["message"]) + return rtn + + @classmethod + def _from_reply(cls, data): + rtn = cls( + text=data.get("body"), + mentions=[ + Mention(m.get("i"), offset=m.get("o"), length=m.get("l")) + for m in json.loads(data.get("data", {}).get("prng", "[]")) + ], + emoji_size=EmojiSize._from_tags(data["messageMetadata"].get("tags")), + ) + metadata = data.get("messageMetadata", {}) + rtn.uid = metadata.get("messageId") + rtn.author = str(metadata.get("actorFbId")) + rtn.timestamp = metadata.get("timestamp") + rtn.unsent = False + if data.get("data", {}).get("platform_xmd"): + quick_replies = json.loads(data["data"]["platform_xmd"]).get( + "quick_replies" + ) + if isinstance(quick_replies, list): + rtn.quick_replies = [ + _quick_reply.graphql_to_quick_reply(q) for q in quick_replies + ] + elif isinstance(quick_replies, dict): + rtn.quick_replies = [ + _quick_reply.graphql_to_quick_reply(quick_replies, is_response=True) + ] + if data.get("attachments") is not None: + for attachment in data["attachments"]: + attachment = json.loads(attachment["mercuryJSON"]) + if attachment.get("blob_attachment"): + rtn.attachments.append( + _file.graphql_to_attachment(attachment["blob_attachment"]) + ) + if attachment.get("extensible_attachment"): + extensible_attachment = graphql_to_extensible_attachment( + attachment["extensible_attachment"] + ) + if isinstance(extensible_attachment, _attachment.UnsentMessage): + rtn.unsent = True + else: + rtn.attachments.append(extensible_attachment) + if attachment.get("sticker_attachment"): + rtn.sticker = _sticker.Sticker._from_graphql( + attachment["sticker_attachment"] + ) return rtn @classmethod diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 8ae8898..3206140 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" @classmethod def _from_graphql(cls, color):