Compare commits

..

1 Commits

Author SHA1 Message Date
Mads Marquart
1d42c4d3a6 Updated to 1.0.4, added fetchThread&GroupInfo and improved models 2017-06-26 15:41:58 +02:00
8 changed files with 228 additions and 36 deletions

View File

@@ -52,12 +52,12 @@ A thread can refer to two things: A Messenger group chat or a single Facebook us
These will specify whether the thread is a single user chat or a group chat. These will specify whether the thread is a single user chat or a group chat.
This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally
Searching for group chats and finding their ID is not yet possible with `fbchat`, Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
but searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching` and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
You can get your own user ID by using :any:`Client.uid` You can get your own user ID by using :any:`Client.uid`
Getting the ID of a group chat is fairly trivial though, since you only need to navigate to `<https://www.facebook.com/messages/>`_, Getting the ID of a group chat is fairly trivial otherwise, since you only need to navigate to `<https://www.facebook.com/messages/>`_,
click on the group you want to find the ID of, and then read the id from the address bar. click on the group you want to find the ID of, and then read the id from the address bar.
The URL will look something like this: ``https://www.facebook.com/messages/t/1234567890``, where ``1234567890`` would be the ID of the group. The URL will look something like this: ``https://www.facebook.com/messages/t/1234567890``, where ``1234567890`` would be the ID of the group.
An image to illustrate this is shown below: An image to illustrate this is shown below:

View File

@@ -12,7 +12,7 @@ print("users' IDs: {}".format(user.uid for user in users))
print("users' names: {}".format(user.name for user in users)) print("users' names: {}".format(user.name for user in users))
# If we have a user id, we can use `getUserInfo` to fetch a `User` object # If we have a user id, we can use `fetchUserInfo` to fetch a `User` object
user = client.fetchUserInfo('<user id>')['<user id>'] user = client.fetchUserInfo('<user id>')['<user id>']
# We can also query both mutiple users together, which returns list of `User` objects # We can also query both mutiple users together, which returns list of `User` objects
users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>') users = client.fetchUserInfo('<1st user id>', '<2nd user id>', '<3rd user id>')
@@ -49,4 +49,16 @@ for message in messages:
print(message.text) print(message.text)
# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object
thread = client.fetchThreadInfo('<thread id>')['<thread id>']
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# `searchForThreads` searches works like `searchForUsers`, but gives us a list of threads instead
thread = client.searchForThreads('<name of thread>')[0]
print("thread's name: {}".format(thread.name))
print("thread's type: {}".format(thread.type))
# Here should be an example of `getUnread` # Here should be an example of `getUnread`

View File

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

View File

@@ -110,7 +110,7 @@ class Client(object):
headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type') headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type')
return self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files) return self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files)
def graphql_request(self, *queries): def graphql_requests(self, *queries):
""" """
.. todo:: .. todo::
Documenting this Documenting this
@@ -125,10 +125,15 @@ class Client(object):
j = graphql_response_to_json(checkRequest(self._post(ReqUrl.GRAPHQL, payload), do_json_check=False)) j = graphql_response_to_json(checkRequest(self._post(ReqUrl.GRAPHQL, payload), do_json_check=False))
if len(j) == 1: return tuple(j)
return j[0]
else: def graphql_request(self, query):
return tuple(j) """
Shorthand for `graphql_requests(query)[0]`
:raises: Exception if request failed
"""
return self.graphql_requests(query)[0]
""" """
@@ -455,9 +460,6 @@ class Client(object):
return [graphql_to_page(node) for node in j[name]['pages']['nodes']] return [graphql_to_page(node) for node in j[name]['pages']['nodes']]
#entries = self._searchFor(name)
#return [k for k in entries if k.type == ThreadType.PAGE]
def searchForGroups(self, name, limit=1): def searchForGroups(self, name, limit=1):
""" """
Find and get group thread by its name Find and get group thread by its name
@@ -510,35 +512,56 @@ class Client(object):
j = checkRequest(self._post(ReqUrl.INFO, data)) j = checkRequest(self._post(ReqUrl.INFO, data))
if not j['payload']['profiles']: if not j['payload']['profiles']:
raise Exception('No users returned') raise Exception('No users/pages returned')
entries = {} entries = {}
for _id in j['payload']['profiles']: for _id in j['payload']['profiles']:
k = j['payload']['profiles'][_id] k = j['payload']['profiles'][_id]
if k['type'] in ['user', 'friend']: if k['type'] in ['user', 'friend']:
entries[_id] = User(_id, url=k['uri'], first_name=k['firstName'], is_friend=k['is_friend'], gender=GENDERS[k['gender']], photo=k['thumbSrc'], name=k['name']) entries[_id] = {
'id': _id,
'type': ThreadType.USER,
'url': k.get('uri'),
'first_name': k.get('firstName'),
'is_viewer_friend': k.get('is_friend'),
'gender': k.get('gender'),
'profile_picture': {'uri': k.get('thumbSrc')},
'name': k.get('name')
}
elif k['type'] == 'page': elif k['type'] == 'page':
entries[_id] = Page(_id, url=k['uri'], photo=k['thumbSrc'], name=k['name']) entries[_id] = {
'id': _id,
'type': ThreadType.PAGE,
'url': k.get('uri'),
'profile_picture': {'uri': k.get('thumbSrc')},
'name': k.get('name')
}
else: else:
raise Exception('{} had an unknown thread type: {}'.format(_id, k)) raise Exception('{} had an unknown thread type: {}'.format(_id, k))
log.debug(entries)
return entries return entries
def fetchUserInfo(self, *user_ids): def fetchUserInfo(self, *user_ids):
""" """
Get users' info from IDs, unordered Get users' info from IDs, unordered
.. warning::
Sends two requests, to fetch all available info!
:param user_ids: One or more user ID(s) to query :param user_ids: One or more user ID(s) to query
:return: :class:`models.User` objects, labeled by their ID :return: :class:`models.User` objects, labeled by their ID
:rtype: dict :rtype: dict
:raises: Exception if request failed :raises: Exception if request failed
""" """
entries = self._fetchInfo(*user_ids) threads = self.fetchThreadInfo(*user_ids)
users = {} users = {}
for k in entries: for k in threads:
if entries[k].type == ThreadType.USER: if threads[k].type == ThreadType.USER:
users[k] = entries[k] users[k] = threads[k]
else:
raise Exception('Thread {} was not a user'.format(threads[k]))
return users return users
@@ -546,19 +569,104 @@ class Client(object):
""" """
Get pages' info from IDs, unordered Get pages' info from IDs, unordered
.. warning::
Sends two requests, to fetch all available info!
:param page_ids: One or more page ID(s) to query :param page_ids: One or more page ID(s) to query
:return: :class:`models.Page` objects, labeled by their ID :return: :class:`models.Page` objects, labeled by their ID
:rtype: dict :rtype: dict
:raises: Exception if request failed :raises: Exception if request failed
""" """
entries = self._fetchInfo(*user_ids) threads = self.fetchThreadInfo(*page_ids)
users = {} pages = {}
for k in entries: for k in threads:
if entries[k].type == ThreadType.PAGE: if threads[k].type == ThreadType.PAGE:
users[k] = entries[k] pages[k] = threads[k]
else:
raise Exception('Thread {} was not a page'.format(threads[k]))
return users return pages
def fetchGroupInfo(self, *group_ids):
"""
Get groups' info from IDs, unordered
:param group_ids: One or more group ID(s) to query
:return: :class:`models.Group` objects, labeled by their ID
:rtype: dict
:raises: Exception if request failed
"""
threads = self.fetchThreadInfo(*group_ids)
groups = {}
for k in threads:
if threads[k].type == ThreadType.GROUP:
groups[k] = threads[k]
else:
raise Exception('Thread {} was not a group'.format(threads[k]))
return groups
def fetchThreadInfo(self, *thread_ids):
"""
Get threads' info from IDs, unordered
.. warning::
Sends two requests if users or pages are present, to fetch all available info!
:param thread_ids: One or more thread ID(s) to query
:return: :class:`models.Thread` objects, labeled by their ID
:rtype: dict
:raises: Exception if request failed
"""
queries = []
for thread_id in thread_ids:
queries.append(GraphQL(doc_id='1386147188135407', params={
'id': thread_id,
'message_limit': 0,
'load_messages': False,
'load_read_receipts': False,
'before': None
}))
j = self.graphql_requests(*queries)
for i, entry in enumerate(j):
if entry.get('message_thread') is None:
# If you don't have an existing thread with this person, attempt to retrieve user data anyways
j[i]['message_thread'] = {
'thread_key': {
'other_user_id': thread_ids[i]
},
'thread_type': 'ONE_TO_ONE'
}
pages_and_user_ids = [k['message_thread']['thread_key']['other_user_id'] for k in j if k['message_thread'].get('thread_type') == 'ONE_TO_ONE']
pages_and_users = {}
if len(pages_and_user_ids) != 0:
pages_and_users = self._fetchInfo(*pages_and_user_ids)
rtn = {}
for i, entry in enumerate(j):
entry = entry['message_thread']
if entry.get('thread_type') == 'GROUP':
_id = entry['thread_key']['thread_fbid']
rtn[_id] = graphql_to_group(entry)
elif entry.get('thread_type') == 'ONE_TO_ONE':
_id = entry['thread_key']['other_user_id']
if pages_and_users.get(_id) is None:
raise Exception('Could not fetch thread {}'.format(_id))
entry.update(pages_and_users[_id])
if entry['type'] == ThreadType.USER:
rtn[_id] = graphql_to_user(entry)
else:
rtn[_id] = graphql_to_page(entry)
else:
raise Exception('{} had an unknown thread type: {}'.format(thread_ids[i], entry))
return rtn
def fetchThreadMessages(self, thread_id=None, limit=20, before=None): def fetchThreadMessages(self, thread_id=None, limit=20, before=None):
""" """

View File

@@ -23,6 +23,39 @@ class ConcatJSONDecoder(json.JSONDecoder):
return objs return objs
# End shameless copy # End shameless copy
def graphql_color_to_enum(color):
if color is None:
return None
try:
return ThreadColor('#{}'.format(color[2:].lower()))
except KeyError:
raise Exception('Could not get ThreadColor from color: {}'.format(color))
def get_customization_info(thread):
if thread is None or thread.get('customization_info') is None:
return {}
info = thread['customization_info']
rtn = {
'emoji': info.get('emoji'),
'color': graphql_color_to_enum(info.get('outgoing_bubble_color'))
}
if thread.get('thread_type') == 'GROUP' or thread.get('is_group_thread') or thread.get('thread_key', {}).get('thread_fbid'):
rtn['nicknames'] = {}
for k in info['participant_customizations']:
rtn['nicknames'][k['participant_id']] = k.get('nickname')
else:
_id = thread.get('thread_key', {}).get('other_user_id') or thread.get('id')
if info['participant_customizations'][0]['participant_id'] == _id:
rtn['nickname'] = info['participant_customizations'][0]
rtn['own_nickname'] = info['participant_customizations'][1]
elif info['participant_customizations'][1]['participant_id'] == _id:
rtn['nickname'] = info['participant_customizations'][1]
rtn['own_nickname'] = info['participant_customizations'][0]
else:
raise Exception('No participant matching the user {} found: {}'.format(_id, info['participant_customizations']))
return rtn
def graphql_to_message(message): def graphql_to_message(message):
if message.get('message_sender') is None: if message.get('message_sender') is None:
message['message_sender'] = {} message['message_sender'] = {}
@@ -46,6 +79,7 @@ def graphql_to_message(message):
def graphql_to_user(user): def graphql_to_user(user):
if user.get('profile_picture') is None: if user.get('profile_picture') is None:
user['profile_picture'] = {} user['profile_picture'] = {}
c_info = get_customization_info(user)
return User( return User(
user['id'], user['id'],
url=user.get('url'), url=user.get('url'),
@@ -54,6 +88,10 @@ def graphql_to_user(user):
is_friend=user.get('is_viewer_friend'), is_friend=user.get('is_viewer_friend'),
gender=GENDERS[user.get('gender')], gender=GENDERS[user.get('gender')],
affinity=user.get('affinity'), affinity=user.get('affinity'),
nickname=c_info.get('nickname'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
own_nickname=c_info.get('own_nickname'),
photo=user['profile_picture'].get('uri'), photo=user['profile_picture'].get('uri'),
name=user.get('name') name=user.get('name')
) )
@@ -61,9 +99,13 @@ def graphql_to_user(user):
def graphql_to_group(group): def graphql_to_group(group):
if group.get('image') is None: if group.get('image') is None:
group['image'] = {} group['image'] = {}
c_info = get_customization_info(group)
return Group( return Group(
group['thread_key']['thread_fbid'], group['thread_key']['thread_fbid'],
participants=[node['messaging_actor']['id'] for node in group['all_participants']['nodes']], participants=[node['messaging_actor']['id'] for node in group['all_participants']['nodes']],
nicknames=c_info.get('nicknames'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
photo=group['image'].get('uri'), photo=group['image'].get('uri'),
name=group.get('name') name=group.get('name')
) )
@@ -160,6 +202,14 @@ class GraphQL(object):
id id
} }
} }
},
customization_info {
participant_customizations {
participant_id,
nickname
},
outgoing_bubble_color,
emoji
} }
} }
""" """

View File

@@ -41,8 +41,16 @@ class User(Thread):
gender = str gender = str
#: From 0 to 1. How close the client is to the user #: From 0 to 1. How close the client is to the user
affinity = float affinity = float
#: The user's nickname
nickname = str
#: The clients nickname, as seen by the user
own_nickname = str
#: A :class:`ThreadColor`. The message color
color = None
#: The default emoji
emoji = str
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, **kwargs): def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
"""Represents a Facebook user. Inherits `Thread`""" """Represents a Facebook user. Inherits `Thread`"""
super(User, self).__init__(ThreadType.USER, uid, **kwargs) super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url self.url = url
@@ -51,16 +59,29 @@ class User(Thread):
self.is_friend = is_friend self.is_friend = is_friend
self.gender = gender self.gender = gender
self.affinity = affinity self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
class Group(Thread): class Group(Thread):
#: List of the group thread's participant user IDs #: List of the group thread's participant user IDs
participants = list participants = list
#: Dict, containing user nicknames mapped to their IDs
nicknames = dict
#: A :class:`ThreadColor`. The groups's message color
color = None
#: The groups's default emoji
emoji = str
def __init__(self, uid, participants=[], **kwargs): def __init__(self, uid, participants=[], nicknames=[], color=None, emoji=None, **kwargs):
"""Represents a Facebook group. Inherits `Thread`""" """Represents a Facebook group. Inherits `Thread`"""
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
self.participants = participants self.participants = participants
self.nicknames = nicknames
self.color = color
self.emoji = emoji
class Page(Thread): class Page(Thread):

View File

@@ -32,12 +32,6 @@ USER_AGENTS = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
] ]
TYPES = {
'Page': ThreadType.PAGE,
'User': ThreadType.USER,
'Group': ThreadType.GROUP
}
GENDERS = { GENDERS = {
# For standard requests # For standard requests
0: 'unknown', 0: 'unknown',

View File

@@ -79,7 +79,7 @@ class TestFbchat(unittest.TestCase):
users = client.fetchAllUsers() users = client.fetchAllUsers()
self.assertGreater(len(users), 0) self.assertGreater(len(users), 0)
def test_searchForUsers(self): def test_searchFor(self):
users = client.searchForUsers('Mark Zuckerberg') users = client.searchForUsers('Mark Zuckerberg')
self.assertGreater(len(users), 0) self.assertGreater(len(users), 0)
@@ -92,6 +92,10 @@ class TestFbchat(unittest.TestCase):
self.assertEqual(u.url[:4], 'http') self.assertEqual(u.url[:4], 'http')
self.assertEqual(u.name, 'Mark Zuckerberg') self.assertEqual(u.name, 'Mark Zuckerberg')
group_name = client.changeThreadTitle('tést_searchFor', thread_id=group_id, thread_type=ThreadType.GROUP)
groups = client.searchForGroups('')
self.assertGreater(len(groups), 0)
def test_sendEmoji(self): def test_sendEmoji(self):
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_id, thread_type=ThreadType.USER)) self.assertIsNotNone(client.sendEmoji(size=EmojiSize.SMALL, thread_id=user_id, thread_type=ThreadType.USER))
self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER)) self.assertIsNotNone(client.sendEmoji(size=EmojiSize.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER))
@@ -140,10 +144,13 @@ class TestFbchat(unittest.TestCase):
self.assertTrue(client.got_qprimer) self.assertTrue(client.got_qprimer)
def test_fetchUserInfo(self): def test_fetchInfo(self):
info = client.fetchUserInfo('4')['4'] info = client.fetchUserInfo('4')['4']
self.assertEqual(info.name, 'Mark Zuckerberg') self.assertEqual(info.name, 'Mark Zuckerberg')
info = client.fetchGroupInfo(group_id)[group_id]
self.assertEqual(info.type, ThreadType.GROUP)
def test_removeAddFromGroup(self): def test_removeAddFromGroup(self):
client.removeUserFromGroup(user_id, thread_id=group_id) client.removeUserFromGroup(user_id, thread_id=group_id)
client.addUsersToGroup(user_id, thread_id=group_id) client.addUsersToGroup(user_id, thread_id=group_id)