See commit description

- Deprecated `sendMessage` and `sendEmoji` in favor of `send`
- (Almost) Fully integrated attachment support
- Updated tests
- General cleanup
This commit is contained in:
Mads Marquart
2017-10-21 17:59:44 +02:00
parent dda75c6099
commit 7ecf229db5
10 changed files with 337 additions and 280 deletions

View File

@@ -17,7 +17,7 @@ from .client import *
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.0.25'
__version__ = '1.1.0'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'

View File

@@ -5,7 +5,6 @@ import requests
import urllib
from uuid import uuid1
from random import choice
from datetime import datetime
from bs4 import BeautifulSoup as bs
from mimetypes import guess_type
from .utils import *
@@ -730,7 +729,7 @@ class Client(object):
"""
Get the last messages in a thread
:param thread_id: User/Group ID to default to. See :ref:`intro_threads`
:param thread_id: User/Group ID to get messages from. See :ref:`intro_threads`
:param limit: Max. number of messages to retrieve
:param before: A timestamp, indicating from which point to retrieve messages
:type limit: int
@@ -740,6 +739,8 @@ class Client(object):
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
j = self.graphql_request(GraphQL(doc_id='1386147188135407', params={
'id': thread_id,
'message_limit': limit,
@@ -867,45 +868,53 @@ class Client(object):
SEND METHODS
"""
def _getSendData(self, thread_id=None, thread_type=ThreadType.USER):
def _oldMessage(self, message):
return message if isinstance(message, Message) else Message(text=message)
def _getSendData(self, message=None, thread_id=None, thread_type=ThreadType.USER):
"""Returns the data needed to send a request to `SendURL`"""
messageAndOTID = generateOfflineThreadingID()
timestamp = now()
date = datetime.now()
data = {
'client': self.client,
'author' : 'fbid:' + str(self.uid),
'timestamp' : timestamp,
'timestamp_absolute' : 'Today',
'timestamp_relative' : str(date.hour) + ":" + str(date.minute).zfill(2),
'timestamp_time_passed' : '0',
'is_unread' : False,
'is_cleared' : False,
'is_forward' : False,
'is_filtered_content' : False,
'is_filtered_content_bh': False,
'is_filtered_content_account': False,
'is_filtered_content_quasar': False,
'is_filtered_content_invalid_app': False,
'is_spoof_warning' : False,
'source' : 'source:chat:web',
'source_tags[0]' : 'source:chat',
'html_body' : False,
'ui_push_phase' : 'V3',
'status' : '0',
'offline_threading_id': messageAndOTID,
'message_id' : messageAndOTID,
'threading_id': generateMessageID(self.client_id),
'ephemeral_ttl_mode:': '0',
'manual_retry_cnt' : '0',
'signatureID' : getSignatureID()
'ephemeral_ttl_mode:': '0'
}
# Set recipient
if thread_type in [ThreadType.USER, ThreadType.PAGE]:
data["other_user_fbid"] = thread_id
data['other_user_fbid'] = thread_id
elif thread_type == ThreadType.GROUP:
data["thread_fbid"] = thread_id
data['thread_fbid'] = thread_id
if message is None:
message = Message()
if message.text or message.sticker or message.emoji_size:
data['action_type'] = 'ma-type:user-generated-message'
if message.text:
data['body'] = message.text
for i, mention in enumerate(message.mentions):
data['profile_xmd[{}][id]'.format(i)] = mention.thread_id
data['profile_xmd[{}][offset]'.format(i)] = mention.offset
data['profile_xmd[{}][length]'.format(i)] = mention.length
data['profile_xmd[{}][type]'.format(i)] = 'p'
if message.emoji_size:
if message.text:
data['tags[0]'] = 'hot_emoji_size:' + message.emoji_size.name.lower()
else:
data['sticker_id'] = message.emoji_size.value
if message.sticker:
data['sticker_id'] = message.sticker.uid
return data
@@ -928,67 +937,34 @@ class Client(object):
return message_id
def sendMessage(self, message, mention=None, thread_id=None,
thread_type=ThreadType.USER):
def send(self, message, thread_id=None, thread_type=ThreadType.USER):
"""
Sends a message to a thread
:param message: Message to send
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type message: models.Message
:type thread_type: models.ThreadType
:mention is in this format {userID : (start, end)},
where start is relative start position of @mention in a message
and end is relative end position of @mention
:return: :ref:`Message ID <intro_message_ids>` 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_type)
data['action_type'] = 'ma-type:user-generated-message'
data['body'] = message or ''
if mention:
n = 0
for key, value in mention.items():
data['profile_xmd[%d][id]'%n] = key
data['profile_xmd[%d][offset]'%n] = value[0]
data['profile_xmd[%d][length]'%n] = value[1] - value[0]
data['profile_xmd[%d][type]'%n] = 'p'
n += 1
data['has_attachment'] = False
data['specific_to_list[0]'] = 'fbid:' + thread_id
data['specific_to_list[1]'] = 'fbid:' + self.uid
data = self._getSendData(message=message, thread_id=thread_id, thread_type=thread_type)
return self._doSendRequest(data)
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
"""
Deprecated. Use :func:`fbchat.Client.send` instead
"""
return self.send(Message(text=message), thread_id=thread_id, thread_type=thread_type)
def sendEmoji(self, emoji=None, size=EmojiSize.SMALL, thread_id=None, thread_type=ThreadType.USER):
"""
Sends an emoji to a thread
:param emoji: The chosen emoji to send. If not specified, the default `like` emoji is sent
:param size: If not specified, a small emoji is sent
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:type size: models.EmojiSize
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent emoji
:raises: FBchatException if request failed
Deprecated. Use :func:`fbchat.Client.send` instead
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type)
data['action_type'] = 'ma-type:user-generated-message'
data['has_attachment'] = False
data['specific_to_list[0]'] = 'fbid:' + thread_id
data['specific_to_list[1]'] = 'fbid:' + self.uid
if emoji:
data['body'] = emoji
data['tags[0]'] = 'hot_emoji_size:' + size.name.lower()
else:
data["sticker_id"] = size.value
return self._doSendRequest(data)
return self.send(Message(text=emoji, emoji_size=size), thread_id=thread_id, thread_type=thread_type)
def _uploadImage(self, image_path, data, mimetype):
"""Upload an image and get the image_id for sending in a message"""
@@ -1008,25 +984,13 @@ class Client(object):
def sendImage(self, image_id, message=None, thread_id=None, thread_type=ThreadType.USER, is_gif=False):
"""
Sends an already uploaded image to a thread. (Used by :func:`Client.sendRemoteImage` and :func:`Client.sendLocalImage`)
:param image_id: ID of an image that's already uploaded to Facebook
:param message: Additional message
:param thread_id: User/Group ID to send to. See :ref:`intro_threads`
:param thread_type: See :ref:`intro_threads`
:param is_gif: if sending GIF, True, else False
:type thread_type: models.ThreadType
:return: :ref:`Message ID <intro_message_ids>` of the sent image
:raises: FBchatException if request failed
Deprecated. Use :func:`fbchat.Client.send` instead
"""
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = self._getSendData(thread_id, thread_type)
data = self._getSendData(message=message, thread_id=thread_id, thread_type=thread_type)
data['action_type'] = 'ma-type:user-generated-message'
data['body'] = message or ''
data['has_attachment'] = True
data['specific_to_list[0]'] = 'fbid:' + str(thread_id)
data['specific_to_list[1]'] = 'fbid:' + str(self.uid)
if not is_gif:
data['image_ids[0]'] = image_id
@@ -1083,7 +1047,7 @@ class Client(object):
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
data = self._getSendData(thread_id, ThreadType.GROUP)
data = self._getSendData(thread_id=thread_id, thread_type=ThreadType.GROUP)
data['action_type'] = 'ma-type:log-message'
data['log_message_type'] = 'log:subscribe'
@@ -1138,7 +1102,7 @@ class Client(object):
# The thread is a user, so we change the user's nickname
return self.changeNickname(title, thread_id, thread_id=thread_id, thread_type=thread_type)
else:
data = self._getSendData(thread_id, thread_type)
data = self._getSendData(thread_id=thread_id, thread_type=thread_type)
data['action_type'] = 'ma-type:log-message'
data['log_message_data[name]'] = title
@@ -1240,11 +1204,17 @@ class Client(object):
"""
Sets an event reminder
:param thread_id: :ref:`Thread ID <intro_thread_ids>` to send event to
..warning::
Does not work in Python2.7
..todo::
Make this work in Python2.7
:param thread_id: User/Group ID to send event to. See :ref:`intro_threads`
:param time: Event time (unix time stamp)
:param title: Event title
:param location: Event location
:param location_ir: Event location ID
:param location: Event location name
:param location_id: Event location ID
:raises: FBchatException if request failed
"""
full_data = {
@@ -1506,82 +1476,43 @@ class Client(object):
except Exception:
log.exception('An exception occured while reading attachments')
sticker = None
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')
))
if mercury.get('attach_type'):
image_metadata = a.get('imageMetadata', {})
attach_type = mercury['attach_type']
attachment = graphql_to_attachment(mercury.get('blob_attachment', {}))
if attach_type == ['file', 'video']:
# TODO: Add more data here for audio files
attachment.size = int(a['fileSize'])
elif attach_type == 'share':
# TODO: Add more data here for shared stuff (URLs, events and so on)
pass
attachments.append(attachment)
if a['mercury'].get('sticker_attachment'):
sticker = graphql_to_sticker(a['mercury']['sticker_attachment'])
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
emoji_size = get_emojisize_from_tags(metadata.get('tags'))
message = Message(
text=delta.get('body'),
mentions=mentions,
emoji_size=emoji_size
emoji_size=emoji_size,
sticker=sticker,
attachments=attachments
)
message.uid = mid
message.author = author_id
message.timestamp = ts
message.attachments = attachments
#message.is_read = None
#message.reactions = []
#message.reactions = {}
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)

View File

@@ -61,26 +61,88 @@ def get_customization_info(thread):
rtn['own_nickname'] = pc[1].get('nickname')
return rtn
def graphql_to_sticker(s):
if not s:
return None
sticker = Sticker(
uid=s['id']
)
if s.get('pack'):
sticker.pack = s['pack'].get('id')
if s.get('sprite_image'):
sticker.is_animated = True
sticker.medium_sprite_image = s['sprite_image'].get('uri')
sticker.large_sprite_image = s['sprite_image_2x'].get('uri')
sticker.frames_per_row = s.get('frames_per_row')
sticker.frames_per_col = s.get('frames_per_column')
sticker.frame_rate = s.get('frame_rate')
sticker.url = s.get('url')
sticker.width = s.get('width')
sticker.height = s.get('height')
if s.get('label'):
sticker.label = s['label']
return sticker
def graphql_to_attachment(a):
_type = a['__typename']
if _type in ['MessageImage', 'MessageAnimatedImage']:
return ImageAttachment(
original_extension=a.get('original_extension') or (a['filename'].split('-')[0] if a.get('filename') else None),
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
is_animated=_type=='MessageAnimatedImage',
thumbnail_url=a.get('thumbnail', {}).get('uri'),
preview=a.get('preview') or a.get('preview_image'),
large_preview=a.get('large_preview'),
animated_preview=a.get('animated_image'),
uid=a.get('legacy_attachment_id')
)
elif _type == 'MessageVideo':
return VideoAttachment(
width=a.get('original_dimensions', {}).get('width'),
height=a.get('original_dimensions', {}).get('height'),
duration=a.get('playable_duration_in_ms'),
preview_url=a.get('playable_url'),
small_image=a.get('chat_image'),
medium_image=a.get('inbox_image'),
large_image=a.get('large_image'),
uid=a.get('legacy_attachment_id')
)
elif _type == 'MessageFile':
return FileAttachment(
url=a.get('url'),
name=a.get('filename'),
is_malicious=a.get('is_malicious'),
uid=a.get('message_file_fbid')
)
else:
return Attachment(
uid=a.get('legacy_attachment_id')
)
def graphql_to_message(message):
if message.get('message_sender') is None:
message['message_sender'] = {}
if message.get('message') is None:
message['message'] = {}
is_read = None
if message.get('unread') is not None:
is_read = not message['unread']
return Message(
message.get('message_id'),
author=message.get('message_sender').get('id'),
timestamp=message.get('timestamp_precise'),
is_read=is_read,
reactions=message.get('message_reactions'),
rtn = Message(
text=message.get('message').get('text'),
mentions=[Mention(m.get('entity', {}).get('id'), offset=m.get('offset'), length=m.get('length')) for m in message.get('message').get('ranges', [])],
sticker=message.get('sticker'),
attachments=message.get('blob_attachments'),
extensible_attachment=message.get('extensible_attachment')
emoji_size=get_emojisize_from_tags(message.get('tags_list')),
sticker=graphql_to_sticker(message.get('sticker'))
)
rtn.uid = str(message.get('message_id'))
rtn.author = str(message.get('message_sender').get('id'))
rtn.timestamp = message.get('timestamp_precise')
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')}
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')
return rtn
def graphql_to_user(user):
if user.get('profile_picture') is None:

View File

@@ -164,51 +164,40 @@ class Page(Thread):
class Message(object):
#: The actual message
text = str
text = None
#: A list of :class:`Mention` objects
mentions = list
#: The message ID
uid = str
#: ID of the sender
author = int
#: Timestamp of when the message was sent
timestamp = str
#: Whether the message is read
is_read = bool
#: A list of message reactions
reactions = list
#: The actual message
text = str
#: A list of :class:`Mention` objects
mentions = list
#: An ID of a sent sticker
sticker = str
mentions = []
#: A :class:`EmojiSize`. Size of a sent emoji
emoji_size = None
#: The message ID
uid = None
#: ID of the sender
author = None
#: Timestamp of when the message was sent
timestamp = None
#: Whether the message is read
is_read = None
#: A dict with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = {}
#: The actual message
text = None
#: A :class:`Sticker`
sticker = None
#: A list of attachments
attachments = list
attachments = []
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):
def __init__(self, text=None, mentions=None, emoji_size=None, sticker=None, attachments=None):
"""Represents a Facebook message"""
self.uid = uid
self.author = author
self.timestamp = timestamp
self.is_read = is_read
if reactions is None:
reactions = []
self.reactions = reactions
self.text = text
if mentions is None:
mentions = []
self.mentions = mentions
self.emoji_size = emoji_size
self.sticker = sticker
if attachments is None:
attachments = []
self.attachments = attachments
if extensible_attachment is None:
extensible_attachment = {}
self.extensible_attachment = extensible_attachment
self.emoji_size = emoji_size
self.reactions = {}
def __repr__(self):
return self.__unicode__()
@@ -220,14 +209,40 @@ class Attachment(object):
#: The attachment ID
uid = str
def __init__(self, uid=None, mime_type=None):
def __init__(self, uid=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 Sticker(Attachment):
#: The sticker-pack's ID
pack = None
#: Whether the sticker is animated
is_animated = False
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = None
#: URL to a large spritemap
large_sprite_image = None
#: The amount of frames present in the spritemap pr. row
frames_per_row = None
#: The amount of frames present in the spritemap pr. coloumn
frames_per_col = None
#: The frame rate the spritemap is intended to be played in
frame_rate = None
#: URL to the sticker's image
url = None
#: Width of the sticker
width = None
#: Height of the sticker
height = None
#: The sticker's label/name
label = None
def __init__(self, *args, **kwargs):
"""Represents a Facebook sticker that has been sent to a Facebook thread as an attachment"""
super(Sticker, self).__init__(*args, **kwargs)
class ShareAttachment(Attachment):
def __init__(self, **kwargs):
@@ -268,7 +283,7 @@ class ImageAttachment(Attachment):
#: Whether the image is animated
is_animated = bool
#: Url to a thumbnail of the image
#: URL to a thumbnail of the image
thumbnail_url = str
#: URL to a medium preview of the image
@@ -300,7 +315,11 @@ class ImageAttachment(Attachment):
"""
super(ImageAttachment, self).__init__(**kwargs)
self.original_extension = original_extension
if width is not None:
width = int(width)
self.width = width
if height is not None:
height = int(height)
self.height = height
self.is_animated = is_animated
self.thumbnail_url = thumbnail_url
@@ -385,19 +404,25 @@ class VideoAttachment(Attachment):
class Mention(object):
#: The user ID the mention is pointing at
user_id = str
#: The thread ID the mention is pointing at
thread_id = str
#: The character where the mention starts
offset = int
#: The length of the mention
length = int
def __init__(self, user_id, offset=0, length=10):
def __init__(self, thread_id, offset=0, length=10):
"""Represents a @mention"""
self.user_id = user_id
self.thread_id = thread_id
self.offset = offset
self.length = length
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<Mention {}: offset={} length={}>'.format(self.thread_id, self.offset, self.length)
class Enum(enum.Enum):
"""Used internally by fbchat to support enumerations"""
def __repr__(self):

View File

@@ -215,3 +215,14 @@ def get_jsmods_require(j, index):
except (KeyError, IndexError) as e:
log.warning('Error when getting jsmods_require: {}. Facebook might have changed protocol'.format(j))
return None
def get_emojisize_from_tags(tags):
if tags is None:
return None
tmp = [tag for tag in tags if tag.startswith('hot_emoji_size:')]
if len(tmp) > 0:
try:
return LIKES[tmp[0].split(':')[1]]
except (KeyError, IndexError):
log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp))
return None