From e0be9029e471e11c551d0314a0c13e44004d2c2b Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Wed, 12 Sep 2018 17:48:00 +0200 Subject: [PATCH 01/17] Added extensible attachments models --- fbchat/models.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/fbchat/models.py b/fbchat/models.py index cb4f678..e25ca31 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -248,9 +248,81 @@ class Sticker(Attachment): super(Sticker, self).__init__(*args, **kwargs) class ShareAttachment(Attachment): - def __init__(self, **kwargs): - """Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*""" + #: ID of the author of the shared post + author = None + #: Target URL + url = None + #: Original URL if Facebook redirects the URL + original_url = None + #: Title of the attachment + title = None + #: Description of the attachment + description = None + #: Name of the source + source = None + #: URL of the attachment image + image_url = None + #: URL of the original image if Facebook uses `safe_image` + original_image_url = None + #: Width of the image + image_width = None + #: Height of the image + image_height = None + #: List of additional attachments + attachments = None + + def __init__(self, author=None, url=None, original_url=None, title=None, description=None, source=None, image_url=None, original_image_url=None, image_width=None, image_height=None, attachments=None, **kwargs): + """Represents a shared item (eg. URL) that has been sent as a Facebook attachment""" super(ShareAttachment, self).__init__(**kwargs) + self.author = author + self.url = url + self.original_url = original_url + self.title = title + self.description = description + self.source = source + self.image_url = image_url + self.original_image_url = original_image_url + self.image_width = image_width + self.image_height = image_height + if attachments is None: + attachments = [] + self.attachments = attachments + +class LocationAttachment(Attachment): + #: Latidute of the location + latitude = None + #: Longitude of the location + longitude = None + #: URL of image showing the map of the location + image_url = None + #: Width of the image + image_width = None + #: Height of the image + image_height = None + #: URL to Bing maps with the location + url = None + + def __init__(self, latitude=None, longitude=None, image=None, url=None, **kwargs): + """Represents a user location""" + super(LocationAttachment, self).__init__(**kwargs) + self.latitude = latitude + self.longitude = longitude + self.image = image + self.url = url + +class LiveLocationAttachment(LocationAttachment): + #: Name of the location + name = None + #: Timestamp when live location expires + expiration_time = None + #: True if live location is expired + is_expired = None + + def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs): + """Represents a live user location""" + super(LiveLocationAttachment, self).__init__(**kwargs) + self.expiration_time = expiration_time + self.is_expired = is_expired class FileAttachment(Attachment): #: Url where you can download the file From 9b4e753a79a15d717902242482e09b0d411f0f2a Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Wed, 12 Sep 2018 17:48:35 +0200 Subject: [PATCH 02/17] Added graphql methods for extensible attachments --- fbchat/graphql.py | 69 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index f023a16..72cfb13 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -128,9 +128,72 @@ def graphql_to_attachment(a): uid=a.get('legacy_attachment_id') ) +def graphql_to_extensible_attachment(a): + story = a.get('story_attachment') + if story: + _type = story['target']['__typename'] + if _type == 'MessageLocation': + latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ") + return LocationAttachment( + uid=int(story['deduplication_key']), + latitude=float(latitude), + longitude=float(longitude), + image=story['media']['image']['uri'], + url=story['url'], + ) + elif _type == 'MessageLiveLocation': + return LiveLocationAttachment( + uid=int(story['target']['live_location_id']), + latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None, + longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None, + image=story['media']['image']['uri'] if story.get('media') else None, + url=story['url'], + name=story['title_with_entities']['text'], + expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, + is_expired=story['target']['is_expired'], + ) + elif _type in ['ExternalUrl', 'Story']: + return ShareAttachment( + uid=a.get('legacy_attachment_id'), + author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None, + url=story['url'], + original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'], + title=story['title_with_entities']['text'], + description=story['description']['text'], + source=story['source']['text'], + image_url=story['media']['image']['uri'] if story.get('media') else None, + original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None, + image_width=story['media']['image']['width'] if story.get('media') else None, + image_height=story['media']['image']['height'] if story.get('media') else None, + attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')] + ) + + +def graphql_to_subattachment(a): + _type = a['target']['__typename'] + if _type == 'Video': + return VideoAttachment( + duration=a['media'].get('playable_duration_in_ms'), + preview_url=a['media'].get('playable_url'), + small_image=a['media'].get('image'), + medium_image=a['media'].get('image'), + large_image=a['media'].get('image'), + uid=a['target'].get('video_id'), + ) + +def graphql_to_live_location(a): + return LiveLocationAttachment( + uid=a['id'], + latitude=a['coordinate']['latitude'] / (10 ** 8) if not a.get('stopReason') else None, + longitude=a['coordinate']['longitude'] / (10 ** 8) if not a.get('stopReason') else None, + name=a.get('locationTitle'), + expiration_time=a['expirationTime'], + is_expired=bool(a.get('stopReason')), + ) + def graphql_to_poll(a): rtn = Poll( - title=a.get('title') if a.get('title') else a.get("text"), + title=a.get('title') if a.get('title') else a.get('text'), options=[graphql_to_poll_option(m) for m in a.get('options')] ) rtn.uid = int(a["id"]) @@ -212,8 +275,8 @@ def graphql_to_message(message): rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')} if message.get('blob_attachments') is not None: rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] - # TODO: This is still missing parsing: - # message.get('extensible_attachment') + if message.get('extensible_attachment') is not None: + rtn.attachments.append(graphql_to_extensible_attachment(message['extensible_attachment'])) return rtn def graphql_to_user(user): From 940a65954cd6cfa4895ea14022a2f271ba330f7f Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Wed, 12 Sep 2018 17:52:38 +0200 Subject: [PATCH 03/17] 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 """ From 90813c959d626cde0bc9ddabedccf5c123ad107e Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 15 Sep 2018 11:21:35 +0200 Subject: [PATCH 04/17] Added `get_url_parameters` util method --- fbchat/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fbchat/utils.py b/fbchat/utils.py index 92e8a9d..fd1d1ac 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -14,10 +14,10 @@ import requests from .models import * try: - from urllib.parse import urlencode + from urllib.parse import urlencode, parse_qs, urlparse basestring = (str, bytes) except ImportError: - from urllib import urlencode + from urllib import urlencode, parse_qs, urlparse basestring = basestring # Python 2's `input` executes the input, whereas `raw_input` just returns the input @@ -297,3 +297,10 @@ def get_files_from_paths(filenames): yield files for fn, fp, ft in files: fp.close() + +def get_url_parameters(url, *args): + params = parse_qs(urlparse(url).query) + return [params[arg][0] for arg in args if params.get(arg)] + +def get_url_parameter(url, param): + return get_url_parameters(url, param)[0] From 9e8fe7bc1ea4c0fee1a205257ddac426610360ec Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 15 Sep 2018 11:34:16 +0200 Subject: [PATCH 05/17] Fix Python 2.7 compability --- fbchat/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fbchat/utils.py b/fbchat/utils.py index fd1d1ac..7d8003f 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -17,7 +17,8 @@ try: from urllib.parse import urlencode, parse_qs, urlparse basestring = (str, bytes) except ImportError: - from urllib import urlencode, parse_qs, urlparse + from urllib import urlencode + from urlparse import parse_qs, urlparse basestring = basestring # Python 2's `input` executes the input, whereas `raw_input` just returns the input From c6dc432d06dcb3bc678ee1e64f24dcf0415fcf4f Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 22 Sep 2018 20:39:41 +0200 Subject: [PATCH 06/17] Move `on` methods to the right place --- fbchat/client.py | 146 +++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index dae3960..aaf151e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2792,6 +2792,79 @@ class Client(object): """ log.info("{} played \"{}\" in {} ({})".format(author_id, game_name, 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)) + def onQprimer(self, ts=None, msg=None): """ Called when the client just started listening @@ -3005,79 +3078,6 @@ 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 """ From 8169a5f776f37625aad4d9fb3204b7d17c61383c Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 29 Sep 2018 13:40:38 +0200 Subject: [PATCH 07/17] Changed `LocationAttachment` --- fbchat/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fbchat/models.py b/fbchat/models.py index e25ca31..cc06564 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -302,13 +302,11 @@ class LocationAttachment(Attachment): #: URL to Bing maps with the location url = None - def __init__(self, latitude=None, longitude=None, image=None, url=None, **kwargs): + def __init__(self, latitude=None, longitude=None, **kwargs): """Represents a user location""" super(LocationAttachment, self).__init__(**kwargs) self.latitude = latitude self.longitude = longitude - self.image = image - self.url = url class LiveLocationAttachment(LocationAttachment): #: Name of the location From b0bf5ba8e00fe0d180c2b17a1821b8747ef532ee Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 29 Sep 2018 13:42:11 +0200 Subject: [PATCH 08/17] Update graphql.py --- fbchat/graphql.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 72cfb13..ac5b859 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -134,32 +134,38 @@ def graphql_to_extensible_attachment(a): _type = story['target']['__typename'] if _type == 'MessageLocation': latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ") - return LocationAttachment( + rtn = LocationAttachment( uid=int(story['deduplication_key']), latitude=float(latitude), longitude=float(longitude), - image=story['media']['image']['uri'], - url=story['url'], ) + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] + rtn.url = story['url'] + return rtn elif _type == 'MessageLiveLocation': - return LiveLocationAttachment( + rtn = LiveLocationAttachment( uid=int(story['target']['live_location_id']), latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None, longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None, - image=story['media']['image']['uri'] if story.get('media') else None, - url=story['url'], name=story['title_with_entities']['text'], expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, is_expired=story['target']['is_expired'], ) + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] + rtn.url = story['url'] + return rtn elif _type in ['ExternalUrl', 'Story']: return ShareAttachment( uid=a.get('legacy_attachment_id'), author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None, url=story['url'], original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'], - title=story['title_with_entities']['text'], - description=story['description']['text'], + title=story['title_with_entities'].get('text'), + description=story['description'].get('text'), source=story['source']['text'], image_url=story['media']['image']['uri'] if story.get('media') else None, original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None, @@ -175,9 +181,7 @@ def graphql_to_subattachment(a): return VideoAttachment( duration=a['media'].get('playable_duration_in_ms'), preview_url=a['media'].get('playable_url'), - small_image=a['media'].get('image'), medium_image=a['media'].get('image'), - large_image=a['media'].get('image'), uid=a['target'].get('video_id'), ) From b7ea8e600192e4bd300a0087cb0b2d1fd4be4f33 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sat, 29 Sep 2018 13:48:08 +0200 Subject: [PATCH 09/17] New `sendLocation` method --- fbchat/client.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index aaf151e..7693783 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1116,6 +1116,26 @@ class Client(object): data['specific_to_list[0]'] = "fbid:{}".format(thread_id) return self._doSendRequest(data) + def sendLocation(self, location, thread_id=None, thread_type=None): + """ + Sends a given location to a thread + + :param location: Location to send + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` + :type location: models.LocationAttachment + :type thread_type: models.ThreadType + :return: :ref:`Message ID ` of the sent message + :raises: FBchatException if request failed + """ + thread_id, thread_type = self._getThread(thread_id, thread_type) + data = self._getSendData(thread_id=thread_id, thread_type=thread_type) + data['action_type'] = 'ma-type:user-generated-message' + data['location_attachment[coordinates][latitude]'] = location.latitude + data['location_attachment[coordinates][longitude]'] = location.longitude + data['location_attachment[is_current_location]'] = True + return self._doSendRequest(data) + def _upload(self, files): """ Uploads files to Facebook @@ -1485,16 +1505,16 @@ 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): + def _react(self, message_id, reaction=None): data = { "doc_id": 1491398900900362, "variables": json.dumps({ "data": { - "action": "ADD_REACTION" if add_reaction else "REMOVE_REACTION", + "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 add_reaction else None + "reaction": reaction.value if reaction else None } }) } @@ -1517,7 +1537,7 @@ class Client(object): :type reaction: models.MessageReaction :raises: FBchatException if request failed """ - self._react(message_id=message_id, reaction=reaction, add_reaction=True) + self._react(message_id=message_id, reaction=reaction) def removeReaction(self, message_id): """ @@ -1526,7 +1546,7 @@ class Client(object): :param message_id: :ref:`Message ID ` to remove reaction from :raises: FBchatException if request failed """ - self._react(message_id=message_id, add_reaction=False) + self._react(message_id=message_id) def createPlan(self, plan, thread_id=None): """ From 1ac569badd40361bb3283a5f3bd4e0090686174b Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Tue, 30 Oct 2018 22:21:05 +0100 Subject: [PATCH 10/17] Sending pinned or current location --- fbchat/client.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 7693783..8f588ec 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1116,9 +1116,18 @@ class Client(object): data['specific_to_list[0]'] = "fbid:{}".format(thread_id) return self._doSendRequest(data) + def _sendLocation(self, location, current=True, thread_id=None, thread_type=None): + thread_id, thread_type = self._getThread(thread_id, thread_type) + data = self._getSendData(thread_id=thread_id, thread_type=thread_type) + data['action_type'] = 'ma-type:user-generated-message' + data['location_attachment[coordinates][latitude]'] = location.latitude + data['location_attachment[coordinates][longitude]'] = location.longitude + data['location_attachment[is_current_location]'] = current + return self._doSendRequest(data) + def sendLocation(self, location, thread_id=None, thread_type=None): """ - Sends a given location to a thread + Sends a given location to a thread as the user's current location :param location: Location to send :param thread_id: User/Group ID to send to. See :ref:`intro_threads` @@ -1128,13 +1137,21 @@ class Client(object): :return: :ref:`Message ID ` of the sent message :raises: FBchatException if request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - data = self._getSendData(thread_id=thread_id, thread_type=thread_type) - data['action_type'] = 'ma-type:user-generated-message' - data['location_attachment[coordinates][latitude]'] = location.latitude - data['location_attachment[coordinates][longitude]'] = location.longitude - data['location_attachment[is_current_location]'] = True - return self._doSendRequest(data) + self._sendLocation(location=location, current=True, thread_id=thread_id, thread_type=thread_type) + + def sendPinnedLocation(self, location, thread_id=None, thread_type=None): + """ + Sends a given location to a thread as a pinned location + + :param location: Location to send + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` + :type location: models.LocationAttachment + :type thread_type: models.ThreadType + :return: :ref:`Message ID ` of the sent message + :raises: FBchatException if request failed + """ + self._sendLocation(location=location, current=False, thread_id=thread_id, thread_type=thread_type) def _upload(self, files): """ @@ -2812,7 +2829,7 @@ class Client(object): """ log.info("{} played \"{}\" in {} ({})".format(author_id, game_name, 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): + def onReactionAdded(self, mid=None, 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 From 8739318101dd684edd828ec8b7815f3ccb3ebb86 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Tue, 30 Oct 2018 22:24:47 +0100 Subject: [PATCH 11/17] Sending voice clips --- fbchat/client.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 8f588ec..3b28f2d 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1153,7 +1153,7 @@ class Client(object): """ self._sendLocation(location=location, current=False, thread_id=thread_id, thread_type=thread_type) - def _upload(self, files): + def _upload(self, files, voice_clip=False): """ Uploads files to Facebook @@ -1163,7 +1163,12 @@ class Client(object): Returns a list of tuples with a file's ID and mimetype """ file_dict = {'upload_{}'.format(i): f for i, f in enumerate(files)} - j = self._postFile(self.req_url.UPLOAD, files=file_dict, fix_request=True, as_json=True) + + data = { + "voice_clip": voice_clip, + } + + j = self._postFile(self.req_url.UPLOAD, files=file_dict, query=data, fix_request=True, as_json=True) if len(j['payload']['metadata']) != len(files): raise FBchatException("Some files could not be uploaded: {}, {}".format(j, files)) @@ -1207,7 +1212,7 @@ class Client(object): """ Sends local files to a thread - :param file_path: Paths of files to upload and send + :param file_paths: Paths of files to upload and send :param message: Additional message :param thread_id: User/Group ID to send to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` @@ -1220,6 +1225,39 @@ class Client(object): files = self._upload(x) return self._sendFiles(files=files, message=message, thread_id=thread_id, thread_type=thread_type) + def sendRemoteVoiceClips(self, clip_urls, message=None, thread_id=None, thread_type=ThreadType.USER): + """ + Sends voice clips from URLs to a thread + + :param clip_urls: URLs of clips to upload and send + :param message: Additional message + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` + :type thread_type: models.ThreadType + :return: :ref:`Message ID ` of the sent files + :raises: FBchatException if request failed + """ + clip_urls = require_list(clip_urls) + files = self._upload(get_files_from_urls(clip_urls), voice_clip=True) + return self._sendFiles(files=files, message=message, thread_id=thread_id, thread_type=thread_type) + + def sendLocalVoiceClips(self, clip_paths, message=None, thread_id=None, thread_type=ThreadType.USER): + """ + Sends local voice clips to a thread + + :param clip_paths: Paths of clips to upload and send + :param message: Additional message + :param thread_id: User/Group ID to send to. See :ref:`intro_threads` + :param thread_type: See :ref:`intro_threads` + :type thread_type: models.ThreadType + :return: :ref:`Message ID ` of the sent files + :raises: FBchatException if request failed + """ + clip_paths = require_list(clip_paths) + with get_files_from_paths(clip_paths) as x: + files = self._upload(x, voice_clip=True) + return self._sendFiles(files=files, message=message, thread_id=thread_id, thread_type=thread_type) + def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False): """ Deprecated. Use :func:`fbchat.Client._sendFiles` instead From 41bbe18e3d2f2dceced0c7432d5ad6cb60ff4cca Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sun, 9 Dec 2018 14:36:23 +0100 Subject: [PATCH 12/17] Unsending messages --- fbchat/client.py | 12 ++++++++++++ fbchat/utils.py | 1 + 2 files changed, 13 insertions(+) diff --git a/fbchat/client.py b/fbchat/client.py index 3b28f2d..795b280 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1116,6 +1116,18 @@ class Client(object): data['specific_to_list[0]'] = "fbid:{}".format(thread_id) return self._doSendRequest(data) + def unsend(self, mid): + """ + Unsends a message (removes for everyone) + + :param mid: :ref:`Message ID ` of the message to unsend + """ + data = { + 'message_id': mid, + } + r = self._post(self.req_url.UNSEND, data) + r.raise_for_status() + def _sendLocation(self, location, current=True, thread_id=None, thread_type=None): thread_id, thread_type = self._getThread(thread_id, thread_type) data = self._getSendData(thread_id=thread_id, thread_type=thread_type) diff --git a/fbchat/utils.py b/fbchat/utils.py index 7d8003f..c39e652 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -141,6 +141,7 @@ class ReqUrl(object): GET_POLL_OPTIONS = "https://www.facebook.com/ajax/mercury/get_poll_options" SEARCH_MESSAGES = "https://www.facebook.com/ajax/mercury/search_snippets.php?dpr=1" MARK_SPAM = "https://www.facebook.com/ajax/mercury/mark_spam.php?dpr=1" + UNSEND = "https://www.facebook.com/messaging/unsend_message/?dpr=1" pull_channel = 0 From 861f17bc4dac009631580b6f884184426995903e Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sun, 9 Dec 2018 14:55:10 +0100 Subject: [PATCH 13/17] Added DeletedMessage attachment --- fbchat/graphql.py | 86 +++++++++++++++++++++++++---------------------- fbchat/models.py | 6 ++++ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index ac5b859..b6c1ae2 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -131,47 +131,53 @@ def graphql_to_attachment(a): def graphql_to_extensible_attachment(a): story = a.get('story_attachment') if story: - _type = story['target']['__typename'] - if _type == 'MessageLocation': - latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ") - rtn = LocationAttachment( - uid=int(story['deduplication_key']), - latitude=float(latitude), - longitude=float(longitude), - ) - rtn.image_url = story['media']['image']['uri'] - rtn.image_width = story['media']['image']['width'] - rtn.image_height = story['media']['image']['height'] - rtn.url = story['url'] - return rtn - elif _type == 'MessageLiveLocation': - rtn = LiveLocationAttachment( - uid=int(story['target']['live_location_id']), - latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None, - longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None, - name=story['title_with_entities']['text'], - expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, - is_expired=story['target']['is_expired'], - ) - rtn.image_url = story['media']['image']['uri'] - rtn.image_width = story['media']['image']['width'] - rtn.image_height = story['media']['image']['height'] - rtn.url = story['url'] - return rtn - elif _type in ['ExternalUrl', 'Story']: - return ShareAttachment( + target = story.get('target') + if target: + _type = target['__typename'] + if _type == 'MessageLocation': + latitude, longitude = get_url_parameter(get_url_parameter(story['url'], 'u'), 'where1').split(", ") + rtn = LocationAttachment( + uid=int(story['deduplication_key']), + latitude=float(latitude), + longitude=float(longitude), + ) + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] + rtn.url = story['url'] + return rtn + elif _type == 'MessageLiveLocation': + rtn = LiveLocationAttachment( + uid=int(story['target']['live_location_id']), + latitude=story['target']['coordinate']['latitude'] if story['target'].get('coordinate') else None, + longitude=story['target']['coordinate']['longitude'] if story['target'].get('coordinate') else None, + name=story['title_with_entities']['text'], + expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, + is_expired=story['target']['is_expired'], + ) + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] + rtn.url = story['url'] + return rtn + elif _type in ['ExternalUrl', 'Story']: + return ShareAttachment( + uid=a.get('legacy_attachment_id'), + author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None, + url=story['url'], + original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'], + title=story['title_with_entities'].get('text'), + description=story['description'].get('text'), + source=story['source']['text'], + image_url=story['media']['image']['uri'] if story.get('media') else None, + original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None, + image_width=story['media']['image']['width'] if story.get('media') else None, + image_height=story['media']['image']['height'] if story.get('media') else None, + attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')], + ) + else: + return DeletedMessage( uid=a.get('legacy_attachment_id'), - author=story['target']['actors'][0]['id'] if story['target'].get('actors') else None, - url=story['url'], - original_url=get_url_parameter(story['url'], 'u') if "/l.php?u=" in story['url'] else story['url'], - title=story['title_with_entities'].get('text'), - description=story['description'].get('text'), - source=story['source']['text'], - image_url=story['media']['image']['uri'] if story.get('media') else None, - original_image_url=(get_url_parameter(story['media']['image']['uri'], 'url') if "/safe_image.php" in story['media']['image']['uri'] else story['media']['image']['uri']) if story.get('media') else None, - image_width=story['media']['image']['width'] if story.get('media') else None, - image_height=story['media']['image']['height'] if story.get('media') else None, - attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')] ) diff --git a/fbchat/models.py b/fbchat/models.py index cc06564..f038b59 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -216,6 +216,12 @@ class Attachment(object): """Represents a Facebook attachment""" self.uid = uid +class DeletedMessage(Attachment): + + def __init__(self, *args, **kwargs): + """Represents a deleted message""" + super(DeletedMessage, self).__init__(*args, **kwargs) + class Sticker(Attachment): #: The sticker-pack's ID pack = None From d4446280c7361c75365c55f01dc35e1be5f62227 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sun, 9 Dec 2018 15:27:01 +0100 Subject: [PATCH 14/17] Detecting when someone unsends a message --- fbchat/client.py | 25 ++++++++++++++++++++++++- fbchat/graphql.py | 14 ++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 795b280..dfa9160 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2387,10 +2387,20 @@ class Client(object): thread_id, thread_type = getThreadIdAndThreadType(i) for l in i['messageLiveLocations']: mid = l["messageId"] - author_id = l["senderId"] + author_id = str(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) + + # Message deletion + elif d.get('deltaRecallMessageData'): + i = d['deltaRecallMessageData'] + thread_id, thread_type = getThreadIdAndThreadType(i) + mid = i['messageID'] + ts = i['deletionTimestamp'] + author_id = str(i['senderID']) + self.onMessageDeleted(mid=mid, author_id=author_id, thread_id=thread_id, thread_type=thread_type, + ts=ts, msg=m) # New message elif delta.get("class") == "NewMessage": @@ -2798,6 +2808,19 @@ class Client(object): """ log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) + def onMessageDeleted(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + """ + Called when the client is listening, and someone unsends (deleted for everyone) a message + + :param mid: ID of the deleted message + :param author_id: The ID of the person who deleted 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 thread_type: models.ThreadType + """ + log.info("{} unsended the message {} in {} ({}) at {}s".format(author_id, repr(mid), thread_id, thread_type.name, ts/1000)) def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg=None): """ diff --git a/fbchat/graphql.py b/fbchat/graphql.py index b6c1ae2..0d6c5d2 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -141,9 +141,10 @@ def graphql_to_extensible_attachment(a): latitude=float(latitude), longitude=float(longitude), ) - rtn.image_url = story['media']['image']['uri'] - rtn.image_width = story['media']['image']['width'] - rtn.image_height = story['media']['image']['height'] + if story['media']: + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] rtn.url = story['url'] return rtn elif _type == 'MessageLiveLocation': @@ -155,9 +156,10 @@ def graphql_to_extensible_attachment(a): expiration_time=story['target']['expiration_time'] if story['target'].get('expiration_time') else None, is_expired=story['target']['is_expired'], ) - rtn.image_url = story['media']['image']['uri'] - rtn.image_width = story['media']['image']['width'] - rtn.image_height = story['media']['image']['height'] + if story['media']: + rtn.image_url = story['media']['image']['uri'] + rtn.image_width = story['media']['image']['width'] + rtn.image_height = story['media']['image']['height'] rtn.url = story['url'] return rtn elif _type in ['ExternalUrl', 'Story']: From b9d29c0417cf6e00abc97a644b6ceb9fa9674f57 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sun, 23 Dec 2018 14:45:17 +0100 Subject: [PATCH 15/17] Removed `addReaction`, `removeReaction`, `_react` (and undeprecated `reactToMessage`) --- fbchat/client.py | 44 ++++++++++++-------------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index dfa9160..fbc4686 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1150,7 +1150,7 @@ class Client(object): :raises: FBchatException if request failed """ self._sendLocation(location=location, current=True, thread_id=thread_id, thread_type=thread_type) - + def sendPinnedLocation(self, location, thread_id=None, thread_type=None): """ Sends a given location to a thread as a pinned location @@ -1572,7 +1572,15 @@ 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): + def reactToMessage(self, message_id, reaction): + """ + Reacts to a message, or removes reaction + + :param message_id: :ref:`Message ID ` to react to + :param reaction: Reaction emoji to use, if None removes reaction + :type reaction: models.MessageReaction or None + :raises: FBchatException if request failed + """ data = { "doc_id": 1491398900900362, "variables": json.dumps({ @@ -1585,35 +1593,7 @@ class Client(object): } }) } - - 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 - - :param message_id: :ref:`Message ID ` to react to - :param reaction: Reaction emoji to use - :type reaction: models.MessageReaction - :raises: FBchatException if request failed - """ - self._react(message_id=message_id, reaction=reaction) - - 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) + self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True) def createPlan(self, plan, thread_id=None): """ @@ -2391,7 +2371,7 @@ class Client(object): 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) - + # Message deletion elif d.get('deltaRecallMessageData'): i = d['deltaRecallMessageData'] From 22e57f99a1cf1ec1b9c63d25face89aadecfb9c6 Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Sun, 23 Dec 2018 14:56:27 +0100 Subject: [PATCH 16/17] `deleted` attribute of `Message` and batter handling of deleted (unsended) messages --- fbchat/client.py | 14 +++++++++----- fbchat/graphql.py | 6 +++++- fbchat/models.py | 3 +++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index fbc4686..9a17620 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2393,6 +2393,7 @@ class Client(object): sticker = None attachments = [] + deleted = False if delta.get('attachments'): try: for a in delta['attachments']: @@ -2400,7 +2401,7 @@ class Client(object): if mercury.get('blob_attachment'): image_metadata = a.get('imageMetadata', {}) attach_type = mercury['blob_attachment']['__typename'] - attachment = graphql_to_attachment(mercury.get('blob_attachment', {})) + attachment = graphql_to_attachment(mercury['blob_attachment']) if attach_type in ['MessageFile', 'MessageVideo', 'MessageAudio']: # TODO: Add more data here for audio files @@ -2408,11 +2409,13 @@ class Client(object): attachments.append(attachment) elif mercury.get('sticker_attachment'): - sticker = graphql_to_sticker(a['mercury']['sticker_attachment']) + sticker = graphql_to_sticker(mercury['sticker_attachment']) elif mercury.get('extensible_attachment'): - attachment = graphql_to_extensible_attachment(mercury.get('extensible_attachment', {})) - if attachment: + attachment = graphql_to_extensible_attachment(mercury['extensible_attachment']) + if isinstance(attachment, DeletedMessage): + deleted = True + elif attachment: attachments.append(attachment) except Exception: @@ -2426,12 +2429,13 @@ class Client(object): mentions=mentions, emoji_size=emoji_size, sticker=sticker, - attachments=attachments + attachments=attachments, ) message.uid = mid message.author = author_id message.timestamp = ts #message.reactions = {} + message.deleted = deleted thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onMessage(mid=mid, author_id=author_id, message=delta.get('body', ''), message_object=message, thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 0d6c5d2..362134c 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -288,7 +288,11 @@ def graphql_to_message(message): if message.get('blob_attachments') is not None: rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] if message.get('extensible_attachment') is not None: - rtn.attachments.append(graphql_to_extensible_attachment(message['extensible_attachment'])) + attachment = graphql_to_extensible_attachment(message['extensible_attachment']) + if isinstance(attachment, DeletedMessage): + rtn.deleted = True + elif attachment: + rtn.attachments.append(attachment) return rtn def graphql_to_user(user): diff --git a/fbchat/models.py b/fbchat/models.py index f038b59..55b6407 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -188,6 +188,8 @@ class Message(object): sticker = None #: A list of attachments attachments = None + #: Whether the message is deleted (unsended) + deleted = None def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None): """Represents a Facebook message""" @@ -201,6 +203,7 @@ class Message(object): attachments = [] self.attachments = attachments self.reactions = {} + self.deleted = False def __repr__(self): return self.__unicode__() From 7b8ecf8fe398e0d78f0a5fb77384bab34edb164a Mon Sep 17 00:00:00 2001 From: Kacper Ziubryniewicz Date: Fri, 4 Jan 2019 20:02:00 +0100 Subject: [PATCH 17/17] Changed `deleted` to `unsent` --- fbchat/client.py | 20 ++++++++++---------- fbchat/graphql.py | 7 ++++--- fbchat/models.py | 10 +++++----- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index ade6a8f..2606be9 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -2366,7 +2366,7 @@ class Client(object): mid = i['messageID'] ts = i['deletionTimestamp'] author_id = str(i['senderID']) - self.onMessageDeleted(mid=mid, author_id=author_id, thread_id=thread_id, thread_type=thread_type, + self.onMessageUnsent(mid=mid, author_id=author_id, thread_id=thread_id, thread_type=thread_type, ts=ts, msg=m) # New message @@ -2380,7 +2380,7 @@ class Client(object): sticker = None attachments = [] - deleted = False + unsent = False if delta.get('attachments'): try: for a in delta['attachments']: @@ -2400,8 +2400,8 @@ class Client(object): elif mercury.get('extensible_attachment'): attachment = graphql_to_extensible_attachment(mercury['extensible_attachment']) - if isinstance(attachment, DeletedMessage): - deleted = True + if isinstance(attachment, UnsentMessage): + unsent = True elif attachment: attachments.append(attachment) @@ -2422,7 +2422,7 @@ class Client(object): message.author = author_id message.timestamp = ts #message.reactions = {} - message.deleted = deleted + message.unsent = unsent thread_id, thread_type = getThreadIdAndThreadType(metadata) self.onMessage(mid=mid, author_id=author_id, message=delta.get('body', ''), message_object=message, thread_id=thread_id, thread_type=thread_type, ts=ts, metadata=metadata, msg=m) @@ -2778,19 +2778,19 @@ class Client(object): """ log.info("Marked messages as seen in threads {} at {}s".format([(x[0], x[1].name) for x in threads], seen_ts/1000)) - def onMessageDeleted(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): + def onMessageUnsent(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None): """ - Called when the client is listening, and someone unsends (deleted for everyone) a message + Called when the client is listening, and someone unsends (deletes for everyone) a message - :param mid: ID of the deleted message - :param author_id: The ID of the person who deleted the message + :param mid: ID of the unsent message + :param author_id: The ID of the person who unsent 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 thread_type: models.ThreadType """ - log.info("{} unsended the message {} in {} ({}) at {}s".format(author_id, repr(mid), thread_id, thread_type.name, ts/1000)) + log.info("{} unsent the message {} in {} ({}) at {}s".format(author_id, repr(mid), thread_id, thread_type.name, ts/1000)) def onPeopleAdded(self, mid=None, added_ids=None, author_id=None, thread_id=None, ts=None, msg=None): """ diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 641f1f6..1335893 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -178,7 +178,7 @@ def graphql_to_extensible_attachment(a): attachments=[graphql_to_subattachment(attachment) for attachment in story.get('subattachments')], ) else: - return DeletedMessage( + return UnsentMessage( uid=a.get('legacy_attachment_id'), ) @@ -282,6 +282,7 @@ def graphql_to_message(message): rtn.uid = str(message.get('message_id')) rtn.author = str(message.get('message_sender').get('id')) rtn.timestamp = message.get('timestamp_precise') + rtn.unsent = False if message.get('unread') is not None: rtn.is_read = not message['unread'] rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')} @@ -289,8 +290,8 @@ def graphql_to_message(message): rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']] if message.get('extensible_attachment') is not None: attachment = graphql_to_extensible_attachment(message['extensible_attachment']) - if isinstance(attachment, DeletedMessage): - rtn.deleted = True + if isinstance(attachment, UnsentMessage): + rtn.unsent = True elif attachment: rtn.attachments.append(attachment) return rtn diff --git a/fbchat/models.py b/fbchat/models.py index d1d59d8..ac38455 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -190,8 +190,8 @@ class Message(object): sticker = None #: A list of attachments attachments = None - #: Whether the message is deleted (unsended) - deleted = None + #: Whether the message is unsent (deleted for everyone) + unsent = None def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None): """Represents a Facebook message""" @@ -222,11 +222,11 @@ class Attachment(object): """Represents a Facebook attachment""" self.uid = uid -class DeletedMessage(Attachment): +class UnsentMessage(Attachment): def __init__(self, *args, **kwargs): - """Represents a deleted message""" - super(DeletedMessage, self).__init__(*args, **kwargs) + """Represents an unsent message attachment""" + super(UnsentMessage, self).__init__(*args, **kwargs) class Sticker(Attachment): #: The sticker-pack's ID