From c70a39c568c937bab94e4b23853159f90afb26b5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 8 Sep 2019 15:50:32 +0200 Subject: [PATCH 1/8] Remove deprecated arguments, methods, and classes --- fbchat/__init__.py | 2 +- fbchat/_client.py | 89 ++-------------------------------------------- fbchat/_group.py | 13 ------- fbchat/_thread.py | 2 -- 4 files changed, 3 insertions(+), 103 deletions(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index c8c5831..0b552fe 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -14,7 +14,7 @@ from . import _core, _util from ._exception import FBchatException, FBchatFacebookError, FBchatUserError from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread from ._user import TypingStatus, User, ActiveStatus -from ._group import Group, Room +from ._group import Group from ._page import Page from ._message import EmojiSize, MessageReaction, Mention, Message from ._attachment import Attachment, UnsentMessage, ShareAttachment diff --git a/fbchat/_client.py b/fbchat/_client.py index bf6308e..14652f9 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -789,12 +789,11 @@ class Client: return messages def fetchThreadList( - self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None + self, limit=20, thread_location=ThreadLocation.INBOX, before=None ): """Fetch the client's thread list. Args: - offset: Deprecated. Do not use! limit (int): Max. number of threads to retrieve. Capped at 20 thread_location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER before (datetime.datetime): The point from which to retrieve threads @@ -805,13 +804,6 @@ class Client: Raises: FBchatException: If request failed """ - if offset is not None: - log.warning( - "Using `offset` in `fetchThreadList` is no longer supported, " - "since Facebook migrated to the use of GraphQL in this request. " - "Use `before` instead." - ) - if limit > 20 or limit < 1: raise FBchatUserError("`limit` should be between 1 and 20") @@ -1063,26 +1055,6 @@ class Client: data.update(message._to_send_data()) return self._doSendRequest(data) - def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER): - """Deprecated. Use :func:`fbchat.Client.send` instead.""" - return self.send( - Message(text=message), thread_id=thread_id, thread_type=thread_type - ) - - def sendEmoji( - self, - emoji=None, - size=EmojiSize.SMALL, - thread_id=None, - thread_type=ThreadType.USER, - ): - """Deprecated. Use :func:`fbchat.Client.send` instead.""" - return self.send( - Message(text=emoji, emoji_size=size), - thread_id=thread_id, - thread_type=thread_type, - ) - def wave(self, wave_first=True, thread_id=None, thread_type=None): """Wave hello to a thread. @@ -1337,48 +1309,6 @@ class Client: files=files, message=message, thread_id=thread_id, thread_type=thread_type ) - def sendImage( - self, - image_id, - message=None, - thread_id=None, - thread_type=ThreadType.USER, - is_gif=False, - ): - """Deprecated.""" - if is_gif: - mimetype = "image/gif" - else: - mimetype = "image/png" - return self._sendFiles( - files=[(image_id, mimetype)], - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) - - def sendRemoteImage( - self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Deprecated. Use :func:`fbchat.Client.sendRemoteFiles` instead.""" - return self.sendRemoteFiles( - file_urls=[image_url], - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) - - def sendLocalImage( - self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER - ): - """Deprecated. Use :func:`fbchat.Client.sendLocalFiles` instead.""" - return self.sendLocalFiles( - file_paths=[image_path], - message=message, - thread_id=thread_id, - thread_type=thread_type, - ) - def forwardAttachment(self, attachment_id, thread_id=None): """Forward an attachment. @@ -1801,11 +1731,6 @@ class Client: } j = self._payload_post("/ajax/eventreminder/rsvp", data) - def eventReminder(self, thread_id, time, title, location="", location_id=""): - """Deprecated. Use :func:`fbchat.Client.createPlan` instead.""" - plan = Plan(time=time, title=title, location=location, location_id=location_id) - self.createPlan(plan=plan, thread_id=thread_id) - def createPoll(self, poll, thread_id=None): """Create poll in a group thread. @@ -2728,7 +2653,6 @@ class Client: self.onMessage( mid=message.uid, author_id=message.author, - message=message.text, message_object=message, thread_id=thread_id, thread_type=thread_type, @@ -2743,7 +2667,6 @@ class Client: self.onMessage( mid=mid, author_id=author_id, - message=delta.get("body", ""), message_object=Message._from_pull( delta, mid=mid, @@ -2877,21 +2800,15 @@ class Client: """ self.listening = True - def doOneListen(self, markAlive=None): + def doOneListen(self): """Do one cycle of the listening loop. This method is useful if you want to control the client from an external event loop. - Warning: - ``markAlive`` parameter is deprecated, use :func:`Client.setActiveStatus` - or ``markAlive`` parameter in :func:`Client.listen` instead. - Returns: bool: Whether the loop should keep running """ - if markAlive is not None: - self._markAlive = markAlive try: if self._markAlive: self._ping() @@ -2996,7 +2913,6 @@ class Client: self, mid=None, author_id=None, - message=None, message_object=None, thread_id=None, thread_type=ThreadType.USER, @@ -3009,7 +2925,6 @@ class Client: Args: mid: The message ID author_id: The ID of the author - message: (deprecated. Use ``message_object.text`` instead) message_object (Message): The message (As a `Message` object) thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` thread_type (ThreadType): Type of thread that the message was sent to. See :ref:`intro_threads` diff --git a/fbchat/_group.py b/fbchat/_group.py index c0eeddd..d8eea84 100644 --- a/fbchat/_group.py +++ b/fbchat/_group.py @@ -103,16 +103,3 @@ class Group(Thread): def _to_send_data(self): return {"thread_fbid": self.uid} - - -@attr.s(cmp=False, init=False) -class Room(Group): - """Deprecated. Use `Group` instead.""" - - # True is room is not discoverable - privacy_mode = attr.ib(None) - - def __init__(self, uid, privacy_mode=None, **kwargs): - super(Room, self).__init__(uid, **kwargs) - self.type = ThreadType.ROOM - self.privacy_mode = privacy_mode diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 6cc444c..0659257 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -10,7 +10,6 @@ class ThreadType(Enum): USER = 1 GROUP = 2 - ROOM = 2 PAGE = 3 def _to_class(self): @@ -20,7 +19,6 @@ class ThreadType(Enum): return { ThreadType.USER: _user.User, ThreadType.GROUP: _group.Group, - ThreadType.ROOM: _group.Room, ThreadType.PAGE: _page.Page, }[self] From 7c758501fce8c0849f330c9248c33b3a7271678b Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 30 Aug 2019 17:48:05 +0200 Subject: [PATCH 2/8] Remove methods to set the default thread This has been done to value explicitness over implicitness, and also since the question of whether thread_id=None is acceptable was dependent on mutable variables in Client. --- fbchat/_client.py | 81 ---------------------------------------------- tests/conftest.py | 6 ++-- tests/test_base.py | 10 ------ 3 files changed, 4 insertions(+), 93 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 14652f9..5854f8f 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -87,8 +87,6 @@ class Client: """ self._sticky, self._pool = (None, None) self._seq = "0" - self._default_thread_id = None - self._default_thread_type = None self._pull_channel = 0 self._markAlive = True self._buddylist = dict() @@ -235,45 +233,6 @@ class Client: END LOGIN METHODS """ - """ - DEFAULT THREAD METHODS - """ - - def _getThread(self, given_thread_id=None, given_thread_type=None): - """Check if thread ID is given and if default is set, and return correct values. - - Returns: - tuple: Thread ID and thread type - - Raises: - ValueError: If thread ID is not given and there is no default - """ - if given_thread_id is None: - if self._default_thread_id is not None: - return self._default_thread_id, self._default_thread_type - else: - raise ValueError("Thread ID is not set") - else: - return given_thread_id, given_thread_type - - def setDefaultThread(self, thread_id, thread_type): - """Set default thread to send messages to. - - Args: - thread_id: User/Group ID to default to. See :ref:`intro_threads` - thread_type (ThreadType): See :ref:`intro_threads` - """ - self._default_thread_id = thread_id - self._default_thread_type = thread_type - - def resetDefaultThread(self): - """Reset default thread.""" - self.setDefaultThread(None, None) - - """ - END DEFAULT THREAD METHODS - """ - """ FETCH METHODS """ @@ -494,8 +453,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = { "query": query, "snippetOffset": offset, @@ -756,8 +713,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - params = { "id": thread_id, "message_limit": limit, @@ -902,7 +857,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) message_info = self._forcedFetch(thread_id, mid).get("message") return Message._from_graphql(message_info) @@ -987,7 +941,6 @@ class Client: Returns: typing.Iterable: :class:`ImageAttachment` or :class:`VideoAttachment` """ - thread_id, thread_type = self._getThread(thread_id, None) data = {"id": thread_id, "first": 48} thread_id = str(thread_id) j, = self.graphql_requests(_graphql.from_query_id("515216185516880", data)) @@ -1049,7 +1002,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) thread = thread_type._to_class()(thread_id) data = thread._to_send_data() data.update(message._to_send_data()) @@ -1069,7 +1021,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) thread = thread_type._to_class()(thread_id) data = thread._to_send_data() data["action_type"] = "ma-type:user-generated-message" @@ -1134,7 +1085,6 @@ class Client: def _sendLocation( self, location, current=True, message=None, thread_id=None, thread_type=None ): - thread_id, thread_type = self._getThread(thread_id, thread_type) thread = thread_type._to_class()(thread_id) data = thread._to_send_data() if message is not None: @@ -1203,7 +1153,6 @@ class Client: `files` should be a list of tuples, with a file's ID and mimetype. """ - thread_id, thread_type = self._getThread(thread_id, thread_type) thread = thread_type._to_class()(thread_id) data = thread._to_send_data() data.update(self._oldMessage(message)._to_send_data()) @@ -1319,7 +1268,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) data = { "attachment_id": attachment_id, "recipient_map[{}]".format(_util.generateOfflineThreadingID()): thread_id, @@ -1369,7 +1317,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) data = Group(thread_id)._to_send_data() data["action_type"] = "ma-type:log-message" @@ -1399,14 +1346,10 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = {"uid": user_id, "tid": thread_id} j = self._payload_post("/chat/remove_participants/", data) def _adminStatus(self, admin_ids, admin, thread_id=None): - thread_id, thread_type = self._getThread(thread_id, None) - data = {"add": admin, "thread_fbid": thread_id} admin_ids = _util.require_list(admin_ids) @@ -1450,14 +1393,10 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} j = self._payload_post("/messaging/set_approval_mode/?dpr=1", data) def _usersApproval(self, user_ids, approve, thread_id=None): - thread_id, thread_type = self._getThread(thread_id, None) - user_ids = _util.require_list(user_ids) data = { @@ -1506,8 +1445,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = {"thread_image_id": image_id, "thread_id": thread_id} j = self._payload_post("/messaging/set_thread_image/?dpr=1", data) @@ -1555,8 +1492,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - if thread_type == ThreadType.USER: # The thread is a user, so we change the user's nickname return self.changeNickname( @@ -1580,8 +1515,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - data = { "nickname": nickname, "participant_id": user_id, @@ -1601,8 +1534,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = { "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", "thread_or_other_fbid": thread_id, @@ -1625,8 +1556,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} j = self._payload_post( "/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data @@ -1663,8 +1592,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - data = { "event_type": "EVENT", "event_time": _util.datetime_to_seconds(plan.time), @@ -1741,8 +1668,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) - # We're using ordered dictionaries, because the Facebook endpoint that parses # the POST parameters is badly implemented, and deals with ordering the options # wrongly. If you can find a way to fix this for the endpoint, or if you find @@ -1799,8 +1724,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, thread_type) - data = { "typ": status.value, "thread": thread_id, @@ -2012,7 +1935,6 @@ class Client: Raises: FBchatException: If request failed """ - thread_id, thread_type = self._getThread(thread_id, None) j = self._payload_post("/ajax/mercury/mark_spam.php?dpr=1", {"id": thread_id}) return True @@ -2042,7 +1964,6 @@ class Client: mute_time (datetime.timedelta): Time to mute, use ``None`` to mute forever thread_id: User/Group ID to mute. See :ref:`intro_threads` """ - thread_id, thread_type = self._getThread(thread_id, None) if mute_time is None: mute_settings = -1 else: @@ -2065,7 +1986,6 @@ class Client: mute: Boolean. True to mute, False to unmute thread_id: User/Group ID to mute. See :ref:`intro_threads` """ - thread_id, thread_type = self._getThread(thread_id, None) data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} j = self._payload_post( "/ajax/mercury/change_reactions_mute_thread/?dpr=1", data @@ -2086,7 +2006,6 @@ class Client: mute: Boolean. True to mute, False to unmute thread_id: User/Group ID to mute. See :ref:`intro_threads` """ - thread_id, thread_type = self._getThread(thread_id, None) data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} j = self._payload_post("/ajax/mercury/change_mentions_mute_thread/?dpr=1", data) diff --git a/tests/conftest.py b/tests/conftest.py index 53c4773..f2a8a34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,9 +45,11 @@ def client2(pytestconfig): @pytest.fixture(scope="module") def client(client1, thread): - client1.setDefaultThread(thread["id"], thread["type"]) + # TODO: These are commented out since `setDefaultThread` is removed - But now any + # test that use this fixture is likely broken! + # client1.setDefaultThread(thread["id"], thread["type"]) yield client1 - client1.resetDefaultThread() + # client1.resetDefaultThread() @pytest.fixture(scope="session", params=["client1", "client2"]) diff --git a/tests/test_base.py b/tests/test_base.py index 25aa94b..1528968 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -38,13 +38,3 @@ def test_sessions(client1): 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")) From 41557753056df959dc6122fe1c3d8c9892e9f418 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 30 Aug 2019 17:49:52 +0200 Subject: [PATCH 3/8] Remove ssl_verify property Only used when debugging, and in that case, the functionality could be implemented using private APIs. --- fbchat/_client.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 5854f8f..70335e9 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -49,19 +49,6 @@ class Client: Used when creating an external event loop to determine when to stop listening. """ - @property - def ssl_verify(self): - """Verify SSL certificate. - - Set to False to allow debugging with a proxy. - """ - # TODO: Deprecate this - return self._state._session.verify - - @ssl_verify.setter - def ssl_verify(self, value): - self._state._session.verify = value - @property def uid(self): """The ID of the client. From 91cf4589a5ac3a4dd45f93ed65285cb49e0a9f9c Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 30 Aug 2019 18:05:24 +0200 Subject: [PATCH 4/8] Remove ability to set a custom User-Agent This causes issues if the User-Agent is set to resemble a mobile phone, see #431, and besides, it's not an API surface I want / need to support. --- fbchat/_client.py | 22 +++++++--------------- fbchat/_state.py | 12 ++++++------ tests/utils.py | 1 - 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 70335e9..a89edb9 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -57,15 +57,12 @@ class Client: """ return self._uid - def __init__( - self, email, password, user_agent=None, max_tries=5, session_cookies=None - ): + def __init__(self, email, password, max_tries=5, session_cookies=None): """Initialize and log in the client. Args: email: Facebook ``email``, ``id`` or ``phone number`` password: Facebook account password - user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list max_tries (int): Maximum number of times to try logging in session_cookies (dict): Cookies from a previous session (Will default to login if these are invalid) @@ -81,10 +78,10 @@ class Client: # If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login if ( not session_cookies - or not self.setSession(session_cookies, user_agent=user_agent) + or not self.setSession(session_cookies) or not self.isLoggedIn() ): - self.login(email, password, max_tries, user_agent=user_agent) + self.login(email, password, max_tries) """ INTERNAL REQUEST METHODS @@ -145,7 +142,7 @@ class Client: """ return self._state.get_cookies() - def setSession(self, session_cookies, user_agent=None): + def setSession(self, session_cookies): """Load session cookies. Args: @@ -156,16 +153,14 @@ class Client: """ try: # Load cookies into current session - self._state = _state.State.from_cookies( - session_cookies, user_agent=user_agent - ) + self._state = _state.State.from_cookies(session_cookies) self._uid = self._state.user_id except Exception as e: log.exception("Failed loading session") return False return True - def login(self, email, password, max_tries=5, user_agent=None): + def login(self, email, password, max_tries=5): """Login the user, using ``email`` and ``password``. If the user is already logged in, this will do a re-login. @@ -189,10 +184,7 @@ class Client: for i in range(1, max_tries + 1): try: self._state = _state.State.login( - email, - password, - on_2fa_callback=self.on2FACode, - user_agent=user_agent, + email, password, on_2fa_callback=self.on2FACode ) self._uid = self._state.user_id except Exception: diff --git a/fbchat/_state.py b/fbchat/_state.py index e11e2f6..eb9caa1 100644 --- a/fbchat/_state.py +++ b/fbchat/_state.py @@ -23,11 +23,11 @@ def find_input_fields(html): return bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("input")) -def session_factory(user_agent=None): +def session_factory(): session = requests.session() session.headers["Referer"] = "https://www.facebook.com" # TODO: Deprecate setting the user agent manually - session.headers["User-Agent"] = user_agent or random.choice(_util.USER_AGENTS) + session.headers["User-Agent"] = random.choice(_util.USER_AGENTS) return session @@ -120,8 +120,8 @@ class State: } @classmethod - def login(cls, email, password, on_2fa_callback, user_agent=None): - session = session_factory(user_agent=user_agent) + def login(cls, email, password, on_2fa_callback): + session = session_factory() soup = find_input_fields(session.get("https://m.facebook.com/").text) data = dict( @@ -201,8 +201,8 @@ class State: return self._session.cookies.get_dict() @classmethod - def from_cookies(cls, cookies, user_agent=None): - session = session_factory(user_agent=user_agent) + def from_cookies(cls, cookies): + session = session_factory() session.cookies = requests.cookies.merge_cookies(session.cookies, cookies) return cls.from_session(session=session) diff --git a/tests/utils.py b/tests/utils.py index 440001c..23a8b5b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -93,7 +93,6 @@ def load_client(n, cache): client = Client( load_variable("client{}_email".format(n), cache), load_variable("client{}_password".format(n), cache), - user_agent="Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", session_cookies=cache.get("client{}_session".format(n), None), max_tries=1, ) From b5a37e35c620c92f47217b3bdf790ab06325f9ae Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 30 Aug 2019 18:06:05 +0200 Subject: [PATCH 5/8] Remove FBchatUserError in favor of builtin exceptions --- .github/ISSUE_TEMPLATE/bug_report.md | 4 ++-- docs/api.rst | 1 - fbchat/__init__.py | 2 +- fbchat/_client.py | 22 +++++++++++----------- fbchat/_exception.py | 4 ---- fbchat/_state.py | 2 +- tests/test_base.py | 4 ++-- 7 files changed, 17 insertions(+), 22 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 40e7f5b..453d42c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,8 +21,8 @@ Traceback (most recent call last): File "[site-packages]/fbchat/client.py", line 78, in __init__ self.login(email, password, max_tries) File "[site-packages]/fbchat/client.py", line 407, in login - raise FBchatUserError('Login failed. Check email/password. (Failed on URL: {})'.format(login_url)) -fbchat.models.FBchatUserError: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1) + raise FBchatException('Login failed. Check email/password. (Failed on URL: {})'.format(login_url)) +fbchat.FBchatException: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1) ``` ## Environment information diff --git a/docs/api.rst b/docs/api.rst index 3d9a920..83a9466 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -38,7 +38,6 @@ Exceptions .. autoexception:: FBchatException() .. autoexception:: FBchatFacebookError() -.. autoexception:: FBchatUserError() Attachments ----------- diff --git a/fbchat/__init__.py b/fbchat/__init__.py index 0b552fe..36d1659 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -11,7 +11,7 @@ _logging.getLogger(__name__).addHandler(_logging.NullHandler()) # The order of these is somewhat significant, e.g. User has to be imported after Thread! from . import _core, _util -from ._exception import FBchatException, FBchatFacebookError, FBchatUserError +from ._exception import FBchatException, FBchatFacebookError from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread from ._user import TypingStatus, User, ActiveStatus from ._group import Group diff --git a/fbchat/_client.py b/fbchat/_client.py index a89edb9..d85e0d8 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -7,7 +7,7 @@ from collections import OrderedDict from ._core import log from . import _util, _graphql, _state -from ._exception import FBchatException, FBchatFacebookError, FBchatUserError +from ._exception import FBchatException, FBchatFacebookError from ._thread import ThreadType, ThreadLocation, ThreadColor from ._user import TypingStatus, User, ActiveStatus from ._group import Group @@ -176,10 +176,10 @@ class Client: self.onLoggingIn(email=email) if max_tries < 1: - raise FBchatUserError("Cannot login: max_tries should be at least one") + raise ValueError("Cannot login: max_tries should be at least one") if not (email and password): - raise FBchatUserError("Email and password not set") + raise ValueError("Email and password not set") for i in range(1, max_tries + 1): try: @@ -560,7 +560,7 @@ class Client: if thread.type == ThreadType.USER: users[id_] = thread else: - raise FBchatUserError("Thread {} was not a user".format(thread)) + raise ValueError("Thread {} was not a user".format(thread)) return users @@ -585,7 +585,7 @@ class Client: if thread.type == ThreadType.PAGE: pages[id_] = thread else: - raise FBchatUserError("Thread {} was not a page".format(thread)) + raise ValueError("Thread {} was not a page".format(thread)) return pages @@ -607,7 +607,7 @@ class Client: if thread.type == ThreadType.GROUP: groups[id_] = thread else: - raise FBchatUserError("Thread {} was not a group".format(thread)) + raise ValueError("Thread {} was not a group".format(thread)) return groups @@ -739,12 +739,12 @@ class Client: FBchatException: If request failed """ if limit > 20 or limit < 1: - raise FBchatUserError("`limit` should be between 1 and 20") + raise ValueError("`limit` should be between 1 and 20") if thread_location in ThreadLocation: loc_str = thread_location.value else: - raise FBchatUserError('"thread_location" must be a value of ThreadLocation') + raise TypeError('"thread_location" must be a value of ThreadLocation') params = { "limit": limit, @@ -1033,7 +1033,7 @@ class Client: ) elif isinstance(quick_reply, QuickReplyLocation): if not isinstance(payload, LocationAttachment): - raise ValueError( + raise TypeError( "Payload must be an instance of `fbchat.LocationAttachment`" ) return self.sendLocation( @@ -1274,7 +1274,7 @@ class Client: data = self._oldMessage(message)._to_send_data() if len(user_ids) < 2: - raise FBchatUserError("Error when creating group: Not enough participants") + raise ValueError("Error when creating group: Not enough participants") for i, user_id in enumerate(user_ids + [self._uid]): data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id) @@ -1305,7 +1305,7 @@ class Client: for i, user_id in enumerate(user_ids): if user_id == self._uid: - raise FBchatUserError( + raise ValueError( "Error when adding users: Cannot add self to group thread" ) else: diff --git a/fbchat/_exception.py b/fbchat/_exception.py index c28d68b..b84c5b9 100644 --- a/fbchat/_exception.py +++ b/fbchat/_exception.py @@ -50,7 +50,3 @@ class FBchatPleaseRefresh(FBchatFacebookError): fb_error_code = "1357004" fb_error_message = "Please try closing and re-opening your browser window." - - -class FBchatUserError(FBchatException): - """Thrown by ``fbchat`` when wrong values are entered.""" diff --git a/fbchat/_state.py b/fbchat/_state.py index eb9caa1..f8b489a 100644 --- a/fbchat/_state.py +++ b/fbchat/_state.py @@ -147,7 +147,7 @@ class State: if is_home(r.url): return cls.from_session(session=session) else: - raise _exception.FBchatUserError( + raise _exception.FBchatException( "Login failed. Check email/password. " "(Failed on url: {})".format(r.url) ) diff --git a/tests/test_base.py b/tests/test_base.py index 1528968..6cdf43a 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,7 +3,7 @@ import py_compile from glob import glob from os import path, environ -from fbchat import FBchatUserError, Message, Client +from fbchat import FBchatException, Message, Client @pytest.mark.offline @@ -24,7 +24,7 @@ def test_login(client1): assert not client1.isLoggedIn() - with pytest.raises(FBchatUserError): + with pytest.raises(FBchatException): client1.login("", "", max_tries=1) client1.login(email, password) From 650112a59204b01bc2123ff3e55788f59b3462a5 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 8 Sep 2019 15:52:21 +0200 Subject: [PATCH 6/8] Remove automatic fb_dtsg refreshing This was error prone, inefficient and wouldn't handle all error cases. The real solution is to make some way to retry the request in the general case (since you can alway just get logged out), and that's probably out of scope for this project, at least right now. :/ --- fbchat/_client.py | 6 +++++- fbchat/_state.py | 48 +++++++---------------------------------------- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index d85e0d8..dc66c51 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -817,6 +817,7 @@ class Client: image_id = str(image_id) data = {"photo_id": str(image_id)} j = self._post("/mercury/attachments/photo/", data) + _util.handle_payload_error(j) url = _util.get_jsmods_require(j, 3) if url is None: @@ -2017,6 +2018,7 @@ class Client: "https://{}-edge-chat.facebook.com/active_ping".format(self._pull_channel), data, ) + _util.handle_payload_error(j) def _pullMessage(self): """Call pull api to fetch message data.""" @@ -2028,9 +2030,11 @@ class Client: "clientid": self._state._client_id, "state": "active" if self._markAlive else "offline", } - return self._get( + j = self._get( "https://{}-edge-chat.facebook.com/pull".format(self._pull_channel), data ) + _util.handle_payload_error(j) + return j def _parseDelta(self, m): def getThreadIdAndThreadType(msg_metadata): diff --git a/fbchat/_state.py b/fbchat/_state.py index f8b489a..5215229 100644 --- a/fbchat/_state.py +++ b/fbchat/_state.py @@ -206,58 +206,24 @@ class State: session.cookies = requests.cookies.merge_cookies(session.cookies, cookies) return cls.from_session(session=session) - def _do_refresh(self): - # TODO: Raise the error instead, and make the user do the refresh manually - # It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it! - log.warning("Refreshing state and resending request") - new = State.from_session(session=self._session) - self.user_id = new.user_id - self._fb_dtsg = new._fb_dtsg - self._revision = new._revision - self._counter = new._counter - self._logout_h = new._logout_h or self._logout_h - def _get(self, url, params, error_retries=3): params.update(self.get_params()) r = self._session.get(_util.prefix_url(url), params=params) content = _util.check_request(r) - j = _util.to_json(content) - try: - _util.handle_payload_error(j) - except _exception.FBchatPleaseRefresh: - if error_retries > 0: - self._do_refresh() - return self._get(url, params, error_retries=error_retries - 1) - raise - return j + return _util.to_json(content) - def _post(self, url, data, files=None, as_graphql=False, error_retries=3): + def _post(self, url, data, files=None, as_graphql=False): data.update(self.get_params()) r = self._session.post(_util.prefix_url(url), data=data, files=files) content = _util.check_request(r) - try: - if as_graphql: - return _graphql.response_to_json(content) - else: - j = _util.to_json(content) - # TODO: Remove this, and move it to _payload_post instead - # We can't yet, since errors raised in here need to be caught below - _util.handle_payload_error(j) - return j - except _exception.FBchatPleaseRefresh: - if error_retries > 0: - self._do_refresh() - return self._post( - url, - data, - files=files, - as_graphql=as_graphql, - error_retries=error_retries - 1, - ) - raise + if as_graphql: + return _graphql.response_to_json(content) + else: + return _util.to_json(content) def _payload_post(self, url, data, files=None): j = self._post(url, data, files=files) + _util.handle_payload_error(j) try: return j["payload"] except (KeyError, TypeError): From 856c1ffe0e42cda9acf11d9c51a0e699d5d00605 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 8 Sep 2019 15:39:39 +0200 Subject: [PATCH 7/8] Remove ability to control the listening loop externally It was probably scarcely used, and separate functionality will be developed that makes this redundant anyhow. --- fbchat/_client.py | 35 +++-------------------------------- tests/utils.py | 8 +++----- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index dc66c51..1e18a31 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -43,12 +43,6 @@ class Client: to provide custom event handling (mainly useful while listening). """ - listening = False - """Whether the client is listening. - - Used when creating an external event loop to determine when to stop listening. - """ - @property def uid(self): """The ID of the client. @@ -2694,23 +2688,7 @@ class Client: except Exception as e: self.onMessageError(exception=e, msg=m) - def startListening(self): - """Start listening from an external event loop. - - Raises: - FBchatException: If request failed - """ - self.listening = True - - def doOneListen(self): - """Do one cycle of the listening loop. - - This method is useful if you want to control the client from an external event - loop. - - Returns: - bool: Whether the loop should keep running - """ + def _doOneListen(self): try: if self._markAlive: self._ping() @@ -2729,7 +2707,6 @@ class Client: if e.request_status_code in [502, 503]: # Bump pull channel, while contraining withing 0-4 self._pull_channel = (self._pull_channel + 1) % 5 - self.startListening() else: raise e except Exception as e: @@ -2737,11 +2714,6 @@ class Client: return True - def stopListening(self): - """Clean up the variables from `Client.startListening`.""" - self.listening = False - self._sticky, self._pool = (None, None) - def listen(self, markAlive=None): """Initialize and runs the listening loop continually. @@ -2751,13 +2723,12 @@ class Client: if markAlive is not None: self.setActiveStatus(markAlive) - self.startListening() self.onListening() - while self.listening and self.doOneListen(): + while self._doOneListen(): pass - self.stopListening() + self._sticky, self._pool = (None, None) def setActiveStatus(self, markAlive): """Change active status while listening. diff --git a/tests/utils.py b/tests/utils.py index 23a8b5b..1e44c92 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -39,22 +39,20 @@ TEXT_LIST = [ class ClientThread(threading.Thread): + # TODO: Refactor this to work with the new listening setup 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 + 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(): + while not self.should_stop.is_set() and self.client._doOneListen(): pass - self.client.stopListening() - class CaughtValue(threading.Event): def set(self, res): From 5aed7b0abc1db234cf81152493e68f8b5c23d308 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 30 Aug 2019 18:40:51 +0200 Subject: [PATCH 8/8] Remove login retrying Unnecessary clutter, easy to implement if required by the user. --- fbchat/_client.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 1e18a31..b0f2915 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -51,13 +51,12 @@ class Client: """ return self._uid - def __init__(self, email, password, max_tries=5, session_cookies=None): + def __init__(self, email, password, session_cookies=None): """Initialize and log in the client. Args: email: Facebook ``email``, ``id`` or ``phone number`` password: Facebook account password - max_tries (int): Maximum number of times to try logging in session_cookies (dict): Cookies from a previous session (Will default to login if these are invalid) Raises: @@ -75,7 +74,7 @@ class Client: or not self.setSession(session_cookies) or not self.isLoggedIn() ): - self.login(email, password, max_tries) + self.login(email, password) """ INTERNAL REQUEST METHODS @@ -154,7 +153,7 @@ class Client: return False return True - def login(self, email, password, max_tries=5): + def login(self, email, password): """Login the user, using ``email`` and ``password``. If the user is already logged in, this will do a re-login. @@ -162,33 +161,20 @@ class Client: Args: email: Facebook ``email`` or ``id`` or ``phone number`` password: Facebook account password - max_tries (int): Maximum number of times to try logging in Raises: FBchatException: On failed login """ self.onLoggingIn(email=email) - if max_tries < 1: - raise ValueError("Cannot login: max_tries should be at least one") - if not (email and password): raise ValueError("Email and password not set") - for i in range(1, max_tries + 1): - try: - self._state = _state.State.login( - email, password, on_2fa_callback=self.on2FACode - ) - self._uid = self._state.user_id - except Exception: - if i >= max_tries: - raise - log.exception("Attempt #{} failed, retrying".format(i)) - time.sleep(1) - else: - self.onLoggedIn(email=email) - break + self._state = _state.State.login( + email, password, on_2fa_callback=self.on2FACode + ) + self._uid = self._state.user_id + self.onLoggedIn(email=email) def logout(self): """Safely log out the client.