From 940a65954cd6cfa4895ea14022a2f271ba330f7f Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Wed, 12 Sep 2018 17:52:38 +0200 Subject: [PATCH] Read commit description Added: - Detecting extensible attachments - Fetching live user location - New methods for message reacting - New `on` methods: `onReactionAdded`, `onReactionRemoved`, `onBlock`, `onUnblock`, `onLiveLocation` - Fixed `size` of attachments --- fbchat/client.py | 185 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 21 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 69c858d..dae3960 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1485,7 +1485,30 @@ class Client(object): j = self._post(self.req_url.THREAD_EMOJI, data, fix_request=True, as_json=True) + def _react(self, message_id, reaction=None, add_reaction=True): + data = { + "doc_id": 1491398900900362, + "variables": json.dumps({ + "data": { + "action": "ADD_REACTION" if add_reaction else "REMOVE_REACTION", + "client_mutation_id": "1", + "actor_id": self.uid, + "message_id": str(message_id), + "reaction": reaction.value if add_reaction else None + } + }) + } + + r = self._post(self.req_url.MESSAGE_REACTION, data) + r.raise_for_status() + def reactToMessage(self, message_id, reaction): + """ + Deprecated. Use :func:`fbchat.Client.addReaction` instead + """ + self.addReaction(message_id=message_id, reaction=reaction) + + def addReaction(self, message_id, reaction): """ Reacts to a message @@ -1494,20 +1517,16 @@ class Client(object): :type reaction: models.MessageReaction :raises: FBchatException if request failed """ - full_data = { - "doc_id": 1491398900900362, - "variables": json.dumps({ - "data": { - "action": "ADD_REACTION", - "client_mutation_id": "1", - "actor_id": self.uid, - "message_id": str(message_id), - "reaction": reaction.value - } - }) - } + self._react(message_id=message_id, reaction=reaction, add_reaction=True) - j = self._post(self.req_url.MESSAGE_REACTION, full_data, fix_request=True, as_json=True) + def removeReaction(self, message_id): + """ + Removes reaction from a message + + :param message_id: :ref:`Message ID ` to remove reaction from + :raises: FBchatException if request failed + """ + self._react(message_id=message_id, add_reaction=False) def createPlan(self, plan, thread_id=None): """ @@ -2032,6 +2051,7 @@ class Client(object): delta = m["delta"] delta_type = delta.get("type") + delta_class = delta.get("class") metadata = delta.get("messageMetadata") if metadata: @@ -2068,14 +2088,14 @@ class Client(object): thread_type=thread_type, ts=ts, metadata=metadata, msg=m) # Thread title change - elif delta.get("class") == "ThreadName": + elif delta_class == "ThreadName": new_title = delta["name"] thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onTitleChange(mid=mid, author_id=author_id, new_title=new_title, thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) # Forced fetch - elif delta.get("class") == "ForcedFetch": + elif delta_class == "ForcedFetch": mid = delta.get("messageId") if mid is None: self.onUnknownMesssageType(msg=m) @@ -2121,7 +2141,7 @@ class Client(object): thread_id=thread_id, thread_type=thread_type, ts=ts, msg=m) # Message delivered - elif delta.get("class") == "DeliveryReceipt": + elif delta_class == "DeliveryReceipt": message_ids = delta["messageIds"] delivered_for = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]) ts = int(delta["deliveredWatermarkTimestampMs"]) @@ -2131,7 +2151,7 @@ class Client(object): metadata=metadata, msg=m) # Message seen - elif delta.get("class") == "ReadReceipt": + elif delta_class == "ReadReceipt": seen_by = str(delta.get("actorFbId") or delta["threadKey"]["otherUserFbId"]) seen_ts = int(delta["actionTimestampMs"]) delivered_ts = int(delta["watermarkTimestampMs"]) @@ -2140,7 +2160,7 @@ class Client(object): seen_ts=seen_ts, ts=delivered_ts, metadata=metadata, msg=m) # Messages marked as seen - elif delta.get("class") == "MarkRead": + elif delta_class == "MarkRead": seen_ts = int(delta.get("actionTimestampMs") or delta.get("actionTimestamp")) delivered_ts = int(delta.get("watermarkTimestampMs") or delta.get("watermarkTimestamp")) @@ -2240,6 +2260,51 @@ class Client(object): self.onPlanParticipation(mid=mid, plan=plan, take_part=take_part, author_id=author_id, thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) + # Client payload (that weird numbers) + elif delta_class == "ClientPayload": + payload = json.loads("".join(chr(z) for z in delta['payload'])) + ts = m.get("ofd_ts") + for d in payload.get('deltas', []): + + # Message reaction + if d.get('deltaMessageReaction'): + i = d['deltaMessageReaction'] + thread_id, thread_type = getThreadIdAndThreadType(i) + mid = i["messageId"] + author_id = str(i["userId"]) + reaction = MessageReaction(i["reaction"]) if i.get("reaction") else None + add_reaction = not bool(i["action"]) + if add_reaction: + self.onReactionAdded(mid=mid, reaction=reaction, author_id=author_id, + thread_id=thread_id, thread_type=thread_type, ts=ts, msg=m) + else: + self.onReactionRemoved(mid=mid, author_id=author_id, thread_id=thread_id, + thread_type=thread_type, ts=ts, msg=m) + + # Viewer status change + elif d.get('deltaChangeViewerStatus'): + i = d['deltaChangeViewerStatus'] + thread_id, thread_type = getThreadIdAndThreadType(i) + author_id = str(i["actorFbid"]) + reason = i["reason"] + can_reply = i["canViewerReply"] + if reason == 2: + if can_reply: + self.onUnblock(author_id=author_id, thread_id=thread_id, thread_type=thread_type, ts=ts, msg=m) + else: + self.onBlock(author_id=author_id, thread_id=thread_id, thread_type=thread_type, ts=ts, msg=m) + + # Live location info + elif d.get('liveLocationData'): + i = d['liveLocationData'] + thread_id, thread_type = getThreadIdAndThreadType(i) + for l in i['messageLiveLocations']: + mid = l["messageId"] + author_id = l["senderId"] + location = graphql_to_live_location(l) + self.onLiveLocation(mid=mid, location=location, author_id=author_id, thread_id=thread_id, + thread_type=thread_type, ts=ts, msg=m) + # New message elif delta.get("class") == "NewMessage": mentions = [] @@ -2260,15 +2325,19 @@ class Client(object): attach_type = mercury['blob_attachment']['__typename'] attachment = graphql_to_attachment(mercury.get('blob_attachment', {})) - if attach_type == ['MessageFile', 'MessageVideo', 'MessageAudio']: + if attach_type in ['MessageFile', 'MessageVideo', 'MessageAudio']: # TODO: Add more data here for audio files attachment.size = int(a['fileSize']) attachments.append(attachment) + elif mercury.get('sticker_attachment'): sticker = graphql_to_sticker(a['mercury']['sticker_attachment']) + elif mercury.get('extensible_attachment'): - # TODO: Add more data here for shared stuff (URLs, events and so on) - pass + attachment = graphql_to_extensible_attachment(mercury.get('extensible_attachment', {})) + if attachment: + attachments.append(attachment) + except Exception: log.exception('An exception occured while reading attachments: {}'.format(delta['attachments'])) @@ -2928,6 +2997,7 @@ class Client(object): :param metadata: Extra metadata about the action :param msg: A full set of the data recieved :type plan: models.Plan + :type take_part: bool :type thread_type: models.ThreadType """ if take_part: @@ -2935,6 +3005,79 @@ class Client(object): else: log.info("{} won't take part in {} in {} ({})".format(author_id, plan, thread_id, thread_type.name)) + def onReactionAdded(self, mid=None, reaction=None, add_reaction=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening, and somebody reacts to a message + + :param mid: Message ID, that user reacted to + :param reaction: Reaction + :param add_reaction: Whether user added or removed reaction + :param author_id: The ID of the person who reacted to the message + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type reaction: models.MessageReaction + :type thread_type: models.ThreadType + """ + log.info("{} reacted to message {} with {} in {} ({})".format(author_id, mid, reaction.name, thread_id, thread_type.name)) + + def onReactionRemoved(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening, and somebody removes reaction from a message + + :param mid: Message ID, that user reacted to + :param author_id: The ID of the person who removed reaction + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("{} removed reaction from {} message in {} ({})".format(author_id, mid, thread_id, thread_type)) + + def onBlock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening, and somebody blocks client + + :param author_id: The ID of the person who blocked + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("{} blocked {} ({}) thread".format(author_id, thread_id, thread_type.name)) + + def onUnblock(self, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening, and somebody blocks client + + :param author_id: The ID of the person who unblocked + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type thread_type: models.ThreadType + """ + log.info("{} unblocked {} ({}) thread".format(author_id, thread_id, thread_type.name)) + + def onLiveLocation(self, mid=None, location=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening and somebody sends live location info + + :param mid: The action ID + :param location: Sent location info + :param author_id: The ID of the person who sent location info + :param thread_id: Thread ID that the action was sent to. See :ref:`intro_threads` + :param thread_type: Type of thread that the action was sent to. See :ref:`intro_threads` + :param ts: A timestamp of the action + :param msg: A full set of the data recieved + :type location: models.LiveLocationAttachment + :type thread_type: models.ThreadType + """ + log.info("{} sent live location info in {} ({}) with latitude {} and longitude {}".format(author_id, thread_id, thread_type, location.latitude, location.longitude)) + """ END EVENTS """