Compare commits

...

22 Commits

Author SHA1 Message Date
Mads Marquart
38f66147cb Version up, thanks to @orenyomtov and @Abhinav2812
Also fixed `Client.isLoggedIn`
2018-05-18 17:35:03 +02:00
Mads Marquart
ffa26c20b5 Merge branch 'patch-1' 2018-05-18 16:58:32 +02:00
Abhinav2812
430ada7f84 Resolve FBChatException
Resolve the error `fbchat.models.FBchatException: Could not get ThreadColor from color: FF0084FF` when threadcolor is set to default (MESSENGER_BLUE)
2018-05-16 17:54:37 +05:30
Mads Marquart
988e37eb42 Merge remote-tracking branch 'orenyomtov/patch-3' 2018-05-08 16:51:03 +02:00
Mads Marquart
1938b90bce Merge remote-tracking branch 'orenyomtov/patch-2' 2018-05-08 16:50:56 +02:00
Mads Marquart
f61d1403f3 Merge remote-tracking branch 'orenyomtov/patch-1' 2018-05-08 16:50:48 +02:00
Oren
d228f34f64 Eliminate an unnecessary HTTP request during login
This change eliminates requesting and downloading the entire FB home page (~160kb) every login.
2018-05-08 15:40:46 +03:00
Oren
97049556ed Update obtaining fb_dtsg and fb_h
fb_dtsg is sometimes returned inside an HTML comment, and beautifulsoup can't find it - in that case we'll use a regular expression to extract it.

fb_h is sometimes not returned in the HTML of req_url.BASE (in my experience, when resuming a session using session_cookies).

Following the discussion here:
https://github.com/Schmavery/facebook-chat-api/issues/505
I learned it is used for logging out, and can be found in the response of `https://www.facebook.com/bluebar/modern_settings_menu/`.

I included support for fetching it from there.

Because this library is used many more times for logging in, than for logging out, instead of adding an extra HTTP request during login, I decided to perform it during logout, only in case fb_h is not found in the HTML of req_url.BASE.
2018-05-08 12:41:22 +03:00
Oren
b64c6a94cc Add MODERN_SETTINGS_MENU url to ReqUrl
It is used to obtain the fb_h value
2018-05-08 12:18:15 +03:00
Oren
edc655bae7 Fix IndexError: list index out of range bug
When the returned `short_name` is null, `fbchat` throws an exception:

```python
  File "/usr/local/lib/python2.7/site-packages/fbchat/client.py", line 792, in fetchThreadList
    return [graphql_to_thread(node) for node in j['viewer']['message_threads']['nodes']]
  File "/usr/local/lib/python2.7/site-packages/fbchat/graphql.py", line 193, in graphql_to_thread
    last_name=user.get('name').split(user.get('short_name'),1)[1].strip(),
IndexError: list index out of range
```

This commit fixes that scenario by accessing the last item in the list via `.pop()` instead of via `[1]`
2018-05-07 19:50:43 +03:00
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
8 changed files with 92 additions and 39 deletions

8
.gitignore vendored
View File

@@ -24,7 +24,11 @@ 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
# Virtual environment
venv/

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.3' __version__ = '1.3.7'
__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:
@@ -136,18 +138,19 @@ class Client(object):
return self._graphql(payload, error_retries=error_retries-1) return self._graphql(payload, error_retries=error_retries-1)
raise e raise e
def _cleanGet(self, url, query=None, timeout=30): def _cleanGet(self, url, query=None, timeout=30, allow_redirects=True):
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,
allow_redirects=allow_redirects)
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:
@@ -207,8 +210,18 @@ class Client(object):
r = self._get(self.req_url.BASE) r = self._get(self.req_url.BASE)
soup = bs(r.text, "lxml") soup = bs(r.text, "lxml")
self.fb_dtsg = soup.find("input", {'name':'fb_dtsg'})['value']
self.fb_h = soup.find("input", {'name':'h'})['value'] fb_dtsg_element = soup.find("input", {'name': 'fb_dtsg'})
if fb_dtsg_element:
self.fb_dtsg = fb_dtsg_element['value']
else:
self.fb_dtsg = re.search(r'name="fb_dtsg" value="(.*?)"', r.text).group(1)
fb_h_element = soup.find("input", {'name':'h'})
if fb_h_element:
self.fb_h = fb_h_element['value']
for i in self.fb_dtsg: for i in self.fb_dtsg:
self.ttstamp += str(ord(i)) self.ttstamp += str(ord(i))
self.ttstamp += '2' self.ttstamp += '2'
@@ -323,8 +336,8 @@ class Client(object):
:rtype: bool :rtype: bool
""" """
# Send a request to the login url, to see if we're directed to the home page # Send a request to the login url, to see if we're directed to the home page
r = self._cleanGet(self.req_url.LOGIN) r = self._cleanGet(self.req_url.LOGIN, allow_redirects=False)
return 'home' in r.url return 'Location' in r.headers and 'home' in r.headers['Location']
def getSession(self): def getSession(self):
"""Retrieves session cookies """Retrieves session cookies
@@ -398,6 +411,11 @@ class Client(object):
:return: True if the action was successful :return: True if the action was successful
:rtype: bool :rtype: bool
""" """
if not hasattr(self, 'fb_h'):
h_r = self._post(self.req_url.MODERN_SETTINGS_MENU, {'pmid': '4'})
self.fb_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1)
data = { data = {
'ref': "mb", 'ref': "mb",
'h': self.fb_h 'h': self.fb_h
@@ -791,24 +809,34 @@ class Client(object):
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
@@ -1230,28 +1258,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

@@ -181,14 +181,16 @@ def graphql_to_thread(thread):
c_info = get_customization_info(thread) c_info = get_customization_info(thread)
participants = [node['messaging_actor'] for node in thread['all_participants']['nodes']] 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']) user = next(p for p in participants if p['id'] == thread['thread_key']['other_user_id'])
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise'] last_message_timestamp = None
if 'last_message' in thread:
last_message_timestamp = thread['last_message']['nodes'][0]['timestamp_precise']
return User( return User(
user['id'], user['id'],
url=user.get('url'), url=user.get('url'),
name=user.get('name'), name=user.get('name'),
first_name=user.get('short_name'), first_name=user.get('short_name'),
last_name=user.get('name').split(user.get('short_name'),1)[1].strip(), last_name=user.get('name').split(user.get('short_name'),1).pop().strip(),
is_friend=user.get('is_viewer_friend'), is_friend=user.get('is_viewer_friend'),
gender=GENDERS.get(user.get('gender')), gender=GENDERS.get(user.get('gender')),
affinity=user.get('affinity'), affinity=user.get('affinity'),
@@ -207,7 +209,9 @@ 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 = group['last_message']['nodes'][0]['timestamp_precise'] 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']]),

View File

@@ -469,7 +469,7 @@ class EmojiSize(Enum):
class ThreadColor(Enum): class ThreadColor(Enum):
"""Used to specify a thread colors""" """Used to specify a thread colors"""
MESSENGER_BLUE = '' MESSENGER_BLUE = '#0084ff'
VIKING = '#44bec7' VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300' GOLDEN_POPPY = '#ffc300'
RADICAL_RED = '#fa3c4c' RADICAL_RED = '#fa3c4c'

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"
@@ -113,6 +121,7 @@ class ReqUrl(object):
GRAPHQL = "https://www.facebook.com/api/graphqlbatch/" GRAPHQL = "https://www.facebook.com/api/graphqlbatch/"
ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/" ATTACHMENT_PHOTO = "https://www.facebook.com/mercury/attachments/photo/"
EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create" EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create"
MODERN_SETTINGS_MENU = "https://www.facebook.com/bluebar/modern_settings_menu/"
pull_channel = 0 pull_channel = 0

View File

@@ -114,7 +114,7 @@ class TestFbchat(unittest.TestCase):
self.assertIsNotNone(client.send(Message(sticker=Sticker(test_sticker_id)))) self.assertIsNotNone(client.send(Message(sticker=Sticker(test_sticker_id))))
def test_sendImages(self): def test_sendImages(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png' image_url = 'https://github.com/carpedm20/fbchat/raw/master/tests/image.png'
image_local_url = path.join(path.dirname(__file__), 'tests/image.png') image_local_url = path.join(path.dirname(__file__), 'tests/image.png')
for thread in threads: for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type']) client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])