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)