Compare commits

...

48 Commits

Author SHA1 Message Date
Mads Marquart
6116bc9ca4 addUsersToGroup can no longer return the message id
Updated documentation and tests
2018-06-06 16:39:23 +02:00
Mads Marquart
7bf6a9fadc Version up, thanks to @2FWAH
* Fixed `onTyping`
* Fixed `changeThreadColor` with `MESSENGER_BLUE `
2018-06-05 13:17:46 +02:00
Mads Marquart
4490360e11 Changed encrypted passwords to point to the free TravisCI version 2018-06-05 13:16:14 +02:00
Mads Marquart
a4dfe0d279 changeThreadColor now works with MESSENGER_BLUE again 2018-06-05 12:55:03 +02:00
Mads Marquart
47679d1d3b Merge remote-tracking branch '2FWAH/fix-ontyping' 2018-06-05 12:51:43 +02:00
Mads Marquart
62e17daf78 thread_fbid is not available with typ, there thread_id = author_id
Also enabled tests
2018-06-04 23:57:50 +02:00
2FWAH
1f359f2a72 Call onTyping on "typ" or "ttyp" messages
FB returns "typ" for ONE-TO-ONE conversations and "ttyp" for GROUP conversations.
2018-06-04 23:25:50 +02:00
2FWAH
cebe7a28c0 Fix onTyping detection
FB changed the format of typing notification messages:
- update "mtype" from "typ" to "ttyp".
- Get thread ID from "to" to "thread_fbid" ("thread" looks the same)
2018-06-04 23:25:50 +02:00
Mads Marquart
e614800d5f Update encrypted passwords 2018-06-04 13:57:21 +02:00
Mads Marquart
151a114235 TravisCI integration and updated test suite (#296)
* Make TravisCI setup

* Use pytest, move tests to seperate files

* Added system to check if `onX` events were successfully executed
2018-06-04 13:44:04 +02:00
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
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
Mads Marquart
3142524809 Version up, thanks to @DeltaF1
`onFriendRequest` functionality is restored
2018-02-07 11:30:19 +01:00
Mads Marquart
4c9d3bd9d7 Merge pull request #255 from DeltaF1/master
Restored onFriendRequest functionality
2018-02-07 11:29:04 +01:00
DeltaF1
ba103066b8 Restored onFriendRequest functionality 2018-02-06 00:30:35 -05:00
21 changed files with 768 additions and 360 deletions

7
.gitignore vendored
View File

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

35
.travis.yml Normal file
View File

@@ -0,0 +1,35 @@
sudo: false
language: python
python:
- 2.7
- 3.4
- 3.5
- 3.6
env:
global:
# These two accounts are made specifically for this purpose, and the passwords are really only encrypted for obscurity
# In reality, you could probably still login with the accounts by looking at log output from travis-ci.org
- client1_email=travis.fbchat1@gmail.com # Facebook ID: 100023782141139
- secure: "W1NON6qaLnvYIOVoC93MXkmbAIkUkHcGREBwN0BSVM3cLuMduk4VVkz6PY2T8bnntGYVwicXwcn5aNJ6pDue17TBZqGPk/tdpws8mnAZUtBYhpkIFTTlyh5kJSZejx9fd5s4nceGpH6ofCCnNxPp2PdHKU8piqnQYZVQ4cFNNDE=" # client1_password
- client2_email=fbchat.travis2@gmail.com # Facebook ID: 100026538491708
- secure: "V7RB3go2Tc/DdW1x9DkMI+vCfnOgiS3ygmFCABs/GjfPZjZL7VLMJgYGlx0cjeeeN+Oxa2GrhczRAKeMdGB6Ss2lGGAVs6cjJ56ODuBHWT6/FNzLjtDkTnjD+Kfh0l8ZOdxTF3MQ6M/9hU6z5ek+XYGr7u+/7wOYZ5L2cK5MaQ0=" # client2_password
- group_id=1463789480385605
before_script:
- if [[ "$TRAVIS_PYTHON_VERSION" = "2.7" ]]; then export PYTEST_ADDOPTS='-m ""'; fi; # expensive tests (otherwise disabled in pytest.ini)
- if [[ "$TRAVIS_PULL_REQUEST" != false ]]; then export PYTEST_ADDOPTS='-m offline'; fi; # offline tests only
script: python -m pytest || python -m pytest --lf; # Run failed tests twice
cache: pip
deploy:
provider: pypi
user: madsmtm
password:
secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs="
on:
python: 3.6
branch: master
tags: true

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.0' __version__ = '1.3.8'
__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
@@ -754,19 +772,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 +797,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
@@ -1043,7 +1038,6 @@ class Client(object):
:param user_ids: One or more user IDs to add :param user_ids: One or more user IDs to add
:param thread_id: Group ID to add people to. See :ref:`intro_threads` :param thread_id: Group ID to add people to. See :ref:`intro_threads`
:type user_ids: list :type user_ids: list
:return: :ref:`Message ID <intro_message_ids>` of the executed action
:raises: FBchatException if request failed :raises: FBchatException if request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
@@ -1143,7 +1137,7 @@ class Client(object):
thread_id, thread_type = self._getThread(thread_id, None) thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
'color_choice': color.value, 'color_choice': color.value if color != ThreadColor.MESSENGER_BLUE else '',
'thread_or_other_fbid': thread_id 'thread_or_other_fbid': thread_id
} }
@@ -1263,28 +1257,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)
@@ -1526,13 +1528,18 @@ class Client(object):
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m)
# Typing # Typing
elif mtype == "typ": elif mtype == "typ" or mtype == "ttyp":
author_id = str(m.get("from")) author_id = str(m.get("from"))
thread_id = str(m.get("to")) thread_id = m.get("thread_fbid")
if thread_id == self.uid: if thread_id:
thread_type = ThreadType.USER
else:
thread_type = ThreadType.GROUP thread_type = ThreadType.GROUP
thread_id = str(thread_id)
else:
thread_type = ThreadType.USER
if author_id == self.uid:
thread_id = m.get("to")
else:
thread_id = author_id
typing_status = TypingStatus(m.get("st")) typing_status = TypingStatus(m.get("st"))
self.onTyping(author_id=author_id, status=typing_status, thread_id=thread_id, thread_type=thread_type, msg=m) self.onTyping(author_id=author_id, status=typing_status, thread_id=thread_id, thread_type=thread_type, msg=m)
@@ -1543,9 +1550,9 @@ class Client(object):
# #
# self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time')) # self.onSeen(m.get('realtime_viewer_fbid'), m.get('reader'), m.get('time'))
# elif mtype in ['jewel_requests_add']: elif mtype in ['jewel_requests_add']:
# from_id = m['from'] from_id = m['from']
# self.on_friend_request(from_id) self.onFriendRequest(from_id=from_id, msg=m)
# Happens on every login # Happens on every login
elif mtype == "qprimer": elif mtype == "qprimer":

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).pop().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"""
@@ -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

6
pytest.ini Normal file
View File

@@ -0,0 +1,6 @@
[pytest]
xfail_strict=true
markers =
offline: Offline tests, aka. tests that can be executed without the need of a client
expensive: Expensive tests, which should be executed sparingly
addopts = -m "not expensive"

View File

@@ -2,3 +2,4 @@ requests
lxml lxml
beautifulsoup4 beautifulsoup4
enum34; python_version < '3.4' enum34; python_version < '3.4'
six

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',

260
tests.py
View File

@@ -1,260 +0,0 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import logging
import unittest
from getpass import getpass
from sys import argv
from os import path, chdir
from glob import glob
from fbchat import Client
from fbchat.models import *
import py_compile
logging_level = logging.ERROR
"""
Testing script for `fbchat`.
Full documentation on https://fbchat.readthedocs.io/
"""
test_sticker_id = '767334476626295'
class CustomClient(Client):
def __init__(self, *args, **kwargs):
self.got_qprimer = False
super(type(self), self).__init__(*args, **kwargs)
def onQprimer(self, msg, **kwargs):
self.got_qprimer = True
class TestFbchat(unittest.TestCase):
def test_examples(self):
# Checks for syntax errors in the examples
chdir('examples')
for f in glob('*.txt'):
print(f)
with self.assertRaises(py_compile.PyCompileError):
py_compile.compile(f)
chdir('..')
def test_loginFunctions(self):
self.assertTrue(client.isLoggedIn())
client.logout()
self.assertFalse(client.isLoggedIn())
with self.assertRaises(Exception):
client.login('<email>', '<password>', max_tries=1)
client.login(email, password)
self.assertTrue(client.isLoggedIn())
def test_sessions(self):
global client
session_cookies = client.getSession()
client = CustomClient(email, password, session_cookies=session_cookies, logging_level=logging_level)
self.assertTrue(client.isLoggedIn())
def test_defaultThread(self):
# setDefaultThread
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertTrue(client.send(Message(text='test_default_recipient★')))
# resetDefaultThread
client.resetDefaultThread()
with self.assertRaises(ValueError):
client.send(Message(text='should_not_send'))
def test_fetchAllUsers(self):
users = client.fetchAllUsers()
self.assertGreater(len(users), 0)
def test_searchFor(self):
users = client.searchForUsers('Mark Zuckerberg')
self.assertGreater(len(users), 0)
u = users[0]
# Test if values are set correctly
self.assertEqual(u.uid, '4')
self.assertEqual(u.type, ThreadType.USER)
self.assertEqual(u.photo[:4], 'http')
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_send(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.SMALL)))
self.assertIsNotNone(client.send(Message(emoji_size=EmojiSize.MEDIUM)))
self.assertIsNotNone(client.send(Message(text='😆', emoji_size=EmojiSize.LARGE)))
self.assertIsNotNone(client.send(Message(text='test_send★')))
with self.assertRaises(FBchatFacebookError):
self.assertIsNotNone(client.send(Message(text='test_send_should_fail★'), thread_id=thread['id'], thread_type=(ThreadType.GROUP if thread['type'] == ThreadType.USER else ThreadType.USER)))
self.assertIsNotNone(client.send(Message(text='Hi there @user', mentions=[Mention(user_id, offset=9, length=5)])))
self.assertIsNotNone(client.send(Message(text='Hi there @group', mentions=[Mention(group_id, offset=9, length=6)])))
self.assertIsNotNone(client.send(Message(sticker=Sticker(test_sticker_id))))
def test_sendImages(self):
image_url = 'https://cdn4.iconfinder.com/data/icons/ionicons/512/icon-image-128.png'
image_local_url = path.join(path.dirname(__file__), 'tests/image.png')
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
mentions = [Mention(thread['id'], offset=26, length=4)]
self.assertTrue(client.sendRemoteImage(image_url, Message(text='test_send_image_remote_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):
client.fetchThreadList(offset=0, limit=20)
def test_fetchThreadMessages(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.send(Message(text='test_getThreadInfo★'))
messages = client.fetchThreadMessages(limit=1)
self.assertEqual(messages[0].author, client.uid)
self.assertEqual(messages[0].text, 'test_getThreadInfo★')
def test_listen(self):
client.startListening()
client.doOneListen()
client.stopListening()
self.assertTrue(client.got_qprimer)
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)
def test_changeThreadTitle(self):
for thread in threads:
client.changeThreadTitle('test_changeThreadTitle★', thread_id=thread['id'], thread_type=thread['type'])
def test_changeNickname(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeNickname('test_changeNicknameSelf★', client.uid)
client.changeNickname('test_changeNicknameOther★', user_id)
def test_changeThreadEmoji(self):
for thread in threads:
client.changeThreadEmoji('😀', thread_id=thread['id'])
client.changeThreadEmoji('😀', thread_id=thread['id'])
def test_changeThreadColor(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.changeThreadColor(ThreadColor.BRILLIANT_ROSE)
client.changeThreadColor(ThreadColor.MESSENGER_BLUE)
def test_reactToMessage(self):
for thread in threads:
mid = client.send(Message(text='test_reactToMessage★'), thread_id=thread['id'], thread_type=thread['type'])
client.reactToMessage(mid, MessageReaction.LOVE)
def test_setTypingStatus(self):
for thread in threads:
client.setDefaultThread(thread_id=thread['id'], thread_type=thread['type'])
client.setTypingStatus(TypingStatus.TYPING)
client.setTypingStatus(TypingStatus.STOPPED)
def start_test(param_client, param_group_id, param_user_id, param_threads, tests=[]):
global client
global group_id
global user_id
global threads
client = param_client
group_id = param_group_id
user_id = param_user_id
threads = param_threads
tests = ['test_' + test if 'test_' != test[:5] else test for test in tests]
if len(tests) == 0:
suite = unittest.TestLoader().loadTestsFromTestCase(TestFbchat)
else:
suite = unittest.TestSuite(map(TestFbchat, tests))
print('Starting test(s)')
unittest.TextTestRunner(verbosity=2).run(suite)
client = None
if __name__ == '__main__':
# Python 3 does not use raw_input, whereas Python 2 does
try:
input = raw_input
except Exception as e:
pass
try:
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'r') as f:
j = json.load(f)
email = j['email']
password = j['password']
user_id = j['user_thread_id']
group_id = j['group_thread_id']
session = j.get('session')
except (IOError, IndexError) as e:
email = input('Email: ')
password = getpass()
group_id = input('Please enter a group thread id (To test group functionality): ')
user_id = input('Please enter a user thread id (To test kicking/adding functionality): ')
threads = [
{
'id': user_id,
'type': ThreadType.USER
},
{
'id': group_id,
'type': ThreadType.GROUP
}
]
print('Logging in...')
client = CustomClient(email, password, logging_level=logging_level, session_cookies=session)
# Warning! Taking user input directly like this could be dangerous! Use only for testing purposes!
start_test(client, group_id, user_id, threads, argv[1:])
with open(path.join(path.dirname(__file__), 'tests/my_data.json'), 'w') as f:
session = None
try:
session = client.getSession()
except Exception:
print('Unable to fetch client session!')
json.dump({
'email': email,
'password': password,
'user_thread_id': user_id,
'group_thread_id': group_id,
'session': session
}, f)

101
tests/conftest.py Normal file
View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
import json
from utils import *
from contextlib import contextmanager
from fbchat.models import ThreadType
@pytest.fixture(scope="session")
def user(client2):
return {"id": client2.uid, "type": ThreadType.USER}
@pytest.fixture(scope="session")
def group(pytestconfig):
return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP}
@pytest.fixture(scope="session", params=["user", "group"])
def thread(request, user, group):
return user if request.param == "user" else group
@pytest.fixture(scope="session")
def client1(pytestconfig):
with load_client(1, pytestconfig.cache) as c:
yield c
@pytest.fixture(scope="session")
def client2(pytestconfig):
with load_client(2, pytestconfig.cache) as c:
yield c
@pytest.fixture # (scope="session")
def client(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"])
yield client1
client1.resetDefaultThread()
@pytest.fixture(scope="session", params=["client1", "client2"])
def client_all(request, client1, client2):
return client1 if request.param == "client1" else client2
@pytest.fixture(scope="session")
def catch_event(client2):
t = ClientThread(client2)
t.start()
@contextmanager
def inner(method_name):
caught = CaughtValue()
old_method = getattr(client2, method_name)
# Will be called by the other thread
def catch_value(*args, **kwargs):
old_method(*args, **kwargs)
# Make sure the `set` is only called once
if not caught.is_set():
caught.set(kwargs)
setattr(client2, method_name, catch_value)
yield caught
caught.wait()
if not caught.is_set():
raise ValueError("The value could not be caught")
setattr(client2, method_name, old_method)
yield inner
t.should_stop.set()
try:
# Make the client send a messages to itself, so the blocking pull request will return
# This is probably not safe, since the client is making two requests simultaneously
client2.sendMessage("Shutdown", client2.uid)
finally:
t.join()
@pytest.fixture # (scope="session")
def compare(client, thread):
def inner(caught_event, **kwargs):
d = {
"author_id": client.uid,
"thread_id": client.uid
if thread["type"] == ThreadType.USER
else thread["id"],
"thread_type": thread["type"],
}
d.update(kwargs)
return subset(caught_event.res, **d)
return inner

55
tests/test_base.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
import py_compile
from glob import glob
from os import path, environ
from fbchat import Client
from fbchat.models import FBchatUserError, Message
@pytest.mark.offline
def test_examples():
# Compiles the examples, to check for syntax errors
for name in glob(path.join(path.dirname(__file__), "../examples", "*.py")):
py_compile.compile(name)
@pytest.mark.trylast
@pytest.mark.expensive
def test_login(client1):
assert client1.isLoggedIn()
email = client1.email
password = client1.password
client1.logout()
assert not client1.isLoggedIn()
with pytest.raises(FBchatUserError):
client1.login("<invalid email>", "<invalid password>", max_tries=1)
client1.login(email, password)
assert client1.isLoggedIn()
@pytest.mark.trylast
def test_sessions(client1):
session = client1.getSession()
Client("no email needed", "no password needed", session_cookies=session)
client1.setSession(session)
assert client1.isLoggedIn()
@pytest.mark.tryfirst
def test_default_thread(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"])
assert client1.send(Message(text="Sent to the specified thread"))
client1.resetDefaultThread()
with pytest.raises(ValueError):
client1.send(Message(text="Should not be sent"))

79
tests/test_fetch.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from os import path
from fbchat.models import ThreadType, Message, Mention, EmojiSize, Sticker
from utils import subset
def test_fetch_all_users(client):
users = client.fetchAllUsers()
assert len(users) > 0
def test_fetch_thread_list(client):
threads = client.fetchThreadList(limit=2)
assert len(threads) == 2
@pytest.mark.parametrize(
"emoji, emoji_size",
[
("😆", EmojiSize.SMALL),
("😆", EmojiSize.MEDIUM),
("😆", EmojiSize.LARGE),
# These fail because the emoji is made into a sticker
# This should be fixed
pytest.mark.xfail((None, EmojiSize.SMALL)),
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
pytest.mark.xfail((None, EmojiSize.LARGE)),
],
)
def test_fetch_message_emoji(client, emoji, emoji_size):
mid = client.sendEmoji(emoji, emoji_size)
message, = client.fetchThreadMessages(limit=1)
assert subset(
vars(message), uid=mid, author=client.uid, text=emoji, emoji_size=emoji_size
)
def test_fetch_message_mentions(client):
text = "This is a test of fetchThreadMessages"
mentions = [Mention(client.uid, offset=10, length=4)]
mid = client.send(Message(text, mentions=mentions))
message, = client.fetchThreadMessages(limit=1)
assert subset(vars(message), uid=mid, author=client.uid, text=text)
for i, m in enumerate(mentions):
assert vars(message.mentions[i]) == vars(m)
@pytest.mark.parametrize("sticker_id", ["767334476626295"])
def test_fetch_message_sticker(client, sticker_id):
mid = client.send(Message(sticker=Sticker(sticker_id)))
message, = client.fetchThreadMessages(limit=1)
assert subset(vars(message), uid=mid, author=client.uid)
assert subset(vars(message.sticker), uid=sticker_id)
def test_fetch_info(client1, group):
info = client1.fetchUserInfo("4")["4"]
assert info.name == "Mark Zuckerberg"
info = client1.fetchGroupInfo(group["id"])[group["id"]]
assert info.type == ThreadType.GROUP
def test_fetch_image_url(client):
url = path.join(path.dirname(__file__), "image.png")
client.sendLocalImage(url)
message, = client.fetchThreadMessages(limit=1)
assert client.fetchImageUrl(message.attachments[0].uid)

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import Message, MessageReaction
def test_set_reaction(client):
mid = client.send(Message(text="This message will be reacted to"))
client.reactToMessage(mid, MessageReaction.LOVE)

18
tests/test_search.py Normal file
View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from fbchat.models import ThreadType
def test_search_for(client1):
users = client1.searchForUsers("Mark Zuckerberg")
assert len(users) > 0
u = users[0]
assert u.uid == "4"
assert u.type == ThreadType.USER
assert u.photo[:4] == "http"
assert u.url[:4] == "http"
assert u.name == "Mark Zuckerberg"

110
tests/test_send.py Normal file
View File

@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from os import path
from fbchat.models import Message, Mention, EmojiSize, FBchatFacebookError, Sticker
from utils import subset
@pytest.mark.parametrize(
"text",
[
"test_send",
"😆",
"\\\n\t%?&'\"",
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
"a" * 20000, # Maximum amount of characters you can send
],
)
def test_send_text(client, catch_event, compare, text):
with catch_event("onMessage") as x:
mid = client.sendMessage(text)
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
@pytest.mark.parametrize(
"emoji, emoji_size",
[
("😆", EmojiSize.SMALL),
("😆", EmojiSize.MEDIUM),
("😆", EmojiSize.LARGE),
# These fail because the emoji is made into a sticker
# This should be fixed
pytest.mark.xfail((None, EmojiSize.SMALL)),
pytest.mark.xfail((None, EmojiSize.MEDIUM)),
pytest.mark.xfail((None, EmojiSize.LARGE)),
],
)
def test_send_emoji(client, catch_event, compare, emoji, emoji_size):
with catch_event("onMessage") as x:
mid = client.sendEmoji(emoji, emoji_size)
assert compare(x, mid=mid, message=emoji)
assert subset(
vars(x.res["message_object"]),
uid=mid,
author=client.uid,
text=emoji,
emoji_size=emoji_size,
)
@pytest.mark.xfail(raises=FBchatFacebookError)
@pytest.mark.parametrize("message", [Message("a" * 20001)])
def test_send_invalid(client, message):
client.send(message)
def test_send_mentions(client, client2, thread, catch_event, compare):
text = "Hi there @me, @other and @thread"
mentions = [
dict(thread_id=client.uid, offset=9, length=3),
dict(thread_id=client2.uid, offset=14, length=6),
dict(thread_id=thread["id"], offset=26, length=7),
]
with catch_event("onMessage") as x:
mid = client.send(Message(text, mentions=[Mention(**d) for d in mentions]))
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
# The mentions are not ordered by offset
for m in x.res["message_object"].mentions:
assert vars(m) in mentions
@pytest.mark.parametrize(
"sticker_id",
["767334476626295", pytest.mark.xfail("0", raises=FBchatFacebookError)],
)
def test_send_sticker(client, catch_event, compare, sticker_id):
with catch_event("onMessage") as x:
mid = client.send(Message(sticker=Sticker(sticker_id)))
assert compare(x, mid=mid)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid)
assert subset(vars(x.res["message_object"].sticker), uid=sticker_id)
@pytest.mark.parametrize(
"method_name, url",
[
(
"sendRemoteImage",
"https://github.com/carpedm20/fbchat/raw/master/tests/image.png",
),
("sendLocalImage", path.join(path.dirname(__file__), "image.png")),
],
)
def test_send_images(client, catch_event, compare, method_name, url):
text = "An image sent with {}".format(method_name)
with catch_event("onMessage") as x:
mid = getattr(client, method_name)(url, Message(text))
assert compare(x, mid=mid, message=text)
assert subset(vars(x.res["message_object"]), uid=mid, author=client.uid, text=text)
assert x.res["message_object"].attachments[0]

12
tests/test_tests.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
def test_catch_event(client2, catch_event):
mid = "test"
with catch_event("onMessage") as x:
client2.onMessage(mid=mid)
assert x.res['mid'] == mid

View File

@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import pytest
from fbchat.models import (
Message,
ThreadType,
FBchatFacebookError,
TypingStatus,
ThreadColor,
)
from utils import random_hex, subset
from os import environ
def test_remove_from_and_add_to_group(client1, client2, group, catch_event):
# Test both methods, while ensuring that the user gets added to the group
try:
with catch_event("onPersonRemoved") as x:
client1.removeUserFromGroup(client2.uid, group["id"])
assert subset(
x.res, removed_id=client2.uid, author_id=client1.uid, thread_id=group["id"]
)
finally:
with catch_event("onPeopleAdded") as x:
client1.addUsersToGroup(client2.uid, group["id"])
assert subset(
x.res, added_ids=[client2.uid], author_id=client1.uid, thread_id=group["id"]
)
@pytest.mark.xfail(
raises=FBchatFacebookError, reason="Apparently changeThreadTitle is broken"
)
def test_change_title(client1, catch_event, group):
title = random_hex()
with catch_event("onTitleChange") as x:
mid = client1.changeThreadTitle(title, group["id"])
assert subset(
x.res,
mid=mid,
author_id=client1.uid,
new_title=title,
thread_id=group["id"],
thread_type=ThreadType.GROUP,
)
def test_change_nickname(client, client_all, catch_event, compare):
nickname = random_hex()
with catch_event("onNicknameChange") as x:
client.changeNickname(nickname, client_all.uid)
assert compare(x, changed_for=client_all.uid, new_nickname=nickname)
@pytest.mark.parametrize("emoji", ["😀", "😂", "😕", "😍"])
def test_change_emoji(client, catch_event, compare, emoji):
with catch_event("onEmojiChange") as x:
client.changeThreadEmoji(emoji)
assert compare(x, new_emoji=emoji)
@pytest.mark.xfail(raises=FBchatFacebookError)
@pytest.mark.parametrize("emoji", ["🙃", "not an emoji"])
def test_change_emoji_invalid(client, emoji):
client.changeThreadEmoji(emoji)
@pytest.mark.parametrize(
"color",
[
x
if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN]
else pytest.mark.expensive(x)
for x in ThreadColor
],
)
def test_change_color(client, catch_event, compare, color):
with catch_event("onColorChange") as x:
client.changeThreadColor(color)
assert compare(x, new_color=color)
@pytest.mark.xfail(
raises=FBchatFacebookError, strict=False, reason="Should fail, but doesn't"
)
def test_change_color_invalid(client):
class InvalidColor:
value = "#0077ff"
client.changeThreadColor(InvalidColor())
@pytest.mark.parametrize("status", TypingStatus)
def test_typing_status(client, catch_event, compare, status):
with catch_event("onTyping") as x:
client.setTypingStatus(status)
assert compare(x, status=status)

83
tests/utils.py Normal file
View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import threading
import logging
import six
from os import environ
from random import randrange
from contextlib import contextmanager
from six import viewitems
from fbchat import Client
from fbchat.models import ThreadType
log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler())
class ClientThread(threading.Thread):
def __init__(self, client, *args, **kwargs):
self.client = client
self.should_stop = threading.Event()
super(ClientThread, self).__init__(*args, **kwargs)
def start(self):
self.client.startListening()
self.client.doOneListen() # QPrimer, Facebook now knows we're about to start pulling
super(ClientThread, self).start()
def run(self):
while not self.should_stop.is_set() and self.client.doOneListen():
pass
self.client.stopListening()
if six.PY2:
event_class = threading._Event
else:
event_class = threading.Event
class CaughtValue(event_class):
def set(self, res):
self.res = res
super(CaughtValue, self).set()
def wait(self, timeout=3):
super(CaughtValue, self).wait(timeout=timeout)
def random_hex(length=20):
return "{:X}".format(randrange(16 ** length))
def subset(a, **b):
print(a)
print(b)
return viewitems(b) <= viewitems(a)
def load_variable(name, cache):
var = environ.get(name, None)
if var is not None:
if cache.get(name, None) != var:
cache.set(name, var)
return var
var = cache.get(name, None)
if var is None:
raise ValueError("Variable {!r} neither in environment nor cache".format(name))
return var
@contextmanager
def load_client(n, cache):
client = Client(
load_variable("client{}_email".format(n), cache),
load_variable("client{}_password".format(n), cache),
session_cookies=cache.get("client{}_session".format(n), None),
)
yield client
cache.set("client{}_session".format(n), client.getSession())