diff --git a/docs/api.rst b/docs/api.rst index e08f19b..afebc78 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,7 +13,7 @@ If you are looking for information on a specific function, class, or method, thi Client ------ -This is the main class of `fbchat`, which contains all the methods you use to interract with Facebook. +This is the main class of `fbchat`, which contains all the methods you use to interact with Facebook. You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening) .. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO) diff --git a/docs/examples.rst b/docs/examples.rst index 89da9dc..9b4e682 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -18,7 +18,7 @@ This will show basic usage of `fbchat` Interacting with Threads ------------------------ -This will interract with the thread in every way `fbchat` supports +This will interact with the thread in every way `fbchat` supports .. literalinclude:: ../examples/interract.py diff --git a/docs/faq.rst b/docs/faq.rst index 16b8c59..d4bcc1a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -8,7 +8,7 @@ FAQ Version X broke my installation ------------------------------- -We try to provide backwards compatability where possible, but since we're not part of Facebook, +We try to provide backwards compatibility where possible, but since we're not part of Facebook, most of the things may be broken at any point in time Downgrade to an earlier version of fbchat, run this command diff --git a/docs/intro.rst b/docs/intro.rst index 6f5d58e..8fabddd 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -6,7 +6,7 @@ Introduction ============ `fbchat` uses your email and password to communicate with the Facebook server. -That means that you should always store your password in a seperate file, in case e.g. someone looks over your shoulder while you're writing code. +That means that you should always store your password in a separate file, in case e.g. someone looks over your shoulder while you're writing code. You should also make sure that the file's access control is appropriately restrictive @@ -16,7 +16,7 @@ Logging In ---------- Simply create an instance of :class:`Client`. If you have two factor authentication enabled, type the code in the terminal prompt -(If you want to supply the code in another fasion, overwrite :func:`Client.on2FACode`):: +(If you want to supply the code in another fashion, overwrite :func:`Client.on2FACode`):: from fbchat import Client from fbchat.models import * @@ -50,7 +50,7 @@ A thread can refer to two things: A Messenger group chat or a single Facebook us :class:`models.ThreadType` is an enumerator with two values: ``USER`` and ``GROUP``. These will specify whether the thread is a single user chat or a group chat. -This is required for many of `fbchat`'s functions, since Facebook differetiates between these two internally +This is required for many of `fbchat`'s functions, since Facebook differentiates between these two internally Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`, and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching` @@ -141,7 +141,7 @@ Sessions -------- `fbchat` provides functions to retrieve and set the session cookies. -This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script. +This will enable you to store the session cookies in a separate file, so that you don't have to login each time you start your script. Use :func:`Client.getSession` to retrieve the cookies:: session_cookies = client.getSession() diff --git a/docs/testing.rst b/docs/testing.rst index a3b2f13..f7d01a0 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -15,7 +15,7 @@ To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the Please remember to test all supported python versions. If you've made any changes to the 2FA functionality, test it with a 2FA enabled account. -If you only want to execute specific tests, pass the function names in the commandline (not including the `test_` prefix). Example:: +If you only want to execute specific tests, pass the function names in the command line (not including the `test_` prefix). Example:: $ python tests.py sendMessage sessions sendEmoji diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 25dd5d4..b9efe60 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -15,7 +15,7 @@ from __future__ import unicode_literals from .client import * __title__ = 'fbchat' -__version__ = '1.4.0' +__version__ = '1.4.2' __description__ = 'Facebook Chat (Messenger) for Python' __copyright__ = 'Copyright 2015 - 2018 by Taehoon Kim' diff --git a/fbchat/client.py b/fbchat/client.py index 9a17620..ade6a8f 100644 --- a/fbchat/client.py +++ b/fbchat/client.py @@ -168,10 +168,12 @@ class Client(object): def graphql_requests(self, *queries): """ - .. todo:: - Documenting this + :param queries: Zero or more GraphQL objects + :type queries: GraphQL :raises: FBchatException if request failed + :return: A tuple containing json graphql queries + :rtype: tuple """ return tuple(self._graphql({ @@ -238,22 +240,6 @@ class Client(object): self.payloadDefault['ttstamp'] = self.ttstamp self.payloadDefault['fb_dtsg'] = self.fb_dtsg - self.form = { - 'channel' : self.user_channel, - 'partition' : '-2', - 'clientid' : self.client_id, - 'viewer_uid' : self.uid, - 'uid' : self.uid, - 'state' : 'active', - 'format' : 'json', - 'idle' : 0, - 'cap' : '8' - } - - self.prev = now() - self.tmp_prev = now() - self.last_sync = now() - def _login(self): if not (self.email and self.password): raise FBchatUserError("Email and password not found.") @@ -457,7 +443,8 @@ class Client(object): return given_thread_id, given_thread_type def setDefaultThread(self, thread_id, thread_type): - """Sets default thread to send messages to + """ + Sets default thread to send messages to :param thread_id: User/Group ID to default to. See :ref:`intro_threads` :param thread_type: See :ref:`intro_threads` @@ -515,7 +502,7 @@ class Client(object): return users - def searchForUsers(self, name, limit=1): + def searchForUsers(self, name, limit=10): """ Find and get user by his/her name @@ -530,7 +517,7 @@ class Client(object): return [graphql_to_user(node) for node in j[name]['users']['nodes']] - def searchForPages(self, name, limit=1): + def searchForPages(self, name, limit=10): """ Find and get page by its name @@ -544,7 +531,7 @@ class Client(object): return [graphql_to_page(node) for node in j[name]['pages']['nodes']] - def searchForGroups(self, name, limit=1): + def searchForGroups(self, name, limit=10): """ Find and get group thread by its name @@ -559,7 +546,7 @@ class Client(object): return [graphql_to_group(node) for node in j['viewer']['groups']['nodes']] - def searchForThreads(self, name, limit=1): + def searchForThreads(self, name, limit=10): """ Find and get a thread by its name @@ -850,14 +837,22 @@ class Client(object): 'id': thread_id, 'message_limit': limit, 'load_messages': True, - 'load_read_receipts': False, + 'load_read_receipts': True, 'before': before })) if j.get('message_thread') is None: raise FBchatException('Could not fetch thread {}: {}'.format(thread_id, j)) - return list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) + messages = list(reversed([graphql_to_message(message) for message in j['message_thread']['messages']['nodes']])) + read_receipts = j['message_thread']['read_receipts']['nodes'] + + for message in messages: + for receipt in read_receipts: + if int(receipt['watermark']) >= int(message.timestamp): + message.read_by.append(receipt['actor']['id']) + + return messages def fetchThreadList(self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None): """Get thread list of your facebook account @@ -1801,7 +1796,7 @@ class Client(object): } for thread_id in thread_ids: - data["ids[{}]".format(thread_id)] = read + data["ids[{}]".format(thread_id)] = 'true' if read else 'false' r = self._post(self.req_url.READ_STATUS, data) return r.ok @@ -2047,48 +2042,32 @@ class Client(object): LISTEN METHODS """ - def _ping(self, sticky, pool): + def _ping(self): data = { 'channel': self.user_channel, 'clientid': self.client_id, 'partition': -2, 'cap': 0, 'uid': self.uid, - 'sticky_token': sticky, - 'sticky_pool': pool, + 'sticky_token': self.sticky, + 'sticky_pool': self.pool, 'viewer_uid': self.uid, 'state': 'active', } self._get(self.req_url.PING, data, fix_request=True, as_json=False) - def _fetchSticky(self): - """Call pull api to get sticky and pool parameter, newer api needs these parameters to work""" - - data = { - "msgs_recv": 0, - "channel": self.user_channel, - "clientid": self.client_id - } - - j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) - - if j.get('lb_info') is None: - raise FBchatException('Missing lb_info: {}'.format(j)) - - return j['lb_info']['sticky'], j['lb_info']['pool'] - - def _pullMessage(self, sticky, pool, markAlive=True): + def _pullMessage(self, markAlive=True): """Call pull api with seq value to get message data.""" data = { "msgs_recv": 0, - "sticky_token": sticky, - "sticky_pool": pool, + "sticky_token": self.sticky, + "sticky_pool": self.pool, "clientid": self.client_id, 'state': 'active' if markAlive else 'offline', } - j = self._get(ReqUrl.STICKY, data, fix_request=True, as_json=True) + j = self._get(self.req_url.STICKY, data, fix_request=True, as_json=True) self.seq = j.get('seq', '0') return j @@ -2096,6 +2075,14 @@ class Client(object): def _parseMessage(self, content): """Get message and author name from content. May contain multiple messages in the content.""" + if 'lb_info' in content: + self.sticky = content['lb_info']['sticky'] + self.pool = content['lb_info']['pool'] + + if 'batches' in content: + for batch in content['batches']: + self._parseMessage(batch) + if 'ms' not in content: return for m in content["ms"]: @@ -2505,7 +2492,6 @@ class Client(object): :raises: FBchatException if request failed """ self.listening = True - self.sticky, self.pool = self._fetchSticky() def doOneListen(self, markAlive=True): """ @@ -2519,8 +2505,8 @@ class Client(object): """ try: if markAlive: - self._ping(self.sticky, self.pool) - content = self._pullMessage(self.sticky, self.pool, markAlive) + self._ping() + content = self._pullMessage(markAlive) if content: self._parseMessage(content) except KeyboardInterrupt: diff --git a/fbchat/graphql.py b/fbchat/graphql.py index 362134c..641f1f6 100644 --- a/fbchat/graphql.py +++ b/fbchat/graphql.py @@ -381,8 +381,8 @@ def graphql_to_group(group): color=c_info.get('color'), emoji=c_info.get('emoji'), admins = set([node.get('id') for node in group.get('thread_admins')]), - approval_mode = bool(group.get('approval_mode')), - approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']), + approval_mode = bool(group.get('approval_mode')) if group.get('approval_mode') is not None else None, + approval_requests = set(node["requester"]['id'] for node in group['group_approval_queue']['nodes']) if group.get('group_approval_queue') else None, join_link = group['joinable_mode'].get('link'), photo=group['image'].get('uri'), name=group.get('name'), @@ -556,7 +556,7 @@ class GraphQL(object): """ SEARCH_USER = """ - Query SearchUser( = '', = 1) { + Query SearchUser( = '', = 10) { entities_named() { search_results.of_type(user).first() as users { nodes { @@ -568,7 +568,7 @@ class GraphQL(object): """ + FRAGMENT_USER SEARCH_GROUP = """ - Query SearchGroup( = '', = 1, = 32) { + Query SearchGroup( = '', = 10, = 32) { viewer() { message_threads.with_thread_name().last() as groups { nodes { @@ -580,7 +580,7 @@ class GraphQL(object): """ + FRAGMENT_GROUP SEARCH_PAGE = """ - Query SearchPage( = '', = 1) { + Query SearchPage( = '', = 10) { entities_named() { search_results.of_type(page).first() as pages { nodes { @@ -592,7 +592,7 @@ class GraphQL(object): """ + FRAGMENT_PAGE SEARCH_THREAD = """ - Query SearchThread( = '', = 1) { + Query SearchThread( = '', = 10) { entities_named() { search_results.first() as threads { nodes { diff --git a/fbchat/models.py b/fbchat/models.py index 55b6407..d1d59d8 100644 --- a/fbchat/models.py +++ b/fbchat/models.py @@ -180,6 +180,8 @@ class Message(object): timestamp = None #: Whether the message is read is_read = None + #: A list of pepole IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages` + read_by = None #: A dict with user's IDs as keys, and their :class:`MessageReaction` as values reactions = None #: The actual message @@ -203,6 +205,7 @@ class Message(object): attachments = [] self.attachments = attachments self.reactions = {} + self.read_by = [] self.deleted = False def __repr__(self): diff --git a/tests/conftest.py b/tests/conftest.py index af40730..014b5a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,9 @@ def group(pytestconfig): return {"id": load_variable("group_id", pytestconfig.cache), "type": ThreadType.GROUP} -@pytest.fixture(scope="session", params=["user", "group", pytest.mark.xfail("none")]) +@pytest.fixture(scope="session", params=[ + "user", "group", pytest.param("none", marks=[pytest.mark.xfail()]) +]) def thread(request, user, group): return { "user": user, diff --git a/tests/test_plans.py b/tests/test_plans.py index d16c153..9365d4a 100644 --- a/tests/test_plans.py +++ b/tests/test_plans.py @@ -11,8 +11,14 @@ from time import time @pytest.fixture(scope="module", params=[ Plan(int(time()) + 100, random_hex()), - pytest.mark.xfail(Plan(int(time()), random_hex()), raises=FBchatFacebookError), - pytest.mark.xfail(Plan(0, None)), + pytest.param( + Plan(int(time()), random_hex()), + marks=[pytest.mark.xfail(raises=FBchatFacebookError)] + ), + pytest.param( + Plan(0, None), + marks=[pytest.mark.xfail()], + ), ]) def plan_data(request, client, user, thread, catch_event, compare): with catch_event("onPlanCreated") as x: diff --git a/tests/test_polls.py b/tests/test_polls.py index 96dab76..96de743 100644 --- a/tests/test_polls.py +++ b/tests/test_polls.py @@ -26,7 +26,9 @@ from utils import random_hex, subset PollOption(random_hex()), PollOption(random_hex()), ]), - pytest.mark.xfail(Poll(title=None, options=[]), raises=ValueError), + pytest.param( + Poll(title=None, options=[]), marks=[pytest.mark.xfail(raises=ValueError)] + ), ]) def poll_data(request, client1, group, catch_event): with catch_event("onPollCreated") as x: diff --git a/tests/test_thread_interraction.py b/tests/test_thread_interraction.py index 16e7e9e..be5b0ff 100644 --- a/tests/test_thread_interraction.py +++ b/tests/test_thread_interraction.py @@ -72,8 +72,8 @@ def test_change_nickname(client, client_all, catch_event, compare): "😂", "😕", "😍", - pytest.mark.xfail("🙃", raises=FBchatFacebookError), - pytest.mark.xfail("not an emoji", raises=FBchatFacebookError) + pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), + pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), ]) def test_change_emoji(client, catch_event, compare, emoji): with catch_event("onEmojiChange") as x: @@ -101,7 +101,7 @@ def test_change_image_remote(client1, group, catch_event): [ x if x in [ThreadColor.MESSENGER_BLUE, ThreadColor.PUMPKIN] - else pytest.mark.expensive(x) + else pytest.param(x, marks=[pytest.mark.expensive()]) for x in ThreadColor ], ) diff --git a/tests/utils.py b/tests/utils.py index 51364cb..241a6ad 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,15 +23,15 @@ EMOJI_LIST = [ ("😆", EmojiSize.LARGE), # These fail in `catch_event` 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)), + pytest.param(None, EmojiSize.SMALL, marks=[pytest.mark.xfail()]), + pytest.param(None, EmojiSize.MEDIUM, marks=[pytest.mark.xfail()]), + pytest.param(None, EmojiSize.LARGE, marks=[pytest.mark.xfail()]), ] STICKER_LIST = [ Sticker("767334476626295"), - pytest.mark.xfail(Sticker("0"), raises=FBchatFacebookError), - pytest.mark.xfail(Sticker(None), raises=FBchatFacebookError), + pytest.param(Sticker("0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), + pytest.param(Sticker(None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), ] TEXT_LIST = [ @@ -40,8 +40,8 @@ TEXT_LIST = [ "\\\n\t%?&'\"", "ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط", "a" * 20000, # Maximum amount of characters you can send - pytest.mark.xfail("a" * 20001, raises=FBchatFacebookError), - pytest.mark.xfail(None, raises=FBchatFacebookError), + pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), + pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]), ]