Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
884af48270 | ||
|
95f018fad3 | ||
|
b44758a195 | ||
|
f1c20d490e | ||
|
04372d498e | ||
|
63ea899605 | ||
|
4fdd145d1e | ||
|
57ee68b0e0 | ||
|
99c6884681 | ||
|
1c1438e9bc | ||
|
22f1b3e489 | ||
|
fb1ad5800c | ||
|
4dd15b05ef | ||
|
d7cdb644c4 | ||
|
bfcf4950b3 | ||
|
6612c97f05 | ||
|
b92cf62726 | ||
|
a53ba33a81 | ||
|
c04d38cf63 | ||
|
a051adcbc0 | ||
|
900a9cdf72 | ||
|
611b329934 | ||
|
2642788bc1 | ||
|
8268445f0b | ||
|
c12dcd9263 | ||
|
3142524809 | ||
|
4c9d3bd9d7 | ||
|
ba103066b8 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -24,7 +24,8 @@ develop-eggs
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# Data for tests
|
||||
# Scripts and data for tests
|
||||
my_tests.py
|
||||
my_test_data.json
|
||||
my_data.json
|
||||
tests.data
|
||||
tests.data
|
||||
|
@@ -5,8 +5,8 @@ from fbchat import log, Client
|
||||
# Subclass fbchat.Client and override required methods
|
||||
class EchoBot(Client):
|
||||
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
|
||||
self.markAsDelivered(author_id, thread_id)
|
||||
self.markAsRead(author_id)
|
||||
self.markAsDelivered(thread_id, message_object.uid)
|
||||
self.markAsRead(thread_id)
|
||||
|
||||
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
|
||||
|
||||
|
@@ -17,7 +17,7 @@ from .client import *
|
||||
|
||||
|
||||
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
|
||||
__version__ = '1.3.0'
|
||||
__version__ = '1.3.6'
|
||||
__license__ = 'BSD'
|
||||
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
|
||||
__email__ = 'carpedm20@gmail.com'
|
||||
|
135
fbchat/client.py
135
fbchat/client.py
@@ -20,6 +20,8 @@ class Client(object):
|
||||
See https://fbchat.readthedocs.io for complete documentation of the API.
|
||||
"""
|
||||
|
||||
ssl_verify = True
|
||||
"""Verify ssl certificate, set to False to allow debugging with a proxy"""
|
||||
listening = False
|
||||
"""Whether the client is listening. Used when creating an external event loop to determine when to stop listening"""
|
||||
uid = None
|
||||
@@ -105,7 +107,7 @@ class Client(object):
|
||||
|
||||
def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
|
||||
payload = self._generatePayload(query)
|
||||
r = self._session.get(url, headers=self._header, params=payload, timeout=timeout)
|
||||
r = self._session.get(url, headers=self._header, params=payload, timeout=timeout, verify=self.ssl_verify)
|
||||
if not fix_request:
|
||||
return r
|
||||
try:
|
||||
@@ -117,7 +119,7 @@ class Client(object):
|
||||
|
||||
def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
|
||||
payload = self._generatePayload(query)
|
||||
r = self._session.post(url, headers=self._header, data=payload, timeout=timeout)
|
||||
r = self._session.post(url, headers=self._header, data=payload, timeout=timeout, verify=self.ssl_verify)
|
||||
if not fix_request:
|
||||
return r
|
||||
try:
|
||||
@@ -137,17 +139,17 @@ class Client(object):
|
||||
raise e
|
||||
|
||||
def _cleanGet(self, url, query=None, timeout=30):
|
||||
return self._session.get(url, headers=self._header, params=query, timeout=timeout)
|
||||
return self._session.get(url, headers=self._header, params=query, timeout=timeout, verify=self.ssl_verify)
|
||||
|
||||
def _cleanPost(self, url, query=None, timeout=30):
|
||||
self.req_counter += 1
|
||||
return self._session.post(url, headers=self._header, data=query, timeout=timeout)
|
||||
return self._session.post(url, headers=self._header, data=query, timeout=timeout, verify=self.ssl_verify)
|
||||
|
||||
def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
|
||||
payload=self._generatePayload(query)
|
||||
# Removes 'Content-Type' from the header
|
||||
headers = dict((i, self._header[i]) for i in self._header if i != 'Content-Type')
|
||||
r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files)
|
||||
r = self._session.post(url, headers=headers, data=payload, timeout=timeout, files=files, verify=self.ssl_verify)
|
||||
if not fix_request:
|
||||
return r
|
||||
try:
|
||||
@@ -754,19 +756,23 @@ class Client(object):
|
||||
|
||||
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']]))
|
||||
|
||||
def fetchThreadList(self, offset=0, limit=20, thread_location=ThreadLocation.INBOX):
|
||||
def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None):
|
||||
"""Get thread list of your facebook account
|
||||
|
||||
:param offset: The offset, from where in the list to recieve threads from
|
||||
:param offset: Deprecated. Do not use!
|
||||
:param limit: Max. number of threads to retrieve. Capped at 20
|
||||
:param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER
|
||||
:type offset: int
|
||||
:param before: A timestamp (in milliseconds), indicating from which point to retrieve threads
|
||||
:type limit: int
|
||||
:type before: int
|
||||
:return: :class:`models.Thread` objects
|
||||
:rtype: list
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
|
||||
if offset is not None:
|
||||
log.warning('Using `offset` in `fetchThreadList` is no longer supported, since Facebook migrated to the use of GraphQL in this request. Use `before` instead')
|
||||
|
||||
if limit > 20 or limit < 1:
|
||||
raise FBchatUserError('`limit` should be between 1 and 20')
|
||||
|
||||
@@ -775,73 +781,46 @@ class Client(object):
|
||||
else:
|
||||
raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
|
||||
|
||||
data = {
|
||||
'client' : self.client,
|
||||
loc_str + '[offset]' : offset,
|
||||
loc_str + '[limit]' : limit,
|
||||
}
|
||||
j = self.graphql_request(GraphQL(doc_id='1349387578499440', params={
|
||||
'limit': limit,
|
||||
'tags': [loc_str],
|
||||
'before': before,
|
||||
'includeDeliveryReceipts': True,
|
||||
'includeSeqID': False
|
||||
}))
|
||||
|
||||
j = self._post(self.req_url.THREADS, data, fix_request=True, as_json=True)
|
||||
if j.get('payload') is None:
|
||||
raise FBchatException('Missing payload: {}, with data: {}'.format(j, data))
|
||||
|
||||
participants = {}
|
||||
if 'participants' in j['payload']:
|
||||
for p in j['payload']['participants']:
|
||||
if p['type'] == 'page':
|
||||
participants[p['fbid']] = Page(p['fbid'], url=p['href'], photo=p['image_src'], name=p['name'])
|
||||
elif p['type'] == 'user':
|
||||
participants[p['fbid']] = User(p['fbid'], url=p['href'], first_name=p['short_name'], is_friend=p['is_friend'], gender=GENDERS.get(p['gender']), photo=p['image_src'], name=p['name'])
|
||||
else:
|
||||
raise FBchatException('A participant had an unknown type {}: {}'.format(p['type'], p))
|
||||
|
||||
entries = []
|
||||
if 'threads' in j['payload']:
|
||||
for k in j['payload']['threads']:
|
||||
if k['thread_type'] == 1:
|
||||
if k['other_user_fbid'] not in participants:
|
||||
raise FBchatException('The thread {} was not in participants: {}'.format(k, j['payload']))
|
||||
participants[k['other_user_fbid']].message_count = k['message_count']
|
||||
entries.append(participants[k['other_user_fbid']])
|
||||
elif k['thread_type'] == 2:
|
||||
entries.append(Group(k['thread_fbid'], participants=set([p.strip('fbid:') for p in k['participants']]), photo=k['image_src'], name=k['name'], message_count=k['message_count']))
|
||||
elif k['thread_type'] == 3:
|
||||
entries.append(Room(
|
||||
k['thread_fbid'],
|
||||
participants = set(p.lstrip('fbid:') for p in k['participants']),
|
||||
photo = k['image_src'],
|
||||
name = k['name'],
|
||||
message_count = k['message_count'],
|
||||
admins = set(p.lstrip('fbid:') for p in k['admin_ids']),
|
||||
approval_mode = k['approval_mode'],
|
||||
approval_requests = set(p.lstrip('fbid:') for p in k['approval_queue_ids']),
|
||||
join_link = k['joinable_mode']['link']
|
||||
))
|
||||
else:
|
||||
raise FBchatException('A thread had an unknown thread type: {}'.format(k))
|
||||
|
||||
return entries
|
||||
return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']]
|
||||
|
||||
def fetchUnread(self):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
Get the unread thread list
|
||||
|
||||
:return: List of unread thread ids
|
||||
:rtype: list
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
form = {
|
||||
'client': 'mercury_sync',
|
||||
'folders[0]': 'inbox',
|
||||
'client': 'mercury',
|
||||
'last_action_timestamp': now() - 60*1000
|
||||
# 'last_action_timestamp': 0
|
||||
}
|
||||
|
||||
j = self._post(self.req_url.THREAD_SYNC, form, fix_request=True, as_json=True)
|
||||
j = self._post(self.req_url.UNREAD_THREADS, form, fix_request=True, as_json=True)
|
||||
|
||||
return {
|
||||
"message_counts": j['payload']['message_counts'],
|
||||
"unseen_threads": j['payload']['unseen_thread_ids']
|
||||
}
|
||||
return j['payload']['unread_thread_fbids'][0]['other_user_fbids']
|
||||
|
||||
def fetchUnseen(self):
|
||||
"""
|
||||
Get the unseen (new) thread list
|
||||
|
||||
:return: List of unseen thread ids
|
||||
:rtype: list
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
j = self._post(self.req_url.UNSEEN_THREADS, None, fix_request=True, as_json=True)
|
||||
|
||||
return j['payload']['unseen_thread_fbids'][0]['other_user_fbids']
|
||||
|
||||
def fetchImageUrl(self, image_id):
|
||||
"""Fetches the url to the original image from an image attachment ID
|
||||
@@ -1263,28 +1242,36 @@ class Client(object):
|
||||
END SEND METHODS
|
||||
"""
|
||||
|
||||
def markAsDelivered(self, userID, threadID):
|
||||
def markAsDelivered(self, thread_id, message_id):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
Mark a message as delivered
|
||||
|
||||
:param thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads`
|
||||
:param message_id: Message ID to set as delivered. See :ref:`intro_threads`
|
||||
:return: Whether the request was successful
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
data = {
|
||||
"message_ids[0]": threadID,
|
||||
"thread_ids[%s][0]" % userID: threadID
|
||||
"message_ids[0]": message_id,
|
||||
"thread_ids[%s][0]" % thread_id: message_id
|
||||
}
|
||||
|
||||
r = self._post(self.req_url.DELIVERED, data)
|
||||
return r.ok
|
||||
|
||||
def markAsRead(self, userID):
|
||||
def markAsRead(self, thread_id):
|
||||
"""
|
||||
.. todo::
|
||||
Documenting this
|
||||
Mark a thread as read
|
||||
All messages inside the thread will be marked as read
|
||||
|
||||
:param thread_id: User/Group ID to set as read. See :ref:`intro_threads`
|
||||
:return: Whether the request was successful
|
||||
:raises: FBchatException if request failed
|
||||
"""
|
||||
data = {
|
||||
"ids[%s]" % thread_id: 'true',
|
||||
"watermarkTimestamp": now(),
|
||||
"shouldSendReadReceipt": True,
|
||||
"ids[%s]" % userID: True
|
||||
"shouldSendReadReceipt": 'true',
|
||||
}
|
||||
|
||||
r = self._post(self.req_url.READ_STATUS, data)
|
||||
@@ -1543,9 +1530,9 @@ class Client(object):
|
||||
#
|
||||
# self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
|
||||
|
||||
# elif mtype in ['jewel_requests_add']:
|
||||
# from_id = m['from']
|
||||
# self.on_friend_request(from_id)
|
||||
elif mtype in ['jewel_requests_add']:
|
||||
from_id = m['from']
|
||||
self.onFriendRequest(from_id=from_id, msg=m)
|
||||
|
||||
# Happens on every login
|
||||
elif mtype == "qprimer":
|
||||
|
@@ -172,10 +172,46 @@ def graphql_to_user(user):
|
||||
message_count=user.get('messages_count')
|
||||
)
|
||||
|
||||
def graphql_to_thread(thread):
|
||||
if thread['thread_type'] == 'GROUP':
|
||||
return graphql_to_group(thread)
|
||||
elif thread['thread_type'] == 'ONE_TO_ONE':
|
||||
if thread.get('big_image_src') is None:
|
||||
thread['big_image_src'] = {}
|
||||
c_info = get_customization_info(thread)
|
||||
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']]
|
||||
user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
|
||||
last_message_timestamp = None
|
||||
if 'last_message' in thread:
|
||||
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
|
||||
|
||||
return User(
|
||||
user['id'],
|
||||
url=user.get('url'),
|
||||
name=user.get('name'),
|
||||
first_name=user.get('short_name'),
|
||||
last_name=user.get('name').split(user.get('short_name'),1)[1].strip(),
|
||||
is_friend=user.get('is_viewer_friend'),
|
||||
gender=GENDERS.get(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['big_image_src'].get('uri'),
|
||||
message_count=thread.get('messages_count'),
|
||||
last_message_timestamp=last_message_timestamp
|
||||
)
|
||||
else:
|
||||
raise FBchatException('Unknown thread type: {}, with data: {}'.format(thread.get('thread_type'), thread))
|
||||
|
||||
def graphql_to_group(group):
|
||||
if group.get('image') is None:
|
||||
group['image'] = {}
|
||||
c_info = get_customization_info(group)
|
||||
last_message_timestamp = None
|
||||
if 'last_message' in group:
|
||||
last_message_timestamp = group['last_message']['nodes'][0]['timestamp_precise']
|
||||
return Group(
|
||||
group['thread_key']['thread_fbid'],
|
||||
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]),
|
||||
@@ -184,7 +220,8 @@ def graphql_to_group(group):
|
||||
emoji=c_info.get('emoji'),
|
||||
photo=group['image'].get('uri'),
|
||||
name=group.get('name'),
|
||||
message_count=group.get('messages_count')
|
||||
message_count=group.get('messages_count'),
|
||||
last_message_timestamp=last_message_timestamp
|
||||
)
|
||||
|
||||
def graphql_to_room(room):
|
||||
|
@@ -451,10 +451,10 @@ class ThreadType(Enum):
|
||||
|
||||
class ThreadLocation(Enum):
|
||||
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
|
||||
INBOX = 'inbox'
|
||||
PENDING = 'pending'
|
||||
ARCHIVED = 'action:archived'
|
||||
OTHER = 'other'
|
||||
INBOX = 'INBOX'
|
||||
PENDING = 'PENDING'
|
||||
ARCHIVED = 'ARCHIVED'
|
||||
OTHER = 'OTHER'
|
||||
|
||||
class TypingStatus(Enum):
|
||||
"""Used to specify whether the user is typing or has stopped typing"""
|
||||
|
@@ -9,6 +9,13 @@ import warnings
|
||||
import logging
|
||||
from .models import *
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
basestring = (str, bytes)
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
basestring = basestring
|
||||
|
||||
# Python 2's `input` executes the input, whereas `raw_input` just returns the input
|
||||
try:
|
||||
input = raw_input
|
||||
@@ -87,7 +94,8 @@ class ReqUrl(object):
|
||||
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
|
||||
LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
|
||||
SEND = "https://www.facebook.com/messaging/send/"
|
||||
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php"
|
||||
UNREAD_THREADS = "https://www.facebook.com/ajax/mercury/unread_threads.php"
|
||||
UNSEEN_THREADS = "https://www.facebook.com/mercury/unseen_thread_ids/"
|
||||
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
|
||||
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
|
||||
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"
|
||||
|
2
setup.py
2
setup.py
@@ -54,10 +54,8 @@ setup(
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Information Technology',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
|
3
tests.py
3
tests.py
@@ -123,7 +123,8 @@ class TestFbchat(unittest.TestCase):
|
||||
self.assertTrue(client.sendLocalImage(image_local_url, Message(text='test_send_image_local_to__@you★', mentions=mentions)))
|
||||
|
||||
def test_fetchThreadList(self):
|
||||
client.fetchThreadList(offset=0, limit=20)
|
||||
threads = client.fetchThreadList(limit=2)
|
||||
self.assertEqual(len(threads), 2)
|
||||
|
||||
def test_fetchThreadMessages(self):
|
||||
for thread in threads:
|
||||
|
Reference in New Issue
Block a user