Skip to content

Commit

Permalink
Merge pull request #269 from gave92/FetchUnread
Browse files Browse the repository at this point in the history
Fix `markAsRead` and `fetchUnread`; fixes #261
Added the `ssl_verify` instance variable, which allows disabling SSL varification for proxies
  • Loading branch information
madsmtm authored Mar 19, 2018
2 parents fb1ad58 + 63ea899 commit 04372d4
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 25 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
64 changes: 42 additions & 22 deletions fbchat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -105,7 +107,7 @@ def _fix_fb_errors(self, error_code):

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=encode_params(payload), timeout=timeout, verify=self.ssl_verify)
if not fix_request:
return r
try:
Expand All @@ -117,7 +119,7 @@ def _get(self, url, query=None, timeout=30, fix_request=False, as_json=False, er

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=encode_params(payload), timeout=timeout, verify=self.ssl_verify)
if not fix_request:
return r
try:
Expand All @@ -137,17 +139,17 @@ def _graphql(self, payload, error_retries=3):
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=encode_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=encode_params(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=encode_params(payload), timeout=timeout, files=files, verify=self.ssl_verify)
if not fix_request:
return r
try:
Expand Down Expand Up @@ -791,24 +793,34 @@ def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.

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
Expand Down Expand Up @@ -1230,28 +1242,36 @@ def setTypingStatus(self, status, thread_id=None, thread_type=None):
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
}

r = self._post(self.req_url.READ_STATUS, data)
Expand Down
42 changes: 41 additions & 1 deletion fbchat/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -225,3 +233,35 @@ def get_emojisize_from_tags(tags):
except (KeyError, IndexError):
log.exception('Could not determine emoji size from {} - {}'.format(tags, tmp))
return None

def encode_params(data):
"""Encode parameters in a piece of data.
Will successfully encode parameters when passed as a dict or a list of
2-tuples. Order is retained if data is a list of 2-tuples but arbitrary
if parameters are supplied as a dict.
"""

if isinstance(data, (str, bytes)):
return data
elif hasattr(data, 'read'):
return data
elif hasattr(data, '__iter__'):
result = []
for k, vs in list(data.items()):
if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):
vs = [vs]
for v in vs:
if v is not None:
if isinstance(v, bool):
result.append(
(k.encode('utf-8') if isinstance(k, str) else k,
str(v).lower()))
else:
result.append(
(k.encode('utf-8') if isinstance(k, str) else k,
v.encode('utf-8') if isinstance(v, str) else v))
return urlencode(result, doseq=True)
else:
return data

0 comments on commit 04372d4

Please sign in to comment.