Merge branch 'attachment-support'
This commit is contained in:
@@ -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::
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
121
fbchat/client.py
121
fbchat/client.py
@@ -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):
|
||||
"""
|
||||
|
203
fbchat/models.py
203
fbchat/models.py
@@ -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')
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user