From 1d42c4d3a633228ce90bbac4547435d5c99a732d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 26 Jun 2017 15:41:58 +0200 Subject: [PATCH] Updated to 1.0.4, added fetchThread&GroupInfo and improved models --- docs/intro.rst | 6 +- examples/fetch.py | 14 ++++- fbchat/__init__.py | 2 +- fbchat/client.py | 150 ++++++++++++++++++++++++++++++++++++++------- fbchat/graphql.py | 50 +++++++++++++++ fbchat/models.py | 25 +++++++- fbchat/utils.py | 6 -- tests.py | 11 +++- 8 files changed, 228 insertions(+), 36 deletions(-) diff --git a/docs/intro.rst b/docs/intro.rst index 5e1963d..9e3d07f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -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. 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`, -but searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching` +Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`, +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` -Getting the ID of a group chat is fairly trivial though, since you only need to navigate to ``_, +Getting the ID of a group chat is fairly trivial otherwise, since you only need to navigate to ``_, 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. An image to illustrate this is shown below: diff --git a/examples/fetch.py b/examples/fetch.py index 101ec09..e59ec63 100644 --- a/examples/fetch.py +++ b/examples/fetch.py @@ -12,7 +12,7 @@ print("users' IDs: {}".format(user.uid 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('')[''] # 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>') @@ -49,4 +49,16 @@ for message in messages: print(message.text) +# If we have a thread id, we can use `fetchThreadInfo` to fetch a `Thread` object +thread = client.fetchThreadInfo('')[''] +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('')[0] +print("thread's name: {}".format(thread.name)) +print("thread's type: {}".format(thread.type)) + + # Here should be an example of `getUnread` diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 73e564c..4536c13 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -17,7 +17,7 @@ from .client import * __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) -__version__ = '1.0.3' +__version__ = '1.0.4' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __email__ = 'carpedm20@gmail.com' diff --git a/fbchat/client.py b/fbchat/client.py index f5cdfb3..8d48b6b 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -110,7 +110,7 @@ class Client(object): 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) - def graphql_request(self, *queries): + def graphql_requests(self, *queries): """ .. todo:: Documenting this @@ -125,10 +125,15 @@ class Client(object): j = graphql_response_to_json(checkRequest(self._post(ReqUrl.GRAPHQL, payload), do_json_check=False)) - if len(j) == 1: - return j[0] - else: - return tuple(j) + return tuple(j) + + def graphql_request(self, query): + """ + 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']] - #entries = self._searchFor(name) - #return [k for k in entries if k.type == ThreadType.PAGE] - def searchForGroups(self, name, limit=1): """ Find and get group thread by its name @@ -510,35 +512,56 @@ class Client(object): j = checkRequest(self._post(ReqUrl.INFO, data)) if not j['payload']['profiles']: - raise Exception('No users returned') + raise Exception('No users/pages returned') entries = {} for _id in j['payload']['profiles']: k = j['payload']['profiles'][_id] 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': - 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: raise Exception('{} had an unknown thread type: {}'.format(_id, k)) + log.debug(entries) return entries def fetchUserInfo(self, *user_ids): """ 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 :return: :class:`models.User` objects, labeled by their ID :rtype: dict :raises: Exception if request failed """ - entries = self._fetchInfo(*user_ids) + threads = self.fetchThreadInfo(*user_ids) users = {} - for k in entries: - if entries[k].type == ThreadType.USER: - users[k] = entries[k] + for k in threads: + if threads[k].type == ThreadType.USER: + users[k] = threads[k] + else: + raise Exception('Thread {} was not a user'.format(threads[k])) return users @@ -546,19 +569,104 @@ class Client(object): """ 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 :return: :class:`models.Page` objects, labeled by their ID :rtype: dict :raises: Exception if request failed """ - entries = self._fetchInfo(*user_ids) - users = {} - for k in entries: - if entries[k].type == ThreadType.PAGE: - users[k] = entries[k] + threads = self.fetchThreadInfo(*page_ids) + pages = {} + for k in threads: + if threads[k].type == ThreadType.PAGE: + 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): """ diff --git a/fbchat/graphql.py b/fbchat/graphql.py index ec0e594..d9e260b 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -23,6 +23,39 @@ class ConcatJSONDecoder(json.JSONDecoder): return objs # 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): if message.get('message_sender') is None: message['message_sender'] = {} @@ -46,6 +79,7 @@ def graphql_to_message(message): def graphql_to_user(user): if user.get('profile_picture') is None: user['profile_picture'] = {} + c_info = get_customization_info(user) return User( user['id'], url=user.get('url'), @@ -54,6 +88,10 @@ def graphql_to_user(user): is_friend=user.get('is_viewer_friend'), gender=GENDERS[user.get('gender')], 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'), name=user.get('name') ) @@ -61,9 +99,13 @@ def graphql_to_user(user): def graphql_to_group(group): if group.get('image') is None: group['image'] = {} + c_info = get_customization_info(group) return Group( group['thread_key']['thread_fbid'], 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'), name=group.get('name') ) @@ -160,6 +202,14 @@ class GraphQL(object): id } } + }, + customization_info { + participant_customizations { + participant_id, + nickname + }, + outgoing_bubble_color, + emoji } } """ diff --git a/fbchat/models.py b/fbchat/models.py index fc4b35b..070e890 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -41,8 +41,16 @@ class User(Thread): gender = str #: From 0 to 1. How close the client is to the user 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`""" super(User, self).__init__(ThreadType.USER, uid, **kwargs) self.url = url @@ -51,16 +59,29 @@ class User(Thread): self.is_friend = is_friend self.gender = gender self.affinity = affinity + self.nickname = nickname + self.own_nickname = own_nickname + self.color = color + self.emoji = emoji class Group(Thread): #: List of the group thread's participant user IDs 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`""" super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs) self.participants = participants + self.nicknames = nicknames + self.color = color + self.emoji = emoji class Page(Thread): diff --git a/fbchat/utils.py b/fbchat/utils.py index 4490c9c..65d054c 100644 --- a/fbchat/utils.py +++ b/fbchat/utils.py @@ -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" ] -TYPES = { - 'Page': ThreadType.PAGE, - 'User': ThreadType.USER, - 'Group': ThreadType.GROUP -} - GENDERS = { # For standard requests 0: 'unknown', diff --git a/tests.py b/tests.py index 1758657..7665f5b 100644 --- a/tests.py +++ b/tests.py @@ -79,7 +79,7 @@ class TestFbchat(unittest.TestCase): users = client.fetchAllUsers() self.assertGreater(len(users), 0) - def test_searchForUsers(self): + def test_searchFor(self): users = client.searchForUsers('Mark Zuckerberg') self.assertGreater(len(users), 0) @@ -92,6 +92,10 @@ class TestFbchat(unittest.TestCase): self.assertEqual(u.url[:4], 'http') self.assertEqual(u.name, 'Mark Zuckerberg') + group_name = client.changeThreadTitle('tést_searchFor', thread_id=group_id, thread_type=ThreadType.GROUP) + groups = client.searchForGroups('té') + self.assertGreater(len(groups), 0) + 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.MEDIUM, thread_id=user_id, thread_type=ThreadType.USER)) @@ -140,10 +144,13 @@ class TestFbchat(unittest.TestCase): self.assertTrue(client.got_qprimer) - def test_fetchUserInfo(self): + def test_fetchInfo(self): info = client.fetchUserInfo('4')['4'] self.assertEqual(info.name, 'Mark Zuckerberg') + info = client.fetchGroupInfo(group_id)[group_id] + self.assertEqual(info.type, ThreadType.GROUP) + def test_removeAddFromGroup(self): client.removeUserFromGroup(user_id, thread_id=group_id) client.addUsersToGroup(user_id, thread_id=group_id)