diff --git a/.gitignore b/.gitignore
index 2345f05..cdfa971 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,11 @@
# Packages
*.egg
*.egg-info
+*.dist-info
dist
build
eggs
+.eggs
parts
bin
var
@@ -29,6 +31,7 @@ my_tests.py
my_test_data.json
my_data.json
tests.data
+.pytest_cache
# Virtual environment
venv/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8f1ccbc
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,43 @@
+sudo: false
+language: python
+python:
+- 2.7
+- 3.4
+- 3.5
+- 3.6
+- pypy
+
+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
+
+install:
+ - pip install -U -r requirements.txt
+ - pip install -U -r dev-requirements.txt
+
+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: true
+ directories:
+ - .pytest_cache
+
+deploy:
+ provider: pypi
+ user: madsmtm
+ password:
+ secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs="
+ distributions: sdist bdist_wheel
+ on:
+ branch: master
+ tags: true
diff --git a/MANIFEST.in b/MANIFEST.in
index 8f0b06d..97e2ad3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,2 @@
include LICENSE.txt
-include MANIFEST.in
include README.rst
-include setup.py
diff --git a/README.rst b/README.rst
index a1d14ea..441e203 100644
--- a/README.rst
+++ b/README.rst
@@ -5,21 +5,25 @@ fbchat: Facebook Chat (Messenger) for Python
:target: LICENSE.txt
:alt: License: BSD
-.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
+.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6%20pypy-blue.svg
:target: https://pypi.python.org/pypi/fbchat
- :alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
+ :alt: Supported python versions: 2.7, 3.4, 3.5, 3.6 and pypy
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
:target: https://fbchat.readthedocs.io
:alt: Documentation
+.. image:: https://travis-ci.org/carpedm20/fbchat.svg?branch=master
+ :target: https://travis-ci.org/carpedm20/fbchat
+ :alt: Travis CI
+
Facebook Chat (`Messenger `__) for Python.
This project was inspired by `facebook-chat-api `__.
**No XMPP or API key is needed**. Just use your email and password.
Go to `Read the Docs `__ to see the full documentation,
-or jump right into the code by viewing the `examples `__
+or jump right into the code by viewing the `examples `__
Installation:
@@ -27,6 +31,15 @@ Installation:
$ pip install fbchat
+You can also install from source, by using `setuptools` (You need at least version 30.3.0):
+
+.. code-block:: console
+
+ $ git clone https://github.com/carpedm20/fbchat.git
+ $ cd fbchat
+ $ python setup.py install
+
+
Maintainer
----------
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 0000000..9f73302
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,2 @@
+pytest
+six
diff --git a/fbchat/__init__.py b/fbchat/__init__.py
index 67ef0a9..4cf6cc9 100644
--- a/fbchat/__init__.py
+++ b/fbchat/__init__.py
@@ -1,28 +1,28 @@
# -*- coding: UTF-8 -*-
-from __future__ import unicode_literals
-from datetime import datetime
-from .client import *
-
-
"""
fbchat
~~~~~~
Facebook Chat (Messenger) for Python
- :copyright: (c) 2015 by Taehoon Kim.
+ :copyright: (c) 2015 - 2018 by Taehoon Kim
:license: BSD, see LICENSE for more details.
"""
+from __future__ import unicode_literals
-__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
-__version__ = '1.3.7'
+from .client import *
+
+__title__ = 'fbchat'
+__version__ = '1.3.8'
+__description__ = 'Facebook Chat (Messenger) for Python'
+
+__copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim'
__license__ = 'BSD'
+
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'
-__source__ = 'https://github.com/carpedm20/fbchat/'
-__description__ = 'Facebook Chat (Messenger) for Python'
__all__ = [
'Client',
diff --git a/fbchat/client.py b/fbchat/client.py
index d09b287..8b2a2b6 100644
--- a/fbchat/client.py
+++ b/fbchat/client.py
@@ -212,7 +212,7 @@ class Client(object):
self.ttstamp = ''
r = self._get(self.req_url.BASE)
- soup = bs(r.text, "lxml")
+ soup = bs(r.text, "html.parser")
fb_dtsg_element = soup.find("input", {'name': 'fb_dtsg'})
if fb_dtsg_element:
@@ -255,7 +255,7 @@ class Client(object):
if not (self.email and self.password):
raise FBchatUserError("Email and password not found.")
- soup = bs(self._get(self.req_url.MOBILE).text, "lxml")
+ soup = bs(self._get(self.req_url.MOBILE).text, "html.parser")
data = dict((elem['name'], elem['value']) for elem in soup.findAll("input") if elem.has_attr('value') and elem.has_attr('name'))
data['email'] = self.email
data['pass'] = self.password
@@ -280,7 +280,7 @@ class Client(object):
return False, r.url
def _2FA(self, r):
- soup = bs(r.text, "lxml")
+ soup = bs(r.text, "html.parser")
data = dict()
s = self.on2FACode()
@@ -1041,7 +1041,6 @@ class Client(object):
:param user_ids: One or more user IDs to add
:param thread_id: Group ID to add people to. See :ref:`intro_threads`
:type user_ids: list
- :return: :ref:`Message ID ` of the executed action
:raises: FBchatException if request failed
"""
thread_id, thread_type = self._getThread(thread_id, None)
@@ -1141,7 +1140,7 @@ class Client(object):
thread_id, thread_type = self._getThread(thread_id, None)
data = {
- 'color_choice': color.value,
+ 'color_choice': color.value if color != ThreadColor.MESSENGER_BLUE else '',
'thread_or_other_fbid': thread_id
}
@@ -1551,13 +1550,18 @@ class Client(object):
self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m)
# Typing
- elif mtype == "typ":
+ elif mtype == "typ" or mtype == "ttyp":
author_id = str(m.get("from"))
- thread_id = str(m.get("to"))
- if thread_id == self.uid:
- thread_type = ThreadType.USER
- else:
+ thread_id = m.get("thread_fbid")
+ if thread_id:
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"))
self.onTyping(author_id=author_id, status=typing_status, thread_id=thread_id, thread_type=thread_type, msg=m)
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..ecbb1b3
--- /dev/null
+++ b/pytest.ini
@@ -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"
diff --git a/requirements.txt b/requirements.txt
index 22069fb..9cb2a6c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,3 @@
requests
-lxml
beautifulsoup4
enum34; python_version < '3.4'
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..dbd7670
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,51 @@
+[metadata]
+name = fbchat
+version = attr: fbchat.__version__
+license = BSD
+license_file = LICENSE.txt
+
+author = Taehoon Kim
+author_email = carpedm20@gmail.com
+maintainer = Mads Marquart
+maintainer_email = madsmtm@gmail.com
+
+description = Facebook Chat (Messenger) for Python
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+
+keywords = Facebook FB Messenger Chat Api Bot
+classifiers =
+ Development Status :: 3 - Alpha
+ Intended Audience :: Developers
+ Intended Audience :: Information Technology
+ License :: OSI Approved :: BSD License
+ Operating System :: OS Independent
+ Natural Language :: English
+ Programming Language :: Python
+ Programming Language :: Python :: 2.7
+ Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: PyPy
+ Topic :: Communications :: Chat
+ Topic :: Internet :: WWW/HTTP :: Dynamic Content
+ Topic :: Software Development :: Libraries :: Python Modules
+
+url = https://github.com/carpedm20/fbchat/
+project_urls =
+ Documentation = https://fbchat.readthedocs.io/
+ Repository = https://github.com/carpedm20/fbchat/
+
+[options]
+zip_safe = True
+include_package_data = True
+packages = find:
+python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4.0
+install_requires =
+ requests
+ beautifulsoup4
+ # May not work in pip with bdist_wheel
+ # See https://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies
+ # It is therefore defined in setup.py
+ # enum34; python_version < '3.4'
diff --git a/setup.py b/setup.py
old mode 100644
new mode 100755
index 0c47c6d..1157ca5
--- a/setup.py
+++ b/setup.py
@@ -1,82 +1,8 @@
#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+from __future__ import unicode_literals
-"""
-Setup script for fbchat
-"""
-import os
-try:
- from setuptools import setup
-except ImportError:
- from distutils.core import setup
+from setuptools import setup
-with open('README.rst') as f:
- readme_content = f.read().strip()
-
-requirements = [
- 'requests',
- 'lxml',
- 'beautifulsoup4'
-]
-
-extras_requirements = {
- ':python_version < "3.4"': ['enum34']
-}
-
-version = None
-author = None
-email = None
-source = None
-description = None
-with open(os.path.join('fbchat', '__init__.py')) as f:
- for line in f:
- if line.strip().startswith('__version__'):
- version = line.split('=')[1].strip().replace('"', '').replace("'", '')
- elif line.strip().startswith('__author__'):
- author = line.split('=')[1].strip().replace('"', '').replace("'", '')
- elif line.strip().startswith('__email__'):
- email = line.split('=')[1].strip().replace('"', '').replace("'", '')
- elif line.strip().startswith('__source__'):
- source = line.split('=')[1].strip().replace('"', '').replace("'", '')
- elif line.strip().startswith('__description__'):
- description = line.split('=')[1].strip().replace('"', '').replace("'", '')
- elif None not in (version, author, email, source, description):
- break
-
-setup(
- name='fbchat',
- author=author,
- author_email=email,
- license='BSD License',
- keywords=["facebook chat fbchat"],
- description=description,
- long_description=readme_content,
- classifiers=[
- 'Development Status :: 2 - Pre-Alpha',
- 'Intended Audience :: Developers',
- 'Intended Audience :: Information Technology',
- 'License :: OSI Approved :: BSD License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
- 'Programming Language :: Python',
- 'Topic :: Software Development :: Libraries :: Python Modules',
- 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
- 'Topic :: Communications :: Chat',
- ],
- include_package_data=True,
- packages=['fbchat'],
- install_requires=requirements,
- extras_require=extras_requirements,
- url=source,
- version=version,
- zip_safe=True,
-)
+setup(extras_require={':python_version < "3.4"': ['enum34']})
diff --git a/tests.py b/tests.py
deleted file mode 100644
index caf6748..0000000
--- a/tests.py
+++ /dev/null
@@ -1,261 +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('', '', 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('té')
- 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://github.com/carpedm20/fbchat/raw/master/tests/image.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):
- threads = client.fetchThreadList(limit=2)
- self.assertEqual(len(threads), 2)
-
- 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)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..879e3dc
--- /dev/null
+++ b/tests/conftest.py
@@ -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
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..6a60482
--- /dev/null
+++ b/tests/test_base.py
@@ -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("", "", 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"))
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
new file mode 100644
index 0000000..d755c5a
--- /dev/null
+++ b/tests/test_fetch.py
@@ -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)
diff --git a/tests/test_message_management.py b/tests/test_message_management.py
new file mode 100644
index 0000000..ed6c447
--- /dev/null
+++ b/tests/test_message_management.py
@@ -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)
diff --git a/tests/test_search.py b/tests/test_search.py
new file mode 100644
index 0000000..dda9568
--- /dev/null
+++ b/tests/test_search.py
@@ -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"
diff --git a/tests/test_send.py b/tests/test_send.py
new file mode 100644
index 0000000..82098d7
--- /dev/null
+++ b/tests/test_send.py
@@ -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]
diff --git a/tests/test_tests.py b/tests/test_tests.py
new file mode 100644
index 0000000..cfb2600
--- /dev/null
+++ b/tests/test_tests.py
@@ -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
diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py
new file mode 100644
index 0000000..e897dbb
--- /dev/null
+++ b/tests/test_thread_interraction.py
@@ -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)
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..741f798
--- /dev/null
+++ b/tests/utils.py
@@ -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())