From 151a1142355eb1fac126df79529c7d6711c55332 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 4 Jun 2018 13:44:04 +0200 Subject: [PATCH 01/14] 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 --- .gitignore | 1 + .travis.yml | 35 ++++ pytest.ini | 6 + requirements.txt | 1 + tests.py | 261 ------------------------------ tests/conftest.py | 101 ++++++++++++ tests/test_base.py | 55 +++++++ tests/test_fetch.py | 79 +++++++++ tests/test_message_management.py | 12 ++ tests/test_search.py | 18 +++ tests/test_send.py | 110 +++++++++++++ tests/test_tests.py | 12 ++ tests/test_thread_interraction.py | 112 +++++++++++++ tests/utils.py | 83 ++++++++++ 14 files changed, 625 insertions(+), 261 deletions(-) create mode 100644 .travis.yml create mode 100644 pytest.ini delete mode 100644 tests.py create mode 100644 tests/conftest.py create mode 100644 tests/test_base.py create mode 100644 tests/test_fetch.py create mode 100644 tests/test_message_management.py create mode 100644 tests/test_search.py create mode 100644 tests/test_send.py create mode 100644 tests/test_tests.py create mode 100644 tests/test_thread_interraction.py create mode 100644 tests/utils.py diff --git a/.gitignore b/.gitignore index 2345f05..dd3713c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,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..1b354d3 --- /dev/null +++ b/.travis.yml @@ -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: "JrQ8mrE7S8ActFiQF5ulICi9jXrluYTuEM0cHphI1N/Zj4pEw+k24hVhfGJvLLuIDRV12DxScc7dKgUIvqavc2Ko9cy0Ge7GTkjAjGYiyCPabxUUjgtPB173pK0x9nkjmyXdTbb2P4ZsaIfxPwSGPWS77jEkEboqia8Cqr/RhVA=" # client1_password + - client2_email=fbchat.travis2@gmail.com # Facebook ID: 100026538491708 + - secure: "AGQfwIm6VP3EosnP/Mu7Em0+gb5638+INMqXaJ8HEr2kkV0c12C2G2aDRzsD7DjinKTfGsPAMvY3HPmtx8luU7urQeM55ArmOgeZbbLK+I+hmm08lubBc/Ik3OSehZc0u7IqKIHTxOxlYMzCgZCFQYT0QtOVZUt8Q529AzbUVv4=" # 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: "fH7+JhSnhIh1FZPgt+TE5J+REVCHaasEuhXZmpV8NgOrIZMbTtGOAfbWcwZWNrgoODN+adZFAS0OMDBEKL4WDudM4aXtrX3W4efy4Btmrn10YiAIgGQhG2HtQeM8HMAWX6wyY+ZnQz2NcJYL/SQqRWfRnHtMJlQVYErzqubdB7Y=" + on: + python: 3.6 + branch: master + tags: true 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..5e641fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ requests lxml beautifulsoup4 enum34; python_version < '3.4' +six 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..7aa744a --- /dev/null +++ b/tests/test_thread_interraction.py @@ -0,0 +1,112 @@ +# -*- 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: + mid = client1.addUsersToGroup(client2.uid, group["id"]) + assert subset( + x.res, + mid=mid, + 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", + [ + pytest.mark.xfail( + x, reason="Apparently changing ThreadColor.MESSENGER_BLUE is broken" + ) + if x == ThreadColor.MESSENGER_BLUE + else x + if x == ThreadColor.PUMPKIN + else pytest.mark.expensive(x) + for x in ThreadColor + ], +) +def test_change_color(client, catch_event, compare, color): + if color == ThreadColor.MESSENGER_BLUE: + pytest.xfail(reason="Apparently changing ThreadColor.MESSENGER_BLUE is broken") + 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.xfail(reason="Apparently onTyping is broken") +@pytest.mark.parametrize("status", TypingStatus) +def test_typing_status(client, catch_event, compare, status): + with catch_event("onTyping") as x: + client.setTypingStatus(status) + # x.wait(40) + 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()) From e614800d5f0f5d4e7a4d4e18edaac1069d4f2ca9 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 4 Jun 2018 13:57:21 +0200 Subject: [PATCH 02/14] Update encrypted passwords --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b354d3..7b38f80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,9 @@ env: # 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: "JrQ8mrE7S8ActFiQF5ulICi9jXrluYTuEM0cHphI1N/Zj4pEw+k24hVhfGJvLLuIDRV12DxScc7dKgUIvqavc2Ko9cy0Ge7GTkjAjGYiyCPabxUUjgtPB173pK0x9nkjmyXdTbb2P4ZsaIfxPwSGPWS77jEkEboqia8Cqr/RhVA=" # client1_password + - secure: "DUJwuvaHCfC5xOAoKGOrRfYDCZuLySGlvNQ4aQlJvSiligsyRMTkR4HmCS3u03nVx4nPiceTXzrXGVYPjb8Gkg+pQ4uG5M2AeNBEEi8JjM1T3QAVYjCF/A1CKPteHjrXtlWi9IhPcrOPf4Ve9V6TqZg3x/XNzq//ZOuBm21Lr8FtZyUnqwKNJ5qFY9+IQtW1lulBdP464+U5dUrJ8+xmCunI9a/mSJ9w06FEBRnBkso3X8bVIsfeTpZ9eedzCEWwfShoAhIs63818pN3GFqzAeO+r/PG/ZrVzoAKLAcnCJX/JZtfyq56j55CQQH7TJm2TPXWAO3Y4LXvjeraqfOCdD3w5Gefns3C1zshic9tN9k57D3INQ4KrN9dlTElKZC9W2Sx0nITfLv82IBvkDIX2cEJLjl185zAZKXfqhpZoTyRsDeb2TPK5D36yM+kPMxiPYsS99TOYz/noaHBSqZj5sTkc1MTAEwYBBK0bJy+iw8LZsLuy3UVUO6OUzSUE34BtwKyAHnkhggIxof40Ed8KWsMCaz65j3+MWjdEmF0ZUt7OFMuQ+IbbaacQD262E1AFv/BhwYmGPvBuU06p0g1uph7jCNfMAWoAjayP9q0vqGyen6aNgNcNEGGLB7IvIRugztzEdvmnXIU7ATM87MWSny0K9UL8kDrR7vSS/JtSJ8=" # client1_password - client2_email=fbchat.travis2@gmail.com # Facebook ID: 100026538491708 - - secure: "AGQfwIm6VP3EosnP/Mu7Em0+gb5638+INMqXaJ8HEr2kkV0c12C2G2aDRzsD7DjinKTfGsPAMvY3HPmtx8luU7urQeM55ArmOgeZbbLK+I+hmm08lubBc/Ik3OSehZc0u7IqKIHTxOxlYMzCgZCFQYT0QtOVZUt8Q529AzbUVv4=" # client2_password + - secure: "AO9lE7H00bkgtyXiFfzUmkciFIZw957Z6OS/9GlmXfTO+KBQm7+wVMCLQUhx0+d6+EgpAiY0BQ+tFFyx5kuRrm4rI826EjY48cVM0tW4tnvWCHp036Muz0J7D3hmwtC2XYCySosL9evZm9cfRr02kUUltWqxBQHBRoWW1RoeYLBsTZ9YdDaj9VRk70TrC+jZ8bRi1647g7ssUZKI6x6gNxAWtwY0CwMIZvpUDgMTiyjHSlxu/qs+IjLsZa6z9IU2s0drbdLqnmNwCst9cjbPziDnMZfM+3/3Ty12nCCyagV/+2YU1CqLstvZIji+iH/DFtmq4PWpU2zpbZlPUWfR0e0nTfNVvNCzIic+guLScwvcgtc6fvQzNjog9Q1gVmsDaVwSTfbCOsosJtpCQS9DNQ2v9mn8PTMTNO1kWnfsEU1y3KWXPN0ixWKUx6EGQDr3H3cMOAweADyIMNwIutanJGBfDF+JAF8B/YNfmNfktN5QsmNS0P46F+04SFlYJNSD8Hbg9p45He46AQovedNS8mUnJ3AWhuScNlLF0GMZvwJet0FgCMqSOvUUh+6GHio1wPKNFKyD1sqiY1ZLMyZ2yavt+Ap6dmqb44e19FsqjYxlGLuDGTP7bF2m5VweWgnEIFZPFpTdhn67xQcX1eTzMqSPB0nOt8V/mqGn6uleduU=" # client2_password - group_id=1463789480385605 before_script: @@ -28,7 +28,7 @@ deploy: provider: pypi user: madsmtm password: - secure: "fH7+JhSnhIh1FZPgt+TE5J+REVCHaasEuhXZmpV8NgOrIZMbTtGOAfbWcwZWNrgoODN+adZFAS0OMDBEKL4WDudM4aXtrX3W4efy4Btmrn10YiAIgGQhG2HtQeM8HMAWX6wyY+ZnQz2NcJYL/SQqRWfRnHtMJlQVYErzqubdB7Y=" + secure: "D6RDwtfn4QhT/zMAW/7cSacdb0/6F0mpmSG9L4k3pLlxxXeXa12+do7dYMJFRSkzHmZ08P6xz/Gh/41QkNHN3AWRg11PNlVB2HfQBgzQz31Q2m5XgweTgVTkfx9HAGoa8DNKt/8Xay/hTyuKUyxJlLghLSTr2ZRK/EtNo1IdLPfUIV5S67bYQzbD5u7y5xfi65nd5RpgHCFl49LJ6CF++D9X/MvwTTyy8qceEij0XhIPZ6aFMb5Zf1pAAvgeWnPxmGD7RzL2zrfhizQTb1E+Vp/XVcdE1qF6YD7Wv3I2TCsELJIA3kO+RxHG3fWo7LQAKZe3zWKPLNBvf6w7p9W89CXjqAMA4X00ZCLNU3/VCxRcW/I9hc4/SzScaV22euR9+1hGkuD9I0LFk/SlH52f1i0QPdlm1P4UMS1FNSZal6gw75WWOCdGhJTmWv5JNZYFdUCl5pfSJTMexFgCmdawzRivaqGO+T3yrlPB2dttsesOaHTwTxu/aVaMC5c57kuataUJ5qwjOieTuAcBZ3v/WjxDtpj44FTEymtTuFHmNmq6uJFMROKdTVby9z0WGLJDnF6KM5wuaAC5IFPGuhOOPtbd3b4EMxaaPQY9jysBHStLF2Vc0YU/KO6XzKY8+LAUvQzeZaAuE11eVfx00q9VsDQ8P3qzBYQbmNf1d0f9OKI=" on: python: 3.6 branch: master From cebe7a28c081adaccbc739ecccc33187b0be1f97 Mon Sep 17 00:00:00 2001 From: 2FWAH <36737818+2FWAH@users.noreply.github.com> Date: Fri, 1 Jun 2018 12:57:09 +0200 Subject: [PATCH 03/14] 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) --- fbchat/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 2b20d69..ae1af9e 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1529,9 +1529,9 @@ class Client(object): self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) # Typing - elif mtype == "typ": + elif mtype == "ttyp": author_id = str(m.get("from")) - thread_id = str(m.get("to")) + thread_id = str(m.get("thread_fbid")) if thread_id == self.uid: thread_type = ThreadType.USER else: From 1f359f2a721ad5819929f8c14e01f9c842559cf9 Mon Sep 17 00:00:00 2001 From: 2FWAH <36737818+2FWAH@users.noreply.github.com> Date: Sun, 3 Jun 2018 22:33:15 +0200 Subject: [PATCH 04/14] Call onTyping on "typ" or "ttyp" messages FB returns "typ" for ONE-TO-ONE conversations and "ttyp" for GROUP conversations. --- fbchat/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/client.py b/fbchat/client.py index ae1af9e..d99a20b 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1529,7 +1529,7 @@ class Client(object): self.onInbox(unseen=m["unseen"], unread=m["unread"], recent_unread=m["recent_unread"], msg=m) # Typing - elif mtype == "ttyp": + elif mtype == "typ" or mtype == "ttyp": author_id = str(m.get("from")) thread_id = str(m.get("thread_fbid")) if thread_id == self.uid: From 62e17daf780614cb48dcfaadbd8f4bcd3628eeda Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 4 Jun 2018 23:57:50 +0200 Subject: [PATCH 05/14] `thread_fbid` is not available with `typ`, there `thread_id = author_id` Also enabled tests --- fbchat/client.py | 13 +++++++++---- tests/test_thread_interraction.py | 2 -- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index d99a20b..392f71a 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1531,11 +1531,16 @@ class Client(object): # Typing elif mtype == "typ" or mtype == "ttyp": author_id = str(m.get("from")) - thread_id = str(m.get("thread_fbid")) - 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/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index 7aa744a..9844998 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -103,10 +103,8 @@ def test_change_color_invalid(client): client.changeThreadColor(InvalidColor()) -@pytest.mark.xfail(reason="Apparently onTyping is broken") @pytest.mark.parametrize("status", TypingStatus) def test_typing_status(client, catch_event, compare, status): with catch_event("onTyping") as x: client.setTypingStatus(status) - # x.wait(40) assert compare(x, status=status) From a4dfe0d279569f76b86566195b30a8b7beb3d851 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 5 Jun 2018 12:55:03 +0200 Subject: [PATCH 06/14] `changeThreadColor` now works with `MESSENGER_BLUE` again --- fbchat/client.py | 2 +- tests/test_thread_interraction.py | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index 392f71a..aedcd0d 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1138,7 +1138,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 } diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index 9844998..8737b60 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -75,19 +75,13 @@ def test_change_emoji_invalid(client, emoji): @pytest.mark.parametrize( "color", [ - pytest.mark.xfail( - x, reason="Apparently changing ThreadColor.MESSENGER_BLUE is broken" - ) - if x == ThreadColor.MESSENGER_BLUE - else x - if x == ThreadColor.PUMPKIN + 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): - if color == ThreadColor.MESSENGER_BLUE: - pytest.xfail(reason="Apparently changing ThreadColor.MESSENGER_BLUE is broken") with catch_event("onColorChange") as x: client.changeThreadColor(color) assert compare(x, new_color=color) From 4490360e11824de22036d03972d4999664d05c5e Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 5 Jun 2018 13:16:14 +0200 Subject: [PATCH 07/14] Changed encrypted passwords to point to the free TravisCI version --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b38f80..1511593 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,9 @@ env: # 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: "DUJwuvaHCfC5xOAoKGOrRfYDCZuLySGlvNQ4aQlJvSiligsyRMTkR4HmCS3u03nVx4nPiceTXzrXGVYPjb8Gkg+pQ4uG5M2AeNBEEi8JjM1T3QAVYjCF/A1CKPteHjrXtlWi9IhPcrOPf4Ve9V6TqZg3x/XNzq//ZOuBm21Lr8FtZyUnqwKNJ5qFY9+IQtW1lulBdP464+U5dUrJ8+xmCunI9a/mSJ9w06FEBRnBkso3X8bVIsfeTpZ9eedzCEWwfShoAhIs63818pN3GFqzAeO+r/PG/ZrVzoAKLAcnCJX/JZtfyq56j55CQQH7TJm2TPXWAO3Y4LXvjeraqfOCdD3w5Gefns3C1zshic9tN9k57D3INQ4KrN9dlTElKZC9W2Sx0nITfLv82IBvkDIX2cEJLjl185zAZKXfqhpZoTyRsDeb2TPK5D36yM+kPMxiPYsS99TOYz/noaHBSqZj5sTkc1MTAEwYBBK0bJy+iw8LZsLuy3UVUO6OUzSUE34BtwKyAHnkhggIxof40Ed8KWsMCaz65j3+MWjdEmF0ZUt7OFMuQ+IbbaacQD262E1AFv/BhwYmGPvBuU06p0g1uph7jCNfMAWoAjayP9q0vqGyen6aNgNcNEGGLB7IvIRugztzEdvmnXIU7ATM87MWSny0K9UL8kDrR7vSS/JtSJ8=" # client1_password + - secure: "W1NON6qaLnvYIOVoC93MXkmbAIkUkHcGREBwN0BSVM3cLuMduk4VVkz6PY2T8bnntGYVwicXwcn5aNJ6pDue17TBZqGPk/tdpws8mnAZUtBYhpkIFTTlyh5kJSZejx9fd5s4nceGpH6ofCCnNxPp2PdHKU8piqnQYZVQ4cFNNDE=" # client1_password - client2_email=fbchat.travis2@gmail.com # Facebook ID: 100026538491708 - - secure: "AO9lE7H00bkgtyXiFfzUmkciFIZw957Z6OS/9GlmXfTO+KBQm7+wVMCLQUhx0+d6+EgpAiY0BQ+tFFyx5kuRrm4rI826EjY48cVM0tW4tnvWCHp036Muz0J7D3hmwtC2XYCySosL9evZm9cfRr02kUUltWqxBQHBRoWW1RoeYLBsTZ9YdDaj9VRk70TrC+jZ8bRi1647g7ssUZKI6x6gNxAWtwY0CwMIZvpUDgMTiyjHSlxu/qs+IjLsZa6z9IU2s0drbdLqnmNwCst9cjbPziDnMZfM+3/3Ty12nCCyagV/+2YU1CqLstvZIji+iH/DFtmq4PWpU2zpbZlPUWfR0e0nTfNVvNCzIic+guLScwvcgtc6fvQzNjog9Q1gVmsDaVwSTfbCOsosJtpCQS9DNQ2v9mn8PTMTNO1kWnfsEU1y3KWXPN0ixWKUx6EGQDr3H3cMOAweADyIMNwIutanJGBfDF+JAF8B/YNfmNfktN5QsmNS0P46F+04SFlYJNSD8Hbg9p45He46AQovedNS8mUnJ3AWhuScNlLF0GMZvwJet0FgCMqSOvUUh+6GHio1wPKNFKyD1sqiY1ZLMyZ2yavt+Ap6dmqb44e19FsqjYxlGLuDGTP7bF2m5VweWgnEIFZPFpTdhn67xQcX1eTzMqSPB0nOt8V/mqGn6uleduU=" # client2_password + - secure: "V7RB3go2Tc/DdW1x9DkMI+vCfnOgiS3ygmFCABs/GjfPZjZL7VLMJgYGlx0cjeeeN+Oxa2GrhczRAKeMdGB6Ss2lGGAVs6cjJ56ODuBHWT6/FNzLjtDkTnjD+Kfh0l8ZOdxTF3MQ6M/9hU6z5ek+XYGr7u+/7wOYZ5L2cK5MaQ0=" # client2_password - group_id=1463789480385605 before_script: @@ -28,7 +28,7 @@ deploy: provider: pypi user: madsmtm password: - secure: "D6RDwtfn4QhT/zMAW/7cSacdb0/6F0mpmSG9L4k3pLlxxXeXa12+do7dYMJFRSkzHmZ08P6xz/Gh/41QkNHN3AWRg11PNlVB2HfQBgzQz31Q2m5XgweTgVTkfx9HAGoa8DNKt/8Xay/hTyuKUyxJlLghLSTr2ZRK/EtNo1IdLPfUIV5S67bYQzbD5u7y5xfi65nd5RpgHCFl49LJ6CF++D9X/MvwTTyy8qceEij0XhIPZ6aFMb5Zf1pAAvgeWnPxmGD7RzL2zrfhizQTb1E+Vp/XVcdE1qF6YD7Wv3I2TCsELJIA3kO+RxHG3fWo7LQAKZe3zWKPLNBvf6w7p9W89CXjqAMA4X00ZCLNU3/VCxRcW/I9hc4/SzScaV22euR9+1hGkuD9I0LFk/SlH52f1i0QPdlm1P4UMS1FNSZal6gw75WWOCdGhJTmWv5JNZYFdUCl5pfSJTMexFgCmdawzRivaqGO+T3yrlPB2dttsesOaHTwTxu/aVaMC5c57kuataUJ5qwjOieTuAcBZ3v/WjxDtpj44FTEymtTuFHmNmq6uJFMROKdTVby9z0WGLJDnF6KM5wuaAC5IFPGuhOOPtbd3b4EMxaaPQY9jysBHStLF2Vc0YU/KO6XzKY8+LAUvQzeZaAuE11eVfx00q9VsDQ8P3qzBYQbmNf1d0f9OKI=" + secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs=" on: python: 3.6 branch: master From 7bf6a9fadc2011dbcac805d47b9f3e89057d7f33 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 5 Jun 2018 13:17:46 +0200 Subject: [PATCH 08/14] Version up, thanks to @2FWAH * Fixed `onTyping` * Fixed `changeThreadColor` with `MESSENGER_BLUE ` --- fbchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 67ef0a9..988000a 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -17,7 +17,7 @@ from .client import * __copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year) -__version__ = '1.3.7' +__version__ = '1.3.8' __license__ = 'BSD' __author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart' __email__ = 'carpedm20@gmail.com' From 6116bc9ca42aec938d0fb967ff59d5df0afe7db3 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 6 Jun 2018 16:39:23 +0200 Subject: [PATCH 09/14] `addUsersToGroup` can no longer return the message id Updated documentation and tests --- fbchat/client.py | 1 - tests/test_thread_interraction.py | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index aedcd0d..da9751a 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -1038,7 +1038,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) diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index 8737b60..e897dbb 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -25,13 +25,9 @@ def test_remove_from_and_add_to_group(client1, client2, group, catch_event): ) finally: with catch_event("onPeopleAdded") as x: - mid = client1.addUsersToGroup(client2.uid, group["id"]) + client1.addUsersToGroup(client2.uid, group["id"]) assert subset( - x.res, - mid=mid, - added_ids=[client2.uid], - author_id=client1.uid, - thread_id=group["id"], + x.res, added_ids=[client2.uid], author_id=client1.uid, thread_id=group["id"] ) From b2ff7fefaa5f8864fcada595ad5a35d259b0e2c7 Mon Sep 17 00:00:00 2001 From: Oren Date: Thu, 14 Jun 2018 16:19:09 +0300 Subject: [PATCH 10/14] Replace lxml with Python's built in html.parser --- fbchat/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fbchat/client.py b/fbchat/client.py index da9751a..7109644 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -209,7 +209,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: @@ -252,7 +252,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 @@ -277,7 +277,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() From 73f4c98be9dd920d54386e54424d16d57d4815c9 Mon Sep 17 00:00:00 2001 From: Oren Date: Thu, 14 Jun 2018 16:20:35 +0300 Subject: [PATCH 11/14] Remove lxml dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5e641fc..fbd1adc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ requests -lxml beautifulsoup4 enum34; python_version < '3.4' six From 66736519ed56993c6a2aedbd8c29a7066152b294 Mon Sep 17 00:00:00 2001 From: Oren Date: Thu, 14 Jun 2018 16:20:57 +0300 Subject: [PATCH 12/14] Remove lxml dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 0c47c6d..f2542ac 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ with open('README.rst') as f: requirements = [ 'requests', - 'lxml', 'beautifulsoup4' ] From d640e7d2ea99cc93da601289ae4ba6be897f17e5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 19 Jun 2018 13:49:10 +0200 Subject: [PATCH 13/14] Enabled pypy and pytest session caching, updated README --- .travis.yml | 6 +++++- README.rst | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1511593..b59cee8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - 3.4 - 3.5 - 3.6 +- pypy env: global: @@ -22,7 +23,10 @@ before_script: script: python -m pytest || python -m pytest --lf; # Run failed tests twice -cache: pip +cache: + pip: true + directories: + - .pytest_cache deploy: provider: pypi diff --git a/README.rst b/README.rst index a1d14ea..6409ab5 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: From 44ebf38e47fda3b77df365183577ed3e217afde3 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 20 Jun 2018 13:35:56 +0200 Subject: [PATCH 14/14] Updated `setup.py` and requirements, now we use `setup.cfg` --- .gitignore | 2 ++ .travis.yml | 6 +++- MANIFEST.in | 2 -- README.rst | 9 +++++ dev-requirements.txt | 2 ++ fbchat/__init__.py | 18 +++++----- requirements.txt | 1 - setup.cfg | 52 ++++++++++++++++++++++++++++ setup.py | 82 +++----------------------------------------- 9 files changed, 83 insertions(+), 91 deletions(-) create mode 100644 dev-requirements.txt create mode 100644 setup.cfg mode change 100644 => 100755 setup.py diff --git a/.gitignore b/.gitignore index dd3713c..cdfa971 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,11 @@ # Packages *.egg *.egg-info +*.dist-info dist build eggs +.eggs parts bin var diff --git a/.travis.yml b/.travis.yml index b59cee8..8f1ccbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,10 @@ env: - 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 @@ -33,7 +37,7 @@ deploy: user: madsmtm password: secure: "VA0MLSrwIW/T2KjMwjLZCzrLHw8pJT6tAvb48t7qpBdm8x192hax61pz1TaBZoJvlzyBPFKvluftuclTc7yEFwzXe7Gjqgd/ODKZl/wXDr36hQ7BBOLPZujdwmWLvTzMh3eJZlvkgcLCzrvK3j2oW8cM/+FZeVi/5/FhVuJ4ofs=" + distributions: sdist bdist_wheel on: - python: 3.6 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 6409ab5..441e203 100644 --- a/README.rst +++ b/README.rst @@ -31,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 988000a..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) +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/requirements.txt b/requirements.txt index 5e641fc..22069fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ requests lxml beautifulsoup4 enum34; python_version < '3.4' -six diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f6bb631 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,52 @@ +[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 + lxml + 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']})