Compare commits

...

12 Commits

Author SHA1 Message Date
Mads Marquart
64125a1aca Updated to 1.0.6, thanks to @enwar3 2017-06-28 10:24:44 +02:00
Mads Marquart
4feae03092 Merge pull request #169 from OMGWINNING/master
Handle empty participant_customizations field
2017-06-28 10:23:33 +02:00
Joe Lau
5f993c2bf8 Use .get() instead 2017-06-27 16:16:51 -07:00
Joe Lau
35bbcbffba Add __init__.py 2017-06-26 17:54:25 -07:00
Joe Lau
5faca54d67 Handle empty participant_customizations field 2017-06-26 14:16:57 -07:00
Mads Marquart
82496b8e04 Minor fixes 2017-06-26 17:02:32 +02:00
Mads Marquart
2d74ec7823 Made getAllUsers more stable 2017-06-26 15:42:26 +02:00
Mads Marquart
1d42c4d3a6 Updated to 1.0.4, added fetchThread&GroupInfo and improved models 2017-06-26 15:41:58 +02:00
Mads Marquart
4a8ef00442 Fixed a few bugs, updated to v. 1.0.3 2017-06-26 11:37:54 +02:00
Mads Marquart
add06ffa7a I was having trouble with PyPI ;) 2017-06-22 23:35:28 +02:00
Mads Marquart
fbb8d8e24a Update README.rst 2017-06-22 22:54:12 +02:00
Mads Marquart
cd0e001219 Update README.rst 2017-06-22 22:50:10 +02:00
10 changed files with 264 additions and 63 deletions

View File

@@ -9,7 +9,7 @@ fbchat: Facebook Chat (Messenger) for Python
:target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
.. image:: https://readthedocs.org/projects/fbchat/badge/
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
:target: https://fbchat.readthedocs.io
:alt: Documentation

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.
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 `<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.
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:

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))
# 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>']
# 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('<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`

View File

@@ -1,5 +1,10 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from .client import *
"""
fbchat
~~~~~~
@@ -10,11 +15,9 @@
:license: BSD, see LICENSE for more details.
"""
from datetime import datetime
from .client import *
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.0.0'
__version__ = '1.0.6'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'

View File

@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import requests
import urllib
import traceback
from uuid import uuid1
from random import choice
from datetime import datetime
@@ -109,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
@@ -124,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]
"""
@@ -420,7 +426,11 @@ class Client(object):
for key in j['payload']:
k = j['payload'][key]
users.append(User(k['id'], first_name=k['firstName'], url=k['uri'], photo=k['thumbSrc'], name=k['name'], is_friend=k['is_friend'], gender=GENDERS[k['gender']]))
if k['type'] in ['user', 'friend']:
if k['id'] in ['0', 0]:
# Skip invalid users
pass
users.append(User(k['id'], first_name=k.get('firstName'), url=k.get('uri'), photo=k.get('thumbSrc'), name=k.get('name'), is_friend=k.get('is_friend'), gender=GENDERS[k.get('gender')]))
return users
@@ -453,9 +463,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
@@ -508,35 +515,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
@@ -544,19 +572,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):
"""
@@ -626,7 +739,7 @@ class Client(object):
raise Exception('A thread was not in participants: {}'.format(j['payload']))
entries.append(participants[k['other_user_fbid']])
elif k['thread_type'] == 2:
entries.append(Group(k['thread_fbid'], participants=[p.strip('fbid:') for p in k['participants']], photo=k['image_src'], name=k['name']))
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name']))
else:
raise Exception('A thread had an unknown thread type: {}'.format(k))
@@ -1175,7 +1288,7 @@ class Client(object):
# Color change
elif delta_type == "change_thread_theme":
new_color = ThreadColor(delta["untypedData"]["theme_color"])
new_color = graphql_color_to_enum(delta["untypedData"]["theme_color"])
thread_id, thread_type = getThreadIdAndThreadType(metadata)
self.onColorChange(mid=mid, author_id=author_id, new_color=new_color, thread_id=thread_id,
thread_type=thread_type, ts=ts, metadata=metadata, msg=m)
@@ -1237,7 +1350,7 @@ class Client(object):
threads = [getThreadIdAndThreadType({"threadKey": thr}) for thr in delta.get("threadKeys")]
# thread_id, thread_type = getThreadIdAndThreadType(delta)
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, delivered_ts=delivered_ts, metadata=delta, msg=m)
self.onMarkedSeen(threads=threads, seen_ts=seen_ts, ts=delivered_ts, metadata=delta, msg=m)
continue
# New message
@@ -1298,9 +1411,6 @@ class Client(object):
Does one cycle of the listening loop.
This method is useful if you want to control fbchat from an external event loop
.. note::
markAlive is currently broken, and is ignored
:param markAlive: Whether this should ping the Facebook server before running
:type markAlive: bool
:return: Whether the loop should keep running
@@ -1380,7 +1490,7 @@ class Client(object):
:param exception: The exception that was encountered
"""
raise exception
traceback.print_exc()
def onMessage(self, mid=None, author_id=None, message=None, thread_id=None, thread_type=ThreadType.USER, ts=None, metadata=None, msg={}):
@@ -1558,7 +1668,7 @@ class Client(object):
"""
log.info('Inbox event: {}, {}, {}'.format(unseen, unread, recent_unread))
def onQprimer(self, made=None, msg={}):
def onQprimer(self, ts=None, msg={}):
"""
Called when the client just started listening

View File

@@ -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')
elif info.get('participant_customizations'):
_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']],
participants=set([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
}
}
"""

View File

@@ -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
#: Unique list (set) of the group thread's participant user IDs
participants = set
#: 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=set(), 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):

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"
]
TYPES = {
'Page': ThreadType.PAGE,
'User': ThreadType.USER,
'Group': ThreadType.GROUP
}
GENDERS = {
# For standard requests
0: 'unknown',
@@ -108,17 +102,17 @@ def now():
def strip_to_json(text):
try:
return text[text.index('{'):]
except ValueError as e:
return None
except ValueError:
raise Exception('No JSON object found: {}, {}'.format(repr(text), text.index('{')))
def get_decoded(r):
if not isinstance(r._content, str):
return r._content.decode(facebookEncoding)
else:
return r._content
def get_decoded_r(r):
return get_decoded(r._content)
def get_decoded(content):
return content.decode(facebookEncoding)
def get_json(r):
return json.loads(strip_to_json(get_decoded(r)))
return json.loads(strip_to_json(get_decoded_r(r)))
def digitToChar(digit):
if digit < 10:
@@ -162,16 +156,17 @@ def checkRequest(r, do_json_check=True):
if not r.ok:
raise Exception('Error when sending request: Got {} response'.format(r.status_code))
content = get_decoded(r)
content = get_decoded_r(r)
if content is None or len(content) == 0:
raise Exception('Error when sending request: Got empty response')
if do_json_check:
content = strip_to_json(content)
try:
j = json.loads(strip_to_json(content))
j = json.loads(content)
except Exception as e:
raise Exception('Error while parsing JSON: {}'.format(repr(content)))
raise Exception('Error while parsing JSON: {}'.format(repr(content)), e)
check_json(j)
return j
else:

View File

@@ -16,7 +16,10 @@ except ImportError:
with open('README.rst') as f:
readme_content = f.read().strip()
requirements = [line.rstrip('\n') for line in open('requirements.txt')]
try:
requirements = [line.rstrip('\n') for line in open(os.path.join('fbchat.egg-info', 'requires.txt'))]
except FileNotFoundError:
requirements = [line.rstrip('\n') for line in open('requirements.txt')]
version = None
author = None

View File

@@ -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('')
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)