From 3445eccc324a0977a9c55a529d356627d8750d22 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 15 Jan 2020 10:49:16 +0100 Subject: [PATCH] Initial redo of exceptions --- fbchat/__init__.py | 10 +++++- fbchat/_client.py | 76 ++++++++++++-------------------------------- fbchat/_exception.py | 62 ++++++++++++++++++++++++++---------- fbchat/_graphql.py | 6 ++-- fbchat/_plan.py | 5 ++- fbchat/_poll.py | 7 ++-- fbchat/_session.py | 33 +++++++------------ fbchat/_thread.py | 16 ++++------ 8 files changed, 101 insertions(+), 114 deletions(-) diff --git a/fbchat/__init__.py b/fbchat/__init__.py index d4a1355..ec17a54 100644 --- a/fbchat/__init__.py +++ b/fbchat/__init__.py @@ -12,7 +12,15 @@ _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 ._core import Image -from ._exception import FBchatException, FBchatFacebookError +from ._exception import ( + FacebookError, + HTTPError, + ParseError, + ExternalError, + InvalidParameters, + NotLoggedIn, + PleaseRefresh, +) from ._session import Session from ._thread import ThreadLocation, ThreadABC, Thread from ._user import User, UserData, ActiveStatus diff --git a/fbchat/_client.py b/fbchat/_client.py index 5f1c999..daff692 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -3,9 +3,19 @@ import time import requests from ._core import log -from . import _util, _graphql, _session, _poll, _user, _page, _group, _thread, _message +from . import ( + _exception, + _util, + _graphql, + _session, + _poll, + _user, + _page, + _group, + _thread, + _message, +) -from ._exception import FBchatException, FBchatFacebookError from ._thread import ThreadLocation from ._user import User, UserData, ActiveStatus from ._group import Group, GroupData @@ -228,7 +238,7 @@ class Client: j = self.session._payload_post("/chat/user_info/", data) if j.get("profiles") is None: - raise FBchatException("No users/pages returned: {}".format(j)) + raise _exception.ParseError("No users/pages returned", data=j) entries = {} for _id in j["profiles"]: @@ -251,9 +261,7 @@ class Client: "name": k.get("name"), } else: - raise FBchatException( - "{} had an unknown thread type: {}".format(_id, k) - ) + raise _exception.ParseError("Unknown thread type", data=k) log.debug(entries) return entries @@ -269,9 +277,6 @@ class Client: Returns: dict: `Thread` objects, labeled by their ID - - Raises: - FBchatException: If request failed """ queries = [] for thread_id in thread_ids: @@ -312,16 +317,16 @@ class Client: elif entry.get("thread_type") == "ONE_TO_ONE": _id = entry["thread_key"]["other_user_id"] if pages_and_users.get(_id) is None: - raise FBchatException("Could not fetch thread {}".format(_id)) + raise _exception.ParseError( + "Could not fetch thread {}".format(_id), data=pages_and_users + ) entry.update(pages_and_users[_id]) if "first_name" in entry: rtn[_id] = UserData._from_graphql(self.session, entry) else: rtn[_id] = PageData._from_graphql(self.session, entry) else: - raise FBchatException( - "{} had an unknown thread type: {}".format(thread_ids[i], entry) - ) + raise _exception.ParseError("Unknown thread type", data=entry) return rtn @@ -389,9 +394,6 @@ class Client: Returns: list: List of unread thread ids - - Raises: - FBchatException: If request failed """ form = { "folders[0]": "inbox", @@ -409,9 +411,6 @@ class Client: Returns: list: List of unseen thread ids - - Raises: - FBchatException: If request failed """ j = self.session._payload_post("/mercury/unseen_thread_ids/", {}) @@ -426,9 +425,6 @@ class Client: Returns: str: An URL where you can download the original image - - Raises: - FBchatException: If request failed """ image_id = str(image_id) data = {"photo_id": str(image_id)} @@ -437,7 +433,7 @@ class Client: url = _util.get_jsmods_require(j, 3) if url is None: - raise FBchatException("Could not fetch image URL from: {}".format(j)) + raise _exception.ParseError("Could not fetch image URL", data=j) return url def _get_private_data(self): @@ -488,12 +484,6 @@ class Client: Args: thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads` message_id: Message ID to set as delivered. See :ref:`intro_threads` - - Returns: - True - - Raises: - FBchatException: If request failed """ data = { "message_ids[0]": message_id, @@ -526,9 +516,6 @@ class Client: Args: thread_ids: User/Group IDs to set as read. See :ref:`intro_threads` timestamp: Timestamp (as a Datetime) to signal the read cursor at, default is the current time - - Raises: - FBchatException: If request failed """ self._read_status(True, thread_ids, timestamp) @@ -540,9 +527,6 @@ class Client: Args: thread_ids: User/Group IDs to set as unread. See :ref:`intro_threads` timestamp: Timestamp (as a Datetime) to signal the read cursor at, default is the current time - - Raises: - FBchatException: If request failed """ self._read_status(False, thread_ids, timestamp) @@ -561,12 +545,6 @@ class Client: Args: location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER thread_ids: Thread IDs to move. See :ref:`intro_threads` - - Returns: - True - - Raises: - FBchatException: If request failed """ thread_ids = _util.require_list(thread_ids) @@ -597,12 +575,6 @@ class Client: Args: thread_ids: Thread IDs to delete. See :ref:`intro_threads` - - Returns: - True - - Raises: - FBchatException: If request failed """ thread_ids = _util.require_list(thread_ids) @@ -624,12 +596,6 @@ class Client: Args: message_ids: Message IDs to delete - - Returns: - True - - Raises: - FBchatException: If request failed """ message_ids = _util.require_list(message_ids) data = dict() @@ -1242,9 +1208,9 @@ class Client: except requests.ConnectionError: # If the client has lost their internet connection, keep trying every 30 seconds time.sleep(30) - except FBchatFacebookError as e: + except _exception.HTTPError as e: # Fix 502 and 503 pull errors - if e.request_status_code in [502, 503]: + if e.status_code in [502, 503]: # Bump pull channel, while contraining withing 0-4 self._pull_channel = (self._pull_channel + 1) % 5 else: diff --git a/fbchat/_exception.py b/fbchat/_exception.py index 92c654d..8dad169 100644 --- a/fbchat/_exception.py +++ b/fbchat/_exception.py @@ -5,29 +5,57 @@ attrs_exception = attr.s(slots=True, auto_exc=True) @attrs_exception -class FBchatException(Exception): - """Custom exception thrown by ``fbchat``. +class FacebookError(Exception): + """Base class for all custom exceptions raised by ``fbchat``. - All exceptions in the module inherits this. + All exceptions in the module inherit this. """ - message = attr.ib() + message = attr.ib(type=str) @attrs_exception -class FBchatFacebookError(FBchatException): - """Raised when Facebook returns an error.""" +class HTTPError(FacebookError): + """Base class for errors with the HTTP(s) connection to Facebook.""" + + status_code = attr.ib(None, type=int) + + +@attrs_exception +class ParseError(FacebookError): + """Raised when we fail parsing a response from Facebook. + + This may contain sensitive data, so should not be logged to file. + """ + + data = attr.ib() + """The data that triggered the error. + + The format of this cannot be relied on, it's only for debugging purposes. + """ + + def __str__(self): + msg = "{}. Please report this, and the associated data: {}" + return msg.format(self.message, self.data) + + +@attrs_exception +class ExternalError(FacebookError): + """Base class for errors that Facebook return.""" - #: The error code that Facebook returned - fb_error_code = attr.ib(None) #: The error message that Facebook returned (In the user's own language) - fb_error_message = attr.ib(None) - #: The status code that was sent in the HTTP response (e.g. 404) (Usually only set if not successful, aka. not 200) - request_status_code = attr.ib(None) + message = attr.ib(type=str) + #: The error code that Facebook returned + code = attr.ib(None, type=int) + + def __str__(self): + if self.code: + return "#{}: {}".format(self.code, self.message) + return self.message @attrs_exception -class FBchatInvalidParameters(FBchatFacebookError): +class InvalidParameters(ExternalError): """Raised by Facebook if: - Some function supplied invalid parameters. @@ -37,18 +65,18 @@ class FBchatInvalidParameters(FBchatFacebookError): @attrs_exception -class FBchatNotLoggedIn(FBchatFacebookError): +class NotLoggedIn(ExternalError): """Raised by Facebook if the client has been logged out.""" - fb_error_code = attr.ib("1357001") + code = attr.ib(1357001) @attrs_exception -class FBchatPleaseRefresh(FBchatFacebookError): +class PleaseRefresh(ExternalError): """Raised by Facebook if the client has been inactive for too long. This error usually happens after 1-2 days of inactivity. """ - fb_error_code = attr.ib("1357004") - fb_error_message = attr.ib("Please try closing and re-opening your browser window.") + code = attr.ib(1357004) + message = attr.ib("Please try closing and re-opening your browser window.") diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 76ec99e..918fdf1 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -38,10 +38,8 @@ def response_to_json(content): content = _util.strip_json_cruft(content) # Usually only needed in some error cases try: j = json.loads(content, cls=ConcatJSONDecoder) - except Exception: - raise _exception.FBchatException( - "Error while parsing JSON: {!r}".format(content) - ) + except Exception as e: + raise _exception.ParseError("Error while parsing JSON", data=content) from e rtn = [None] * (len(j)) for x in j: diff --git a/fbchat/_plan.py b/fbchat/_plan.py index 3cfbf89..a8f33d3 100644 --- a/fbchat/_plan.py +++ b/fbchat/_plan.py @@ -53,9 +53,8 @@ class Plan: } j = thread.session._payload_post("/ajax/eventreminder/create", data) if "error" in j: - raise _exception.FBchatFacebookError( - "Failed creating plan: {}".format(j["error"]), - fb_error_message=j["error"], + raise _exception.ExternalError( + "Failed creating plan: {}".format(j["error"]) ) def edit( diff --git a/fbchat/_poll.py b/fbchat/_poll.py index 957aa49..f7680e3 100644 --- a/fbchat/_poll.py +++ b/fbchat/_poll.py @@ -101,7 +101,8 @@ class Poll: "/messaging/group_polling/update_vote/?dpr=1", data ) if j.get("status") != "success": - raise _exception.FBchatFacebookError( - "Failed updating poll vote: {}".format(j.get("errorTitle")), - fb_error_message=j.get("errorMessage"), + raise _exception.ExternalError( + "Failed updating poll vote: {}: {}".format( + j.get("errorTitle"), j.get("errorMessage") + ) ) diff --git a/fbchat/_session.py b/fbchat/_session.py index 8b29e12..d28268a 100644 --- a/fbchat/_session.py +++ b/fbchat/_session.py @@ -13,9 +13,10 @@ FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"') def get_user_id(session): # TODO: Optimize this `.get_dict()` call! - rtn = session.cookies.get_dict().get("c_user") + cookies = session.cookies.get_dict() + rtn = cookies.get("c_user") if rtn is None: - raise _exception.FBchatException("Could not find user id") + raise _exception.ParseError("Could not find user id", data=cookies) return str(rtn) @@ -153,9 +154,6 @@ class Session: password: Facebook account password on_2fa_callback: Function that will be called, in case a 2FA code is needed. This should return the requested 2FA code. - - Raises: - FBchatException: On failed login """ session = session_factory() @@ -174,7 +172,7 @@ class Session: # Usually, 'Checkpoint' will refer to 2FA if "checkpoint" in r.url and ('id="approvals_code"' in r.text.lower()): if not on_2fa_callback: - raise _exception.FBchatException( + raise ValueError( "2FA code required, please add `on_2fa_callback` to .login" ) code = on_2fa_callback() @@ -188,10 +186,8 @@ class Session: return cls._from_session(session=session) else: code, msg = get_error_data(r.text, r.url) - raise _exception.FBchatFacebookError( - "Login failed (Failed on url: {})".format(r.url), - fb_error_code=code, - fb_error_message=msg, + raise _exception.ExternalError( + "Login failed. {}, url: {}".format(msg, r.url), code=code ) def is_logged_in(self): @@ -242,9 +238,7 @@ class Session: # Fall back to searching with a regex res = FB_DTSG_REGEX.search(r.text) if not res: - raise _exception.FBchatException( - "Failed loading session: Could not find fb_dtsg" - ) + raise ValueError("Failed loading session, could not find fb_dtsg") fb_dtsg = res.group(1) revision = int(r.text.split('"client_revision":', 1)[1].split(",", 1)[0]) @@ -302,8 +296,8 @@ class Session: _util.handle_payload_error(j) try: return j["payload"] - except (KeyError, TypeError): - raise _exception.FBchatException("Missing payload: {}".format(j)) + except (KeyError, TypeError) as e: + raise _exception.ParseError("Missing payload", data=j) from e def _graphql_requests(self, *queries): data = { @@ -330,9 +324,7 @@ class Session: ) if len(j["metadata"]) != len(files): - raise _exception.FBchatException( - "Some files could not be uploaded: {}, {}".format(j, files) - ) + raise _exception.ParseError("Some files could not be uploaded", data=j) return [ (data[_util.mimetype_to_key(data["filetype"])], data["filetype"]) @@ -366,7 +358,4 @@ class Session: log.warning("Got multiple message ids' back: {}".format(message_ids)) return message_ids[0] except (KeyError, IndexError, TypeError) as e: - raise _exception.FBchatException( - "Error when sending message: " - "No message IDs could be found: {}".format(j) - ) + raise _exception.ParseError("No message IDs could be found", data=j) from e diff --git a/fbchat/_thread.py b/fbchat/_thread.py index 923f7e7..a080a82 100644 --- a/fbchat/_thread.py +++ b/fbchat/_thread.py @@ -313,9 +313,7 @@ class ThreadABC(metaclass=abc.ABCMeta): ) if j.get("message_thread") is None: - raise _exception.FBchatException( - "Could not fetch thread {}: {}".format(self.id, j) - ) + raise _exception.ParseError("Could not fetch messages", data=j) # TODO: Should we parse the returned thread data, too? @@ -481,9 +479,8 @@ class ThreadABC(metaclass=abc.ABCMeta): } j = self.session._payload_post("/mercury/attachments/forward/", data) if not j.get("success"): - raise _exception.FBchatFacebookError( - "Failed forwarding attachment: {}".format(j["error"]), - fb_error_message=j["error"], + raise _exception.ExternalError( + "Failed forwarding attachment: {}".format(j["error"]) ) def _set_typing(self, typing): @@ -547,9 +544,10 @@ class ThreadABC(metaclass=abc.ABCMeta): "/messaging/group_polling/create_poll/?dpr=1", data ) if j.get("status") != "success": - raise _exception.FBchatFacebookError( - "Failed creating poll: {}".format(j.get("errorTitle")), - fb_error_message=j.get("errorMessage"), + raise _exception.ExternalError( + "Failed creating poll: {}, {}".format( + j.get("errorTitle"), j.get("errorMessage") + ) ) def mute(self, duration: datetime.timedelta = None):