From d17f741f97e616c0326a4e44cf2f9325025c4223 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 16:21:20 +0200 Subject: [PATCH 1/8] Refactor _util.check_json --- fbchat/_util.py | 71 ++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/fbchat/_util.py b/fbchat/_util.py index 823e313..81751bb 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -107,36 +107,53 @@ def generateOfflineThreadingID(): return str(int(msgs, 2)) -def check_json(j): - if hasattr(j.get("payload"), "get") and j["payload"].get("error"): +def handle_error_in_payload(j): + payload = j.get("payload") + if isinstance(payload, dict) and payload.get("error"): raise FBchatFacebookError( - "Error when sending request: {}".format(j["payload"]["error"]), + "Error when sending request: {}".format(payload["error"]), fb_error_code=None, - fb_error_message=j["payload"]["error"], + fb_error_message=payload["error"], ) - elif j.get("error"): - if "errorDescription" in j: - # 'errorDescription' is in the users own language! - raise FBchatFacebookError( - "Error #{} when sending request: {}".format( - j["error"], j["errorDescription"] - ), - fb_error_code=j["error"], - fb_error_message=j["errorDescription"], - ) - elif "debug_info" in j["error"] and "code" in j["error"]: - raise FBchatFacebookError( - "Error #{} when sending request: {}".format( - j["error"]["code"], repr(j["error"]["debug_info"]) - ), - fb_error_code=j["error"]["code"], - fb_error_message=j["error"]["debug_info"], - ) - else: - raise FBchatFacebookError( - "Error {} when sending request".format(j["error"]), - fb_error_code=j["error"], - ) + + +def handle_payload_error(j): + # TODO: Check known error codes, and raise more relevant errors! + # TODO: Use j["errorSummary"] + if "error" in j and "errorDescription" in j: + # "errorDescription" is in the users own language! + raise FBchatFacebookError( + "Error #{} when sending request: {}".format( + j["error"], j["errorDescription"] + ), + fb_error_code=j["error"], + fb_error_message=j["errorDescription"], + ) + + +def handle_graphql_error(j): + if j.get("error") and "debug_info" in j["error"] and "code" in j["error"]: + raise FBchatFacebookError( + "Error #{} when sending request: {!r}".format( + j["error"]["code"], j["error"]["debug_info"] + ), + fb_error_code=j["error"]["code"], + fb_error_message=j["error"]["debug_info"], + ) + + +def handle_generic_error(j): + if j.get("error"): + raise FBchatFacebookError( + "Error {} when sending request".format(j["error"]), fb_error_code=j["error"] + ) + + +def check_json(j): + handle_error_in_payload(j) + handle_payload_error(j) + handle_graphql_error(j) + handle_generic_error(j) def check_request(r, as_json=True): From 4fdf0bbc57bed3873bf527b20b3a5a21987f9618 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 16:40:56 +0200 Subject: [PATCH 2/8] Remove JSON conversion from _util.check_request --- fbchat/_client.py | 7 ++++--- fbchat/_util.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index bfee672..788ef95 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -122,7 +122,8 @@ class Client(object): payload = self._generatePayload(query) r = self._state._session.get(prefix_url(url), params=payload) try: - return check_request(r) + content = check_request(r) + return to_json(content) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): return self._get(url, query=query, error_retries=error_retries - 1) @@ -132,11 +133,11 @@ class Client(object): payload = self._generatePayload(query) r = self._state._session.post(prefix_url(url), data=payload, files=files) try: + content = check_request(r) if as_graphql: - content = check_request(r, as_json=False) return graphql_response_to_json(content) else: - return check_request(r) + return to_json(content) except FBchatFacebookError as e: if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): return self._post( diff --git a/fbchat/_util.py b/fbchat/_util.py index 81751bb..4406ae7 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -156,11 +156,11 @@ def check_json(j): handle_generic_error(j) -def check_request(r, as_json=True): +def check_request(r): check_http_code(r.status_code) content = get_decoded_r(r) check_content(content) - return to_json(content) if as_json else content + return content def check_http_code(code): From 7345de149a441bf427e25751ed11c35e8bca6f54 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:10:24 +0200 Subject: [PATCH 3/8] Improve HTTP error handling --- fbchat/_util.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/fbchat/_util.py b/fbchat/_util.py index 4406ae7..c7b7eaf 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -164,11 +164,15 @@ def check_request(r): def check_http_code(code): - if 400 <= code < 600: + msg = "Error when sending request: Got {} response.".format(code) + if code == 404: raise FBchatFacebookError( - "Error when sending request: Got {} response".format(code), + msg + " This is either because you specified an invalid URL, or because" + " you provided an invalid id (Facebook usually requires integer ids).", request_status_code=code, ) + if 400 <= code < 600: + raise FBchatFacebookError(msg, request_status_code=code) def check_content(content, as_json=True): From 794696d327f5f9bc96d2703a4007b9f8bc3a1142 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:11:11 +0200 Subject: [PATCH 4/8] Improve payload error handling --- fbchat/_exception.py | 25 +++++++++++++++++++++++++ fbchat/_util.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/fbchat/_exception.py b/fbchat/_exception.py index c1dd0d6..8b28faf 100644 --- a/fbchat/_exception.py +++ b/fbchat/_exception.py @@ -28,5 +28,30 @@ class FBchatFacebookError(FBchatException): self.request_status_code = request_status_code +class FBchatInvalidParameters(FBchatFacebookError): + """Raised by Facebook if: + + - Some function supplied invalid parameters. + - Some content is not found. + - Some content is no longer available. + """ + + +class FBchatNotLoggedIn(FBchatFacebookError): + """Raised by Facebook if the client has been logged out.""" + + fb_error_code = "1357001" + + +class FBchatPleaseRefresh(FBchatFacebookError): + """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 = "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/_util.py b/fbchat/_util.py index c7b7eaf..c45dd39 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -11,7 +11,13 @@ from os.path import basename import warnings import logging import requests -from ._exception import FBchatException, FBchatFacebookError +from ._exception import ( + FBchatException, + FBchatFacebookError, + FBchatInvalidParameters, + FBchatNotLoggedIn, + FBchatPleaseRefresh, +) try: from urllib.parse import urlencode, parse_qs, urlparse @@ -118,17 +124,24 @@ def handle_error_in_payload(j): def handle_payload_error(j): - # TODO: Check known error codes, and raise more relevant errors! + if "error" not in j: + return + error = j["error"] + if j["error"] == 1357001: + error_cls = FBchatNotLoggedIn + elif j["error"] == 1357004: + error_cls = FBchatPleaseRefresh + elif j["error"] in (1357031, 1545010, 1545003): + error_cls = FBchatInvalidParameters + else: + error_cls = FBchatFacebookError # TODO: Use j["errorSummary"] - if "error" in j and "errorDescription" in j: - # "errorDescription" is in the users own language! - raise FBchatFacebookError( - "Error #{} when sending request: {}".format( - j["error"], j["errorDescription"] - ), - fb_error_code=j["error"], - fb_error_message=j["errorDescription"], - ) + # "errorDescription" is in the users own language! + raise error_cls( + "Error #{} when sending request: {}".format(error, j["errorDescription"]), + fb_error_code=error, + fb_error_message=j["errorDescription"], + ) def handle_graphql_error(j): From 3236ea5b97c9fa42ab297d68f1d902b88adb3bce Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:22:41 +0200 Subject: [PATCH 5/8] Improve state refresh handler --- fbchat/_client.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 788ef95..13e679e 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -106,17 +106,11 @@ class Client(object): query.update(self._state.get_params()) return query - def _fix_fb_errors(self, error_code): - """ - This fixes "Please try closing and re-opening your browser window" errors (1357004) - This error usually happens after 1-2 days of inactivity - It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it! - """ - if error_code == "1357004": - log.warning("Got error #1357004. Refreshing state and resending request") - self._state = State.from_session(session=self._state._session) - return True - return False + 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") + self._state = State.from_session(session=self._state._session) def _get(self, url, query=None, error_retries=3): payload = self._generatePayload(query) @@ -124,10 +118,11 @@ class Client(object): try: content = check_request(r) return to_json(content) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): + except FBchatPleaseRefresh: + if error_retries > 0: + self._do_refresh() return self._get(url, query=query, error_retries=error_retries - 1) - raise e + raise def _post(self, url, query=None, files=None, as_graphql=False, error_retries=3): payload = self._generatePayload(query) @@ -138,8 +133,9 @@ class Client(object): return graphql_response_to_json(content) else: return to_json(content) - except FBchatFacebookError as e: - if error_retries > 0 and self._fix_fb_errors(e.fb_error_code): + except FBchatPleaseRefresh: + if error_retries > 0: + self._do_refresh() return self._post( url, query=query, @@ -147,7 +143,7 @@ class Client(object): as_graphql=as_graphql, error_retries=error_retries - 1, ) - raise e + raise def _payload_post(self, url, data, files=None): j = self._post(url, data, files=files) From c9f11b924d454fba0fb6bb9406c829f5fa1dc84f Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:32:35 +0200 Subject: [PATCH 6/8] Add more explicit error handling --- fbchat/_client.py | 15 +++++++++++---- fbchat/_graphql.py | 4 ++-- fbchat/_util.py | 15 --------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 13e679e..9b04913 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -115,24 +115,30 @@ class Client(object): def _get(self, url, query=None, error_retries=3): payload = self._generatePayload(query) r = self._state._session.get(prefix_url(url), params=payload) + content = check_request(r) + j = to_json(content) try: - content = check_request(r) - return to_json(content) + handle_payload_error(j) except FBchatPleaseRefresh: if error_retries > 0: self._do_refresh() return self._get(url, query=query, error_retries=error_retries - 1) raise + return j def _post(self, url, query=None, files=None, as_graphql=False, error_retries=3): payload = self._generatePayload(query) r = self._state._session.post(prefix_url(url), data=payload, files=files) + content = check_request(r) try: - content = check_request(r) if as_graphql: return graphql_response_to_json(content) else: - return to_json(content) + j = 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 + handle_payload_error(j) + return j except FBchatPleaseRefresh: if error_retries > 0: self._do_refresh() @@ -147,6 +153,7 @@ class Client(object): def _payload_post(self, url, data, files=None): j = self._post(url, data, files=files) + handle_error_in_payload(j) try: return j["payload"] except (KeyError, TypeError): diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index 508d914..c03577c 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -49,9 +49,9 @@ def graphql_response_to_json(content): if "error_results" in x: del rtn[-1] continue - _util.check_json(x) + _util.handle_payload_error(x) [(key, value)] = x.items() - _util.check_json(value) + _util.handle_graphql_error(value) if "response" in value: rtn[int(key[1:])] = value["response"] else: diff --git a/fbchat/_util.py b/fbchat/_util.py index c45dd39..a6ab6f7 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -155,20 +155,6 @@ def handle_graphql_error(j): ) -def handle_generic_error(j): - if j.get("error"): - raise FBchatFacebookError( - "Error {} when sending request".format(j["error"]), fb_error_code=j["error"] - ) - - -def check_json(j): - handle_error_in_payload(j) - handle_payload_error(j) - handle_graphql_error(j) - handle_generic_error(j) - - def check_request(r): check_http_code(r.status_code) content = get_decoded_r(r) @@ -196,7 +182,6 @@ def check_content(content, as_json=True): def to_json(content): content = strip_json_cruft(content) j = parse_json(content) - check_json(j) log.debug(j) return j From bc551a63c2249477f11545f75260bca6b0198c67 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:50:33 +0200 Subject: [PATCH 7/8] Improve GraphQL error handling --- fbchat/_client.py | 1 + fbchat/_graphql.py | 2 +- fbchat/_util.py | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 9b04913..2aae8d4 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -1735,6 +1735,7 @@ class Client(object): } data = {"doc_id": 1491398900900362, "variables": json.dumps({"data": data})} j = self._payload_post("/webgraphql/mutation", data) + handle_graphql_errors(j) def createPlan(self, plan, thread_id=None): """ diff --git a/fbchat/_graphql.py b/fbchat/_graphql.py index c03577c..50eaea4 100644 --- a/fbchat/_graphql.py +++ b/fbchat/_graphql.py @@ -51,7 +51,7 @@ def graphql_response_to_json(content): continue _util.handle_payload_error(x) [(key, value)] = x.items() - _util.handle_graphql_error(value) + _util.handle_graphql_errors(value) if "response" in value: rtn[int(key[1:])] = value["response"] else: diff --git a/fbchat/_util.py b/fbchat/_util.py index a6ab6f7..56ddf14 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -144,14 +144,21 @@ def handle_payload_error(j): ) -def handle_graphql_error(j): - if j.get("error") and "debug_info" in j["error"] and "code" in j["error"]: +def handle_graphql_errors(j): + errors = [] + if "error" in j: + errors = [j["error"]] + if "errors" in j: + errors = j["errors"] + if errors: + error = errors[0] # TODO: Handle multiple errors + # TODO: Use `summary`, `severity` and `description` raise FBchatFacebookError( - "Error #{} when sending request: {!r}".format( - j["error"]["code"], j["error"]["debug_info"] + "GraphQL error #{}: {} / {!r}".format( + error.get("code"), error.get("message"), error.get("debug_info") ), - fb_error_code=j["error"]["code"], - fb_error_message=j["error"]["debug_info"], + fb_error_code=error.get("code"), + fb_error_message=error.get("message"), ) From 6862bd7be3f6c4bf2368a4711c762d4278dfcf75 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Tue, 2 Jul 2019 17:52:10 +0200 Subject: [PATCH 8/8] Handle errors in `payload` explicitly --- fbchat/_client.py | 21 ++++++++++++++++++++- fbchat/_util.py | 10 ---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/fbchat/_client.py b/fbchat/_client.py index 2aae8d4..afa3815 100644 --- a/fbchat/_client.py +++ b/fbchat/_client.py @@ -153,7 +153,6 @@ class Client(object): def _payload_post(self, url, data, files=None): j = self._post(url, data, files=files) - handle_error_in_payload(j) try: return j["payload"] except (KeyError, TypeError): @@ -1447,6 +1446,11 @@ class Client(object): "recipient_map[{}]".format(generateOfflineThreadingID()): thread_id, } j = self._payload_post("/mercury/attachments/forward/", data) + if not j.get("success"): + raise FBchatFacebookError( + "Failed forwarding attachment: {}".format(j["error"]), + fb_error_message=j["error"], + ) def createGroup(self, message, user_ids): """ @@ -1758,6 +1762,11 @@ class Client(object): "acontext": ACONTEXT, } j = self._payload_post("/ajax/eventreminder/create", data) + if "error" in j: + raise FBchatFacebookError( + "Failed creating plan: {}".format(j["error"]), + fb_error_message=j["error"], + ) def editPlan(self, plan, new_plan): """ @@ -1833,6 +1842,11 @@ class Client(object): data["option_is_selected_array[{}]".format(i)] = str(int(option.vote)) j = self._payload_post("/messaging/group_polling/create_poll/?dpr=1", data) + if j.get("status") != "success": + raise FBchatFacebookError( + "Failed creating poll: {}".format(j.get("errorTitle")), + fb_error_message=j.get("errorMessage"), + ) def updatePollVote(self, poll_id, option_ids=[], new_options=[]): """ @@ -1855,6 +1869,11 @@ class Client(object): data["new_options[{}]".format(i)] = option_text j = self._payload_post("/messaging/group_polling/update_vote/?dpr=1", data) + if j.get("status") != "success": + raise FBchatFacebookError( + "Failed updating poll vote: {}".format(j.get("errorTitle")), + fb_error_message=j.get("errorMessage"), + ) def setTypingStatus(self, status, thread_id=None, thread_type=None): """ diff --git a/fbchat/_util.py b/fbchat/_util.py index 56ddf14..c9729e9 100644 --- a/fbchat/_util.py +++ b/fbchat/_util.py @@ -113,16 +113,6 @@ def generateOfflineThreadingID(): return str(int(msgs, 2)) -def handle_error_in_payload(j): - payload = j.get("payload") - if isinstance(payload, dict) and payload.get("error"): - raise FBchatFacebookError( - "Error when sending request: {}".format(payload["error"]), - fb_error_code=None, - fb_error_message=payload["error"], - ) - - def handle_payload_error(j): if "error" not in j: return