Merge branch 'attachment-support'

This commit is contained in:
Mads Marquart
2017-10-21 12:09:52 +02:00
6 changed files with 335 additions and 45 deletions

View File

@@ -175,8 +175,8 @@ meaning it will simply print information to the console when an event happens
The event actions can be changed by subclassing the :class:`Client`, and then overwriting the event methods::
class CustomClient(Client):
def onMessage(self, mid, author_id, message, thread_id, thread_type, ts, metadata, msg, **kwargs):
# Do something with the message here
def onMessage(self, mid, author_id, message_object, thread_id, thread_type, ts, metadata, msg, **kwargs):
# Do something with the message_object here
pass
client = CustomClient('<email>', '<password>')
@@ -184,13 +184,13 @@ The event actions can be changed by subclassing the :class:`Client`, and then ov
**Notice:** The following snippet is as equally valid as the previous one::
class CustomClient(Client):
def onMessage(self, message, author_id, thread_id, thread_type, **kwargs):
def onMessage(self, message_object, author_id, thread_id, thread_type, **kwargs):
# Do something with the message here
pass
client = CustomClient('<email>', '<password>')
The change was in the parameters that our `onMessage` method took: ``message`` and ``author_id`` got swapped,
The change was in the parameters that our `onMessage` method took: ``message_object`` and ``author_id`` got swapped,
and ``mid``, ``ts``, ``metadata`` and ``msg`` got removed, but the function still works, since we included ``**kwargs``
.. note::

View File

@@ -4,15 +4,15 @@ from fbchat import log, Client
# Subclass fbchat.Client and override required methods
class EchoBot(Client):
def onMessage(self, author_id, message, thread_id, thread_type, **kwargs):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.markAsDelivered(author_id, thread_id)
self.markAsRead(author_id)
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message))
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
# If you're not the author, echo
if author_id != self.uid:
self.sendMessage(message, thread_id=thread_id, thread_type=thread_type)
# If you're not the author, and the message was a message containing text, echo
if author_id != self.uid and message_object.text is not None:
self.sendMessage(message_object.text, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>")
client.listen()

View File

@@ -4,14 +4,14 @@ from fbchat import log, Client
from fbchat.models import *
class RemoveBot(Client):
def onMessage(self, author_id, message, thread_id, thread_type, **kwargs):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat
if message == 'Remove me!' and thread_type == ThreadType.GROUP:
if message_object.text == 'Remove me!' and thread_type == ThreadType.GROUP:
log.info('{} will be removed from {}'.format(author_id, thread_id))
self.removeUserFromGroup(author_id, thread_id=thread_id)
else:
# Sends the data to the inherited onMessage, so that we can still see when a message is recieved
super(type(self), self).onMessage(author_id=author_id, message=message, thread_id=thread_id, thread_type=thread_type, **kwargs)
super(RemoveBot, self).onMessage(author_id=author_id, message_object=message_object, thread_id=thread_id, thread_type=thread_type, **kwargs)
client = RemoveBot("<email>", "<password>")
client.listen()

View File

@@ -842,6 +842,23 @@ class Client(object):
"unseen_threads": j['payload']['unseen_thread_ids']
}
def fetchImageUrl(self, image_id):
"""Fetches the url to the original image from an image attachment ID
:param image_id: The image you want to fethc
:type image_id: str
:return: An url where you can download the original image
:rtype: str
:raises: FBChatException if request failed
"""
image_id = str(image_id)
j = checkRequest(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))
return url
"""
END FETCH METHODS
"""
@@ -905,11 +922,9 @@ class Client(object):
raise FBchatException('Error when sending message: No message IDs could be found: {}'.format(j))
# update JS token if received in response
if j.get('jsmods') is not None and j['jsmods'].get('require') is not None:
try:
self.payloadDefault['fb_dtsg'] = j['jsmods']['require'][0][3][0]
except (KeyError, IndexError) as e:
log.warning('Error when updating fb_dtsg. Facebook might have changed protocol!')
fb_dtsg = get_jsmods_require(j, 2)
if fb_dtsg is not None:
self.payloadDefault['fb_dtsg'] = fb_dtsg
return message_id
@@ -1391,7 +1406,7 @@ class Client(object):
delta_type = delta.get("type")
metadata = delta.get("messageMetadata")
if metadata is not None:
if metadata:
mid = metadata["messageId"]
author_id = str(metadata['actorFbId'])
ts = int(metadata.get("timestamp"))
@@ -1472,9 +1487,91 @@ class Client(object):
# New message
elif delta.get("class") == "NewMessage":
message = delta.get('body', '')
mentions = []
if delta.get('data') and delta['data'].get('prng'):
try:
mentions = [Mention(str(mention.get('i')), offset=mention.get('o'), length=mention.get('l')) for mention in parse_json(delta['data']['prng'])]
except Exception:
log.exception('An exception occured while reading attachments')
attachments = []
if delta.get('attachments'):
try:
for a in delta['attachments']:
mercury = a['mercury']
blob = mercury.get('blob_attachment', {})
image_metadata = a.get('imageMetadata', {})
attach_type = mercury['attach_type']
if attach_type in ['photo', 'animated_image']:
attachments.append(ImageAttachment(
original_extension=blob.get('original_extension') or (blob['filename'].split('-')[0] if blob.get('filename') else None),
width=int(image_metadata['width']),
height=int(image_metadata['height']),
is_animated=attach_type=='animated_image',
thumbnail_url=mercury.get('thumbnail_url'),
preview=blob.get('preview') or blob.get('preview_image'),
large_preview=blob.get('large_preview'),
animated_preview=blob.get('animated_image'),
uid=a['id']
))
elif attach_type == 'file':
# Add more data here for audio files
attachments.append(FileAttachment(
url=mercury.get('url'),
size=int(a['fileSize']),
name=mercury.get('name'),
is_malicious=blob.get('is_malicious'),
uid=a['id']
))
elif attach_type == 'video':
attachments.append(VideoAttachment(
size=int(a['fileSize']),
width=int(image_metadata['width']),
height=int(image_metadata['height']),
duration=blob.get('playable_duration_in_ms'),
preview_url=blob.get('playable_url'),
small_image=blob.get('chat_image'),
medium_image=blob.get('inbox_image'),
large_image=blob.get('large_image'),
uid=a['id']
))
elif attach_type == 'sticker':
# Add more data here for stickers
attachments.append(StickerAttachment(
uid=mercury.get('metadata', {}).get('stickerID')
))
elif attach_type == 'share':
# Add more data here for shared stuff (URLs, events and so on)
attachments.append(ShareAttachment(
uid=a.get('id')
))
else:
attachments.append(Attachment(
uid=a.get('id')
))
except Exception:
log.exception('An exception occured while reading attachments: {}'.format(delta['attachments']))
emoji_size = None
if metadata and metadata.get('tags'):
for tag in metadata['tags']:
if tag.startswith('hot_emoji_size:'):
emoji_size = LIKES[tag.split(':')[1]]
break
message = Message(
text=delta.get('body'),
mentions=mentions,
emoji_size=emoji_size
)
message.uid = mid
message.author = author_id
message.timestamp = ts
message.attachments = attachments
#message.is_read = None
#message.reactions = []
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onMessage(mid=mid, author_id=author_id, message=message,
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)
# Unknown message type
@@ -1632,21 +1729,23 @@ class Client(object):
return True
def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
def onMessage(self, mid=None, author_id=None, message=None, message_object=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
"""
Called when the client is listening, and somebody sends a message
:param mid: The message ID
:param author_id: The ID of the author
:param message: The message
:param message: (deprecated. Use `message_object.text` instead)
:param message_object: The message (As a `Message` object)
:param thread_id: Thread ID that the message was sent to. See :ref:`intro_threads`
:param thread_type: Type of thread that the message was sent to. See :ref:`intro_threads`
:param ts: The timestamp of the message
:param metadata: Extra metadata about the message
:param msg: A full set of the data recieved
:type message_object: models.Message
:type thread_type: models.ThreadType
"""
log.info("Message from {} in {} ({}): {}".format(author_id, thread_id, thread_type.name, message))
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
def onColorChange(self, mid=None, author_id=None, new_color=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg=None):
"""

View File

@@ -163,6 +163,10 @@ class Page(Thread):
class Message(object):
#: The actual message
text = str
#: A list of :class:`Mention` objects
mentions = list
#: The message ID
uid = str
#: ID of the sender
@@ -179,12 +183,12 @@ class Message(object):
mentions = list
#: An ID of a sent sticker
sticker = str
#: A :class:`EmojiSize`. Size of a sent emoji
emoji_size = None
#: A list of attachments
attachments = list
#: An extensible attachment, e.g. share object
extensible_attachment = dict
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=None, text=None, mentions=None, sticker=None, attachments=None, extensible_attachment=None):
def __init__(self, uid, author=None, timestamp=None, is_read=None, reactions=None, text=None, mentions=None, sticker=None, attachments=None, extensible_attachment=None, emoji_size=None):
"""Represents a Facebook message"""
self.uid = uid
self.author = author
@@ -204,6 +208,180 @@ class Message(object):
if extensible_attachment is None:
extensible_attachment = {}
self.extensible_attachment = extensible_attachment
self.emoji_size = emoji_size
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Message ({}): {}, mentions={} emoji_size={} attachments={}>'.format(self.uid, repr(self.text), self.mentions, self.emoji_size, self.attachments)
class Attachment(object):
#: The attachment ID
uid = str
def __init__(self, uid=None, mime_type=None):
"""Represents a Facebook attachment"""
self.uid = uid
class StickerAttachment(Attachment):
def __init__(self, **kwargs):
"""Represents a sticker that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(StickerAttachment, self).__init__(**kwargs)
class ShareAttachment(Attachment):
def __init__(self, **kwargs):
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(ShareAttachment, self).__init__(**kwargs)
class FileAttachment(Attachment):
#: Url where you can download the file
url = str
#: Size of the file in bytes
size = int
#: Name of the file
name = str
#: Whether Facebook determines that this file may be harmful
is_malicious = bool
def __init__(self, url=None, size=None, name=None, is_malicious=None, **kwargs):
"""Represents a file that has been sent as a Facebook attachment"""
super(FileAttachment, self).__init__(**kwargs)
self.url = url
self.size = size
self.name = name
self.is_malicious = is_malicious
class AudioAttachment(FileAttachment):
def __init__(self, **kwargs):
"""Represents an audio file that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(StickerAttachment, self).__init__(**kwargs)
class ImageAttachment(Attachment):
#: The extension of the original image (eg. 'png')
original_extension = str
#: Width of original image
width = int
#: Height of original image
height = int
#: Whether the image is animated
is_animated = bool
#: Url to a thumbnail of the image
thumbnail_url = str
#: URL to a medium preview of the image
preview_url = str
#: Width of the medium preview image
preview_width = int
#: Height of the medium preview image
preview_height = int
#: URL to a large preview of the image
large_preview_url = str
#: Width of the large preview image
large_preview_width = int
#: Height of the large preview image
large_preview_height = int
#: URL to an animated preview of the image (eg. for gifs)
animated_preview_url = str
#: Width of the animated preview image
animated_preview_width = int
#: Height of the animated preview image
animated_preview_height = int
def __init__(self, original_extension=None, width=None, height=None, is_animated=None, thumbnail_url=None, preview=None, large_preview=None, animated_preview=None, **kwargs):
"""
Represents an image that has been sent as a Facebook attachment
To retrieve the full image url, use: :func:`fbchat.Client.fetchImageUrl`,
and pass it the uid of the image attachment
"""
super(ImageAttachment, self).__init__(**kwargs)
self.original_extension = original_extension
self.width = width
self.height = height
self.is_animated = is_animated
self.thumbnail_url = thumbnail_url
if preview is None:
preview = {}
self.preview_url = preview.get('uri')
self.preview_width = preview.get('width')
self.preview_height = preview.get('height')
if large_preview is None:
large_preview = {}
self.large_preview_url = large_preview.get('uri')
self.large_preview_width = large_preview.get('width')
self.large_preview_height = large_preview.get('height')
if animated_preview is None:
animated_preview = {}
self.animated_preview_url = animated_preview.get('uri')
self.animated_preview_width = animated_preview.get('width')
self.animated_preview_height = animated_preview.get('height')
class VideoAttachment(Attachment):
#: Size of the original video in bytes
size = int
#: Width of original video
width = int
#: Height of original video
height = int
#: Length of video in milliseconds
duration = int
#: URL to very compressed preview video
preview_url = str
#: URL to a small preview image of the video
small_image_url = str
#: Width of the small preview image
small_image_width = int
#: Height of the small preview image
small_image_height = int
#: URL to a medium preview image of the video
medium_image_url = str
#: Width of the medium preview image
medium_image_width = int
#: Height of the medium preview image
medium_image_height = int
#: URL to a large preview image of the video
large_image_url = str
#: Width of the large preview image
large_image_width = int
#: Height of the large preview image
large_image_height = int
def __init__(self, size=None, width=None, height=None, duration=None, preview_url=None, small_image=None, medium_image=None, large_image=None, **kwargs):
"""Represents a video that has been sent as a Facebook attachment"""
super(VideoAttachment, self).__init__(**kwargs)
self.size = size
self.width = width
self.height = height
self.duration = duration
self.preview_url = preview_url
if small_image is None:
small_image = {}
self.small_image_url = small_image.get('uri')
self.small_image_width = small_image.get('width')
self.small_image_height = small_image.get('height')
if medium_image is None:
medium_image = {}
self.medium_image_url = medium_image.get('uri')
self.medium_image_width = medium_image.get('width')
self.medium_image_height = medium_image.get('height')
if large_image is None:
large_image = {}
self.large_image_url = large_image.get('uri')
self.large_image_width = large_image.get('width')
self.large_image_height = large_image.get('height')
class Mention(object):
@@ -278,22 +456,3 @@ class MessageReaction(Enum):
ANGRY = '😠'
YES = '👍'
NO = '👎'
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}

View File

@@ -32,6 +32,26 @@ USER_AGENTS = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
]
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}
GENDERS = {
# For standard requests
0: 'unknown',
@@ -92,6 +112,7 @@ class ReqUrl(object):
MESSAGE_REACTION = "https://www.facebook.com/webgraphql/mutation"
TYPING = "https://www.facebook.com/ajax/messaging/typ.php"
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/"
EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create"
pull_channel = 0
@@ -122,6 +143,9 @@ def get_decoded_r(r):
def get_decoded(content):
return content.decode(facebookEncoding)
def parse_json(content):
return json.loads(content)
def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r)))
@@ -183,3 +207,11 @@ def check_request(r, as_json=True):
return j
else:
return content
def get_jsmods_require(j, index):
if j.get('jsmods') and j['jsmods'].get('require'):
try:
return j['jsmods']['require'][0][index][0]
except (KeyError, IndexError) as e:
log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j))
return None