Compare commits

..

25 Commits

Author SHA1 Message Date
Mads Marquart
884af48270 Version up, thanks to @gave92
Properly fixed `markAsRead`, @gave92  reminded me that I forgot to change the `True` to `'true'` when removing `encode_params`
2018-03-21 10:05:07 +01:00
Mads Marquart
95f018fad3 Fixed example Echobot 2018-03-19 21:40:51 +01:00
Mads Marquart
b44758a195 Version up, thanks to @gave92
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:28:48 +01:00
Mads Marquart
f1c20d490e Removed encode_params from PR, as discussed in #269 2018-03-19 21:15:23 +01:00
Mads Marquart
04372d498e Merge pull request #269 from gave92/FetchUnread
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
2018-03-19 21:08:45 +01:00
Marco Gavelli
63ea899605 fix for python3 2018-03-19 20:47:41 +01:00
Marco Gavelli
4fdd145d1e verify in _postFile 2018-03-19 16:52:22 +01:00
Marco Gavelli
57ee68b0e0 added documentation to markAsRead 2018-03-19 16:38:19 +01:00
Marco Gavelli
99c6884681 added documentation to fetchUnread 2018-03-19 16:29:26 +01:00
Marco Gavelli
1c1438e9bc fix for markAsRead, fetchUnread 2018-03-18 11:18:46 +01:00
Marco Gavelli
22f1b3e489 fix FetchUnread 2018-03-17 19:32:45 +01:00
Mads Marquart
fb1ad5800c Minor fix for searchFor. See comments on #266 2018-03-05 22:07:16 +01:00
Taehoon Kim
4dd15b05ef version up thanks to @2FWAH's PR #266 #267 2018-03-03 22:49:25 +09:00
Taehoon Kim
d7cdb644c4 Merge pull request #265 from 2FWAH/fix-fetchThreadList-archived
Fix ThreadLocation to work with new GraphQL and archived threads
2018-03-03 22:22:21 +09:00
Taehoon Kim
bfcf4950b3 Merge pull request #266 from 2FWAH/fill-last_message_timestamp-in-fetchThreadList
Add last_message_timestamp support
2018-03-03 22:21:49 +09:00
Taehoon Kim
6612c97f05 Merge pull request #267 from danijeljw/patch-1
duplicate lines removed from setup
2018-03-03 22:20:28 +09:00
Danijel-James Wynyard
b92cf62726 duplicate lines removed 2018-03-03 12:08:05 +11:00
2FWAH
a53ba33a81 Set offset to 'None' by default 2018-02-23 09:23:34 +01:00
2FWAH
c04d38cf63 Handle last_message_timestamp
Set last_message_timestamp for one to one and group conversations.
2018-02-22 19:53:56 +01:00
2FWAH
a051adcbc0 Fix ThreadLocation to work with new GraphQL 2018-02-22 17:49:26 +01:00
Mads Marquart
900a9cdf72 Version up, thanks to @gave92
`fetchThreadList` is updated with a GraphQL implementation. See #241
2018-02-18 22:40:13 +01:00
Mads Marquart
611b329934 Merge pull request #259 from gave92/fetchThreadListGraphQL
Added GraphQL alternative to fetchThreadList; fixes #241
2018-02-18 22:36:23 +01:00
Mads Marquart
2642788bc1 Merged fetchThreadListGraphQL into fetchThreadList 2018-02-18 22:32:12 +01:00
Marco Gavelli
8268445f0b Changed return type for ONE_TO_ONE to User 2018-02-18 22:49:47 +01:00
Marco Gavelli
c12dcd9263 Added GraphQL alternative to fetchThreadList; fixes #241 2018-02-17 14:29:31 +01:00
9 changed files with 117 additions and 85 deletions

5
.gitignore vendored
View File

@@ -24,7 +24,8 @@ develop-eggs
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# Data for tests # Scripts and data for tests
my_tests.py
my_test_data.json my_test_data.json
my_data.json my_data.json
tests.data tests.data

View File

@@ -5,8 +5,8 @@ from fbchat import log, Client
# Subclass fbchat.Client and override required methods # Subclass fbchat.Client and override required methods
class EchoBot(Client): class EchoBot(Client):
def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs): def onMessage(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.markAsDelivered(author_id, thread_id) self.markAsDelivered(thread_id, message_object.uid)
self.markAsRead(author_id) self.markAsRead(thread_id)
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name)) log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))

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.3.1' __version__ = '1.3.6'
__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

@@ -20,6 +20,8 @@ class Client(object):
See https://fbchat.readthedocs.io for complete documentation of the API. 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 listening = False
"""Whether the client is listening. Used when creating an external event loop to determine when to stop listening""" """Whether the client is listening. Used when creating an external event loop to determine when to stop listening"""
uid = None 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): def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query) 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: if not fix_request:
return r return r
try: 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): def _post(self, url, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload = self._generatePayload(query) 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: if not fix_request:
return r return r
try: try:
@@ -137,17 +139,17 @@ class Client(object):
raise e raise e
def _cleanGet(self, url, query=None, timeout=30): 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): def _cleanPost(self, url, query=None, timeout=30):
self.req_counter += 1 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): def _postFile(self, url, files=None, query=None, timeout=30, fix_request=False, as_json=False, error_retries=3):
payload=self._generatePayload(query) payload=self._generatePayload(query)
# Removes 'Content-Type' from the header # Removes 'Content-Type' from the header
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')
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: if not fix_request:
return r return r
try: try:
@@ -754,19 +756,23 @@ class Client(object):
return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) 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 """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 limit: Max. number of threads to retrieve. Capped at 20
:param thread_location: models.ThreadLocation: INBOX, PENDING, ARCHIVED or OTHER :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 limit: int
:type before: int
:return: :class:`models.Thread` objects :return: :class:`models.Thread` objects
:rtype: list :rtype: list
:raises: FBchatException if request failed :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: if limit > 20 or limit < 1:
raise FBchatUserError('`limit` should be between 1 and 20') raise FBchatUserError('`limit` should be between 1 and 20')
@@ -775,73 +781,46 @@ class Client(object):
else: else:
raise FBchatUserError('"thread_location" must be a value of ThreadLocation') raise FBchatUserError('"thread_location" must be a value of ThreadLocation')
data = { j = self.graphql_request(GraphQL(doc_id='1349387578499440', params={
'client' : self.client, 'limit': limit,
loc_str + '[offset]' : offset, 'tags': [loc_str],
loc_str + '[limit]' : limit, 'before': before,
} 'includeDeliveryReceipts': True,
'includeSeqID': False
}))
j = self._post(self.req_url.THREADS, data, fix_request=True, as_json=True) return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']]
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
def fetchUnread(self): def fetchUnread(self):
""" """
.. todo:: Get the unread thread list
Documenting this
:return: List of unread thread ids
:rtype: list
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
form = { form = {
'client': 'mercury_sync',
'folders[0]': 'inbox', 'folders[0]': 'inbox',
'client': 'mercury',
'last_action_timestamp': now() - 60*1000 'last_action_timestamp': now() - 60*1000
# 'last_action_timestamp': 0 # '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 { return j['payload']['unread_thread_fbids'][0]['other_user_fbids']
"message_counts": j['payload']['message_counts'],
"unseen_threads": j['payload']['unseen_thread_ids'] 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): def fetchImageUrl(self, image_id):
"""Fetches the url to the original image from an image attachment ID """Fetches the url to the original image from an image attachment ID
@@ -1263,28 +1242,36 @@ class Client(object):
END SEND METHODS END SEND METHODS
""" """
def markAsDelivered(self, userID, threadID): def markAsDelivered(self, thread_id, message_id):
""" """
.. todo:: Mark a message as delivered
Documenting this
: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 = { data = {
"message_ids[0]": threadID, "message_ids[0]": message_id,
"thread_ids[%s][0]" % userID: threadID "thread_ids[%s][0]" % thread_id: message_id
} }
r = self._post(self.req_url.DELIVERED, data) r = self._post(self.req_url.DELIVERED, data)
return r.ok return r.ok
def markAsRead(self, userID): def markAsRead(self, thread_id):
""" """
.. todo:: Mark a thread as read
Documenting this 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 = { data = {
"ids[%s]" % thread_id: 'true',
"watermarkTimestamp": now(), "watermarkTimestamp": now(),
"shouldSendReadReceipt": True, "shouldSendReadReceipt": 'true',
"ids[%s]" % userID: True
} }
r = self._post(self.req_url.READ_STATUS, data) r = self._post(self.req_url.READ_STATUS, data)

View File

@@ -172,10 +172,46 @@ def graphql_to_user(user):
message_count=user.get('messages_count') 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): 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) 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( return Group(
group['thread_key']['thread_fbid'], group['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in group['all_participants']['nodes']]), 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'), emoji=c_info.get('emoji'),
photo=group['image'].get('uri'), photo=group['image'].get('uri'),
name=group.get('name'), 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): def graphql_to_room(room):

View File

@@ -451,10 +451,10 @@ class ThreadType(Enum):
class ThreadLocation(Enum): class ThreadLocation(Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other).""" """Used to specify where a thread is located (inbox, pending, archived, other)."""
INBOX = 'inbox' INBOX = 'INBOX'
PENDING = 'pending' PENDING = 'PENDING'
ARCHIVED = 'action:archived' ARCHIVED = 'ARCHIVED'
OTHER = 'other' OTHER = 'OTHER'
class TypingStatus(Enum): class TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing""" """Used to specify whether the user is typing or has stopped typing"""

View File

@@ -9,6 +9,13 @@ import warnings
import logging import logging
from .models import * 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 # Python 2's `input` executes the input, whereas `raw_input` just returns the input
try: try:
input = raw_input input = raw_input
@@ -87,7 +94,8 @@ class ReqUrl(object):
SEARCH = "https://www.facebook.com/ajax/typeahead/search.php" SEARCH = "https://www.facebook.com/ajax/typeahead/search.php"
LOGIN = "https://m.facebook.com/login.php?login_attempt=1" LOGIN = "https://m.facebook.com/login.php?login_attempt=1"
SEND = "https://www.facebook.com/messaging/send/" 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" THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php" MESSAGES = "https://www.facebook.com/ajax/mercury/thread_info.php"
READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php" READ_STATUS = "https://www.facebook.com/ajax/mercury/change_read_status.php"

View File

@@ -54,10 +54,8 @@ setup(
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology', 'Intended Audience :: Information Technology',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',

View File

@@ -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))) self.assertTrue(client.sendLocalImage(image_local_url, Message(text='test_send_image_local_to__@you★', mentions=mentions)))
def test_fetchThreadList(self): def test_fetchThreadList(self):
client.fetchThreadList(offset=0, limit=20) threads = client.fetchThreadList(limit=2)
self.assertEqual(len(threads), 2)
def test_fetchThreadMessages(self): def test_fetchThreadMessages(self):
for thread in threads: for thread in threads: