Merge branch 'master' into fix-enums
This commit is contained in:
291
fbchat/client.py
291
fbchat/client.py
@@ -927,14 +927,14 @@ class Client(object):
|
||||
:type image_id: str
|
||||
:return: An url where you can download the original image
|
||||
:rtype: str
|
||||
:raises: FBChatException if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
image_id = str(image_id)
|
||||
j = check_request(self._get(ReqUrl.ATTACHMENT_PHOTO, query={'photo_id': str(image_id)}))
|
||||
|
||||
url = get_jsmods_require(j, 3)
|
||||
if url is None:
|
||||
raise FBChatException('Could not fetch image url from: {}'.format(j))
|
||||
raise FBchatException('Could not fetch image url from: {}'.format(j))
|
||||
return url
|
||||
|
||||
def fetchMessageInfo(self, mid, thread_id=None):
|
||||
@@ -945,7 +945,7 @@ class Client(object):
|
||||
:param thread_id: User/Group ID to get message info from. See :ref:`intro_threads`
|
||||
:return: :class:`models.Message` object
|
||||
:rtype: models.Message
|
||||
:raises: FBChatException if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
thread_id, thread_type = self._getThread(thread_id, None)
|
||||
message_info = self._forcedFetch(thread_id, mid).get("message")
|
||||
@@ -958,7 +958,7 @@ class Client(object):
|
||||
|
||||
:param poll_id: Poll ID to fetch from
|
||||
:rtype: list
|
||||
:raises: FBChatException if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
data = {
|
||||
"question_id": poll_id
|
||||
@@ -975,7 +975,7 @@ class Client(object):
|
||||
:param plan_id: Plan ID to fetch from
|
||||
:return: :class:`models.Plan` object
|
||||
:rtype: models.Plan
|
||||
:raises: FBChatException if request failed
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
data = {
|
||||
"event_reminder_id": plan_id
|
||||
@@ -1111,7 +1111,56 @@ class Client(object):
|
||||
data['specific_to_list[0]'] = "fbid:{}".format(thread_id)
|
||||
return self._doSendRequest(data)
|
||||
|
||||
def _upload(self, files):
|
||||
def unsend(self, mid):
|
||||
"""
|
||||
Unsends a message (removes for everyone)
|
||||
|
||||
:param mid: :ref:`Message ID <intro_message_ids>` 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)
|
||||
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 as the user's current 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 <intro_message_ids>` of the sent message
|
||||
: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
|
||||
|
||||
: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 <intro_message_ids>` 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, voice_clip=False):
|
||||
"""
|
||||
Uploads files to Facebook
|
||||
|
||||
@@ -1121,7 +1170,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))
|
||||
@@ -1165,7 +1219,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`
|
||||
@@ -1178,6 +1232,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 <intro_message_ids>` 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 <intro_message_ids>` 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
|
||||
@@ -1482,27 +1569,26 @@ class Client(object):
|
||||
|
||||
def reactToMessage(self, message_id, reaction):
|
||||
"""
|
||||
Reacts to a message
|
||||
Reacts to a message, or removes reaction
|
||||
|
||||
:param message_id: :ref:`Message ID <intro_message_ids>` to react to
|
||||
:param reaction: Reaction emoji to use
|
||||
:type reaction: models.MessageReaction
|
||||
:param reaction: Reaction emoji to use, if None removes reaction
|
||||
:type reaction: models.MessageReaction or None
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
full_data = {
|
||||
data = {
|
||||
"doc_id": 1491398900900362,
|
||||
"variables": json.dumps({
|
||||
"data": {
|
||||
"action": "ADD_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
|
||||
"reaction": reaction.value if reaction else None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
j = self._post(self.req_url.MESSAGE_REACTION, full_data, fix_request=True, as_json=True)
|
||||
self._post(self.req_url.MESSAGE_REACTION, data, fix_request=True, as_json=True)
|
||||
|
||||
def createPlan(self, plan, thread_id=None):
|
||||
"""
|
||||
@@ -2019,6 +2105,7 @@ class Client(object):
|
||||
|
||||
delta = m["delta"]
|
||||
delta_type = delta.get("type")
|
||||
delta_class = delta.get("class")
|
||||
metadata = delta.get("messageMetadata")
|
||||
|
||||
if metadata:
|
||||
@@ -2055,14 +2142,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)
|
||||
@@ -2108,7 +2195,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"])
|
||||
@@ -2118,7 +2205,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"])
|
||||
@@ -2127,7 +2214,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"))
|
||||
|
||||
@@ -2227,6 +2314,61 @@ 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 = 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.onMessageUnsent(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":
|
||||
mentions = []
|
||||
@@ -2238,6 +2380,7 @@ class Client(object):
|
||||
|
||||
sticker = None
|
||||
attachments = []
|
||||
unsent = False
|
||||
if delta.get('attachments'):
|
||||
try:
|
||||
for a in delta['attachments']:
|
||||
@@ -2245,17 +2388,23 @@ 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 == ['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'])
|
||||
sticker = graphql_to_sticker(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['extensible_attachment'])
|
||||
if isinstance(attachment, UnsentMessage):
|
||||
unsent = True
|
||||
elif attachment:
|
||||
attachments.append(attachment)
|
||||
|
||||
except Exception:
|
||||
log.exception('An exception occured while reading attachments: {}'.format(delta['attachments']))
|
||||
|
||||
@@ -2267,12 +2416,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.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)
|
||||
@@ -2628,6 +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 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 (deletes for everyone) a 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("{} 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):
|
||||
"""
|
||||
@@ -2709,6 +2872,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, 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
|
||||
@@ -2914,6 +3150,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:
|
||||
|
@@ -127,9 +127,84 @@ def graphql_to_attachment(a):
|
||||
uid=a.get('legacy_attachment_id')
|
||||
)
|
||||
|
||||
def graphql_to_extensible_attachment(a):
|
||||
story = a.get('story_attachment')
|
||||
if story:
|
||||
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),
|
||||
)
|
||||
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':
|
||||
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'],
|
||||
)
|
||||
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']:
|
||||
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 UnsentMessage(
|
||||
uid=a.get('legacy_attachment_id'),
|
||||
)
|
||||
|
||||
|
||||
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'),
|
||||
medium_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"])
|
||||
@@ -206,6 +281,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 = {
|
||||
@@ -214,8 +290,12 @@ 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']]
|
||||
# TODO: This is still missing parsing:
|
||||
# message.get('extensible_attachment')
|
||||
if message.get('extensible_attachment') is not None:
|
||||
attachment = graphql_to_extensible_attachment(message['extensible_attachment'])
|
||||
if isinstance(attachment, UnsentMessage):
|
||||
rtn.unsent = True
|
||||
elif attachment:
|
||||
rtn.attachments.append(attachment)
|
||||
return rtn
|
||||
|
||||
def graphql_to_user(user):
|
||||
|
@@ -190,6 +190,8 @@ class Message(object):
|
||||
sticker = None
|
||||
#: A list of attachments
|
||||
attachments = 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"""
|
||||
@@ -204,6 +206,7 @@ class Message(object):
|
||||
self.attachments = attachments
|
||||
self.reactions = {}
|
||||
self.read_by = []
|
||||
self.deleted = False
|
||||
|
||||
def __repr__(self):
|
||||
return self.__unicode__()
|
||||
@@ -219,6 +222,12 @@ class Attachment(object):
|
||||
"""Represents a Facebook attachment"""
|
||||
self.uid = uid
|
||||
|
||||
class UnsentMessage(Attachment):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Represents an unsent message attachment"""
|
||||
super(UnsentMessage, self).__init__(*args, **kwargs)
|
||||
|
||||
class Sticker(Attachment):
|
||||
#: The sticker-pack's ID
|
||||
pack = None
|
||||
@@ -251,9 +260,79 @@ 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, **kwargs):
|
||||
"""Represents a user location"""
|
||||
super(LocationAttachment, self).__init__(**kwargs)
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
|
||||
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
|
||||
|
@@ -15,10 +15,11 @@ import aenum
|
||||
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 urlparse import parse_qs, urlparse
|
||||
basestring = basestring
|
||||
|
||||
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
|
||||
@@ -141,6 +142,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
|
||||
|
||||
@@ -299,7 +301,6 @@ def get_files_from_paths(filenames):
|
||||
for fn, fp, ft in files:
|
||||
fp.close()
|
||||
|
||||
|
||||
def enum_extend_if_invalid(enumeration, value):
|
||||
try:
|
||||
return enumeration(value)
|
||||
@@ -307,3 +308,10 @@ def enum_extend_if_invalid(enumeration, value):
|
||||
log.warning("Failed parsing {.__name__}({!r}). Extending enum.".format(enumeration, value))
|
||||
aenum.extend_enum(enumeration, "UNKNOWN_{}".format(value).upper(), value)
|
||||
return enumeration(value)
|
||||
|
||||
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]
|
||||
|
Reference in New Issue
Block a user