Merge pull request #510 from carpedm20/improve-exceptions
Improve exceptions
This commit is contained in:
@@ -12,7 +12,16 @@ _logging.getLogger(__name__).addHandler(_logging.NullHandler())
|
|||||||
# The order of these is somewhat significant, e.g. User has to be imported after Thread!
|
# The order of these is somewhat significant, e.g. User has to be imported after Thread!
|
||||||
from . import _core, _util
|
from . import _core, _util
|
||||||
from ._core import Image
|
from ._core import Image
|
||||||
from ._exception import FBchatException, FBchatFacebookError
|
from ._exception import (
|
||||||
|
FacebookError,
|
||||||
|
HTTPError,
|
||||||
|
ParseError,
|
||||||
|
ExternalError,
|
||||||
|
GraphQLError,
|
||||||
|
InvalidParameters,
|
||||||
|
NotLoggedIn,
|
||||||
|
PleaseRefresh,
|
||||||
|
)
|
||||||
from ._session import Session
|
from ._session import Session
|
||||||
from ._thread import ThreadLocation, ThreadABC, Thread
|
from ._thread import ThreadLocation, ThreadABC, Thread
|
||||||
from ._user import User, UserData, ActiveStatus
|
from ._user import User, UserData, ActiveStatus
|
||||||
|
@@ -3,9 +3,19 @@ import time
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ._core import log
|
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 ._thread import ThreadLocation
|
||||||
from ._user import User, UserData, ActiveStatus
|
from ._user import User, UserData, ActiveStatus
|
||||||
from ._group import Group, GroupData
|
from ._group import Group, GroupData
|
||||||
@@ -228,7 +238,7 @@ class Client:
|
|||||||
j = self.session._payload_post("/chat/user_info/", data)
|
j = self.session._payload_post("/chat/user_info/", data)
|
||||||
|
|
||||||
if j.get("profiles") is None:
|
if j.get("profiles") is None:
|
||||||
raise FBchatException("No users/pages returned: {}".format(j))
|
raise _exception.ParseError("No users/pages returned", data=j)
|
||||||
|
|
||||||
entries = {}
|
entries = {}
|
||||||
for _id in j["profiles"]:
|
for _id in j["profiles"]:
|
||||||
@@ -251,9 +261,7 @@ class Client:
|
|||||||
"name": k.get("name"),
|
"name": k.get("name"),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise FBchatException(
|
raise _exception.ParseError("Unknown thread type", data=k)
|
||||||
"{} had an unknown thread type: {}".format(_id, k)
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug(entries)
|
log.debug(entries)
|
||||||
return entries
|
return entries
|
||||||
@@ -269,9 +277,6 @@ class Client:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: `Thread` objects, labeled by their ID
|
dict: `Thread` objects, labeled by their ID
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
queries = []
|
queries = []
|
||||||
for thread_id in thread_ids:
|
for thread_id in thread_ids:
|
||||||
@@ -312,16 +317,16 @@ class Client:
|
|||||||
elif entry.get("thread_type") == "ONE_TO_ONE":
|
elif entry.get("thread_type") == "ONE_TO_ONE":
|
||||||
_id = entry["thread_key"]["other_user_id"]
|
_id = entry["thread_key"]["other_user_id"]
|
||||||
if pages_and_users.get(_id) is None:
|
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])
|
entry.update(pages_and_users[_id])
|
||||||
if "first_name" in entry:
|
if "first_name" in entry:
|
||||||
rtn[_id] = UserData._from_graphql(self.session, entry)
|
rtn[_id] = UserData._from_graphql(self.session, entry)
|
||||||
else:
|
else:
|
||||||
rtn[_id] = PageData._from_graphql(self.session, entry)
|
rtn[_id] = PageData._from_graphql(self.session, entry)
|
||||||
else:
|
else:
|
||||||
raise FBchatException(
|
raise _exception.ParseError("Unknown thread type", data=entry)
|
||||||
"{} had an unknown thread type: {}".format(thread_ids[i], entry)
|
|
||||||
)
|
|
||||||
|
|
||||||
return rtn
|
return rtn
|
||||||
|
|
||||||
@@ -389,9 +394,6 @@ class Client:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of unread thread ids
|
list: List of unread thread ids
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
form = {
|
form = {
|
||||||
"folders[0]": "inbox",
|
"folders[0]": "inbox",
|
||||||
@@ -409,9 +411,6 @@ class Client:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of unseen thread ids
|
list: List of unseen thread ids
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
j = self.session._payload_post("/mercury/unseen_thread_ids/", {})
|
j = self.session._payload_post("/mercury/unseen_thread_ids/", {})
|
||||||
|
|
||||||
@@ -426,18 +425,15 @@ class Client:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: An URL where you can download the original image
|
str: An URL where you can download the original image
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
image_id = str(image_id)
|
image_id = str(image_id)
|
||||||
data = {"photo_id": str(image_id)}
|
data = {"photo_id": str(image_id)}
|
||||||
j = self.session._post("/mercury/attachments/photo/", data)
|
j = self.session._post("/mercury/attachments/photo/", data)
|
||||||
_util.handle_payload_error(j)
|
_exception.handle_payload_error(j)
|
||||||
|
|
||||||
url = _util.get_jsmods_require(j, 3)
|
url = _util.get_jsmods_require(j, 3)
|
||||||
if url is None:
|
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
|
return url
|
||||||
|
|
||||||
def _get_private_data(self):
|
def _get_private_data(self):
|
||||||
@@ -488,12 +484,6 @@ class Client:
|
|||||||
Args:
|
Args:
|
||||||
thread_id: User/Group ID to which the message belongs. See :ref:`intro_threads`
|
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`
|
message_id: Message ID to set as delivered. See :ref:`intro_threads`
|
||||||
|
|
||||||
Returns:
|
|
||||||
True
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"message_ids[0]": message_id,
|
"message_ids[0]": message_id,
|
||||||
@@ -526,9 +516,6 @@ class Client:
|
|||||||
Args:
|
Args:
|
||||||
thread_ids: User/Group IDs to set as read. See :ref:`intro_threads`
|
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
|
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)
|
self._read_status(True, thread_ids, timestamp)
|
||||||
|
|
||||||
@@ -540,9 +527,6 @@ class Client:
|
|||||||
Args:
|
Args:
|
||||||
thread_ids: User/Group IDs to set as unread. See :ref:`intro_threads`
|
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
|
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)
|
self._read_status(False, thread_ids, timestamp)
|
||||||
|
|
||||||
@@ -561,12 +545,6 @@ class Client:
|
|||||||
Args:
|
Args:
|
||||||
location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER
|
location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER
|
||||||
thread_ids: Thread IDs to move. See :ref:`intro_threads`
|
thread_ids: Thread IDs to move. See :ref:`intro_threads`
|
||||||
|
|
||||||
Returns:
|
|
||||||
True
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
thread_ids = _util.require_list(thread_ids)
|
thread_ids = _util.require_list(thread_ids)
|
||||||
|
|
||||||
@@ -597,12 +575,6 @@ class Client:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
thread_ids: Thread IDs to delete. See :ref:`intro_threads`
|
thread_ids: Thread IDs to delete. See :ref:`intro_threads`
|
||||||
|
|
||||||
Returns:
|
|
||||||
True
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
thread_ids = _util.require_list(thread_ids)
|
thread_ids = _util.require_list(thread_ids)
|
||||||
|
|
||||||
@@ -624,12 +596,6 @@ class Client:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
message_ids: Message IDs to delete
|
message_ids: Message IDs to delete
|
||||||
|
|
||||||
Returns:
|
|
||||||
True
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If request failed
|
|
||||||
"""
|
"""
|
||||||
message_ids = _util.require_list(message_ids)
|
message_ids = _util.require_list(message_ids)
|
||||||
data = dict()
|
data = dict()
|
||||||
@@ -659,7 +625,7 @@ class Client:
|
|||||||
"https://{}-edge-chat.facebook.com/active_ping".format(self._pull_channel),
|
"https://{}-edge-chat.facebook.com/active_ping".format(self._pull_channel),
|
||||||
data,
|
data,
|
||||||
)
|
)
|
||||||
_util.handle_payload_error(j)
|
_exception.handle_payload_error(j)
|
||||||
|
|
||||||
def _pull_message(self):
|
def _pull_message(self):
|
||||||
"""Call pull api to fetch message data."""
|
"""Call pull api to fetch message data."""
|
||||||
@@ -674,7 +640,7 @@ class Client:
|
|||||||
j = self.session._get(
|
j = self.session._get(
|
||||||
"https://{}-edge-chat.facebook.com/pull".format(self._pull_channel), data
|
"https://{}-edge-chat.facebook.com/pull".format(self._pull_channel), data
|
||||||
)
|
)
|
||||||
_util.handle_payload_error(j)
|
_exception.handle_payload_error(j)
|
||||||
return j
|
return j
|
||||||
|
|
||||||
def _parse_delta(self, delta):
|
def _parse_delta(self, delta):
|
||||||
@@ -1237,16 +1203,19 @@ class Client:
|
|||||||
self._parse_message(content)
|
self._parse_message(content)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return False
|
return False
|
||||||
except requests.Timeout:
|
except _exception.HTTPError as e:
|
||||||
pass
|
cause = e.__cause__
|
||||||
except requests.ConnectionError:
|
|
||||||
# If the client has lost their internet connection, keep trying every 30 seconds
|
|
||||||
time.sleep(30)
|
|
||||||
except FBchatFacebookError as e:
|
|
||||||
# Fix 502 and 503 pull errors
|
# 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
|
# Bump pull channel, while contraining withing 0-4
|
||||||
self._pull_channel = (self._pull_channel + 1) % 5
|
self._pull_channel = (self._pull_channel + 1) % 5
|
||||||
|
# TODO: Handle these exceptions better
|
||||||
|
elif isinstance(cause, requests.ReadTimeout):
|
||||||
|
pass # Expected
|
||||||
|
elif isinstance(cause, (requests.ConnectTimeout, requests.ConnectionError)):
|
||||||
|
# If the client has lost their internet connection, keep trying every 30 seconds
|
||||||
|
time.sleep(30)
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@@ -1,33 +1,84 @@
|
|||||||
import attr
|
import attr
|
||||||
|
import requests
|
||||||
|
|
||||||
# Not frozen, since that doesn't work in PyPy
|
# Not frozen, since that doesn't work in PyPy
|
||||||
attrs_exception = attr.s(slots=True, auto_exc=True)
|
attrs_exception = attr.s(slots=True, auto_exc=True)
|
||||||
|
|
||||||
|
|
||||||
@attrs_exception
|
@attrs_exception
|
||||||
class FBchatException(Exception):
|
class FacebookError(Exception):
|
||||||
"""Custom exception thrown by ``fbchat``.
|
"""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()
|
#: A message describing the error
|
||||||
|
message = attr.ib(type=str)
|
||||||
|
|
||||||
|
|
||||||
@attrs_exception
|
@attrs_exception
|
||||||
class FBchatFacebookError(FBchatException):
|
class HTTPError(FacebookError):
|
||||||
"""Raised when Facebook returns an error."""
|
"""Base class for errors with the HTTP(s) connection to Facebook."""
|
||||||
|
|
||||||
|
#: The returned HTTP status code, if relevant
|
||||||
|
status_code = attr.ib(None, type=int)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.status_code:
|
||||||
|
return self.message
|
||||||
|
return "Got {} response: {}".format(self.status_code, self.message)
|
||||||
|
|
||||||
|
|
||||||
|
@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, along with the data below!\n{}"
|
||||||
|
return msg.format(self.message, self.data)
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
|
class ExternalError(FacebookError):
|
||||||
|
"""Base class for errors that Facebook return."""
|
||||||
|
|
||||||
|
#: The error message that Facebook returned (Possibly in the user's own language)
|
||||||
|
description = attr.ib(type=str)
|
||||||
#: The error code that Facebook returned
|
#: The error code that Facebook returned
|
||||||
fb_error_code = attr.ib(None)
|
code = attr.ib(None, type=int)
|
||||||
#: The error message that Facebook returned (In the user's own language)
|
|
||||||
fb_error_message = attr.ib(None)
|
def __str__(self):
|
||||||
#: The status code that was sent in the HTTP response (e.g. 404) (Usually only set if not successful, aka. not 200)
|
if self.code:
|
||||||
request_status_code = attr.ib(None)
|
return "#{} {}: {}".format(self.code, self.message, self.description)
|
||||||
|
return "{}: {}".format(self.message, self.description)
|
||||||
|
|
||||||
|
|
||||||
@attrs_exception
|
@attrs_exception
|
||||||
class FBchatInvalidParameters(FBchatFacebookError):
|
class GraphQLError(ExternalError):
|
||||||
|
"""Raised by Facebook if there was an error in the GraphQL query."""
|
||||||
|
|
||||||
|
# TODO: Handle multiple errors
|
||||||
|
|
||||||
|
#: Query debug information
|
||||||
|
debug_info = attr.ib(None, type=str)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.debug_info:
|
||||||
|
return "{}, {}".format(super().__str__(), self.debug_info)
|
||||||
|
return super().__str__()
|
||||||
|
|
||||||
|
|
||||||
|
@attrs_exception
|
||||||
|
class InvalidParameters(ExternalError):
|
||||||
"""Raised by Facebook if:
|
"""Raised by Facebook if:
|
||||||
|
|
||||||
- Some function supplied invalid parameters.
|
- Some function supplied invalid parameters.
|
||||||
@@ -37,18 +88,81 @@ class FBchatInvalidParameters(FBchatFacebookError):
|
|||||||
|
|
||||||
|
|
||||||
@attrs_exception
|
@attrs_exception
|
||||||
class FBchatNotLoggedIn(FBchatFacebookError):
|
class NotLoggedIn(ExternalError):
|
||||||
"""Raised by Facebook if the client has been logged out."""
|
"""Raised by Facebook if the client has been logged out."""
|
||||||
|
|
||||||
fb_error_code = attr.ib("1357001")
|
code = attr.ib(1357001)
|
||||||
|
|
||||||
|
|
||||||
@attrs_exception
|
@attrs_exception
|
||||||
class FBchatPleaseRefresh(FBchatFacebookError):
|
class PleaseRefresh(ExternalError):
|
||||||
"""Raised by Facebook if the client has been inactive for too long.
|
"""Raised by Facebook if the client has been inactive for too long.
|
||||||
|
|
||||||
This error usually happens after 1-2 days of inactivity.
|
This error usually happens after 1-2 days of inactivity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fb_error_code = attr.ib("1357004")
|
code = attr.ib(1357004)
|
||||||
fb_error_message = attr.ib("Please try closing and re-opening your browser window.")
|
|
||||||
|
|
||||||
|
def handle_payload_error(j):
|
||||||
|
if "error" not in j:
|
||||||
|
return
|
||||||
|
code = j["error"]
|
||||||
|
if code == 1357001:
|
||||||
|
error_cls = NotLoggedIn
|
||||||
|
elif code == 1357004:
|
||||||
|
error_cls = PleaseRefresh
|
||||||
|
elif code in (1357031, 1545010, 1545003):
|
||||||
|
error_cls = InvalidParameters
|
||||||
|
else:
|
||||||
|
error_cls = ExternalError
|
||||||
|
raise error_cls(j["errorSummary"], description=j["errorDescription"], code=code)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_graphql_errors(j):
|
||||||
|
errors = []
|
||||||
|
if j.get("error"):
|
||||||
|
errors = [j["error"]]
|
||||||
|
if "errors" in j:
|
||||||
|
errors = j["errors"]
|
||||||
|
if errors:
|
||||||
|
error = errors[0] # TODO: Handle multiple errors
|
||||||
|
# TODO: Use `severity` and `description`
|
||||||
|
raise GraphQLError(
|
||||||
|
# TODO: What data is always available?
|
||||||
|
message=error.get("summary", "Unknown error"),
|
||||||
|
description=error.get("message", ""),
|
||||||
|
code=error.get("code"),
|
||||||
|
debug_info=error.get("debug_info"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_http_error(code):
|
||||||
|
if code == 404:
|
||||||
|
raise HTTPError(
|
||||||
|
"This might be because you provided an invalid id"
|
||||||
|
+ " (Facebook usually require integer ids)",
|
||||||
|
status_code=code,
|
||||||
|
)
|
||||||
|
if code == 500:
|
||||||
|
raise HTTPError(
|
||||||
|
"There is probably an error on the endpoint, or it might be rate limited",
|
||||||
|
status_code=code,
|
||||||
|
)
|
||||||
|
if 400 <= code < 600:
|
||||||
|
raise HTTPError("Failed sending request", status_code=code)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_requests_error(e):
|
||||||
|
if isinstance(e, requests.ConnectionError):
|
||||||
|
raise HTTPError("Connection error") from e
|
||||||
|
if isinstance(e, requests.HTTPError):
|
||||||
|
pass # Raised when using .raise_for_status, so should never happen
|
||||||
|
if isinstance(e, requests.URLRequired):
|
||||||
|
pass # Should never happen, we always prove valid URLs
|
||||||
|
if isinstance(e, requests.TooManyRedirects):
|
||||||
|
pass # TODO: Consider using allow_redirects=False to prevent this
|
||||||
|
if isinstance(e, requests.Timeout):
|
||||||
|
pass # Should never happen, we don't set timeouts
|
||||||
|
|
||||||
|
raise HTTPError("Requests error") from e
|
||||||
|
@@ -38,19 +38,17 @@ def response_to_json(content):
|
|||||||
content = _util.strip_json_cruft(content) # Usually only needed in some error cases
|
content = _util.strip_json_cruft(content) # Usually only needed in some error cases
|
||||||
try:
|
try:
|
||||||
j = json.loads(content, cls=ConcatJSONDecoder)
|
j = json.loads(content, cls=ConcatJSONDecoder)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
raise _exception.FBchatException(
|
raise _exception.ParseError("Error while parsing JSON", data=content) from e
|
||||||
"Error while parsing JSON: {!r}".format(content)
|
|
||||||
)
|
|
||||||
|
|
||||||
rtn = [None] * (len(j))
|
rtn = [None] * (len(j))
|
||||||
for x in j:
|
for x in j:
|
||||||
if "error_results" in x:
|
if "error_results" in x:
|
||||||
del rtn[-1]
|
del rtn[-1]
|
||||||
continue
|
continue
|
||||||
_util.handle_payload_error(x)
|
_exception.handle_payload_error(x)
|
||||||
[(key, value)] = x.items()
|
[(key, value)] = x.items()
|
||||||
_util.handle_graphql_errors(value)
|
_exception.handle_graphql_errors(value)
|
||||||
if "response" in value:
|
if "response" in value:
|
||||||
rtn[int(key[1:])] = value["response"]
|
rtn[int(key[1:])] = value["response"]
|
||||||
else:
|
else:
|
||||||
|
@@ -2,7 +2,16 @@ import attr
|
|||||||
import enum
|
import enum
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from ._core import log, attrs_default
|
from ._core import log, attrs_default
|
||||||
from . import _util, _session, _attachment, _location, _file, _quick_reply, _sticker
|
from . import (
|
||||||
|
_exception,
|
||||||
|
_util,
|
||||||
|
_session,
|
||||||
|
_attachment,
|
||||||
|
_location,
|
||||||
|
_file,
|
||||||
|
_quick_reply,
|
||||||
|
_sticker,
|
||||||
|
)
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +121,7 @@ class Message:
|
|||||||
"variables": _util.json_minimal({"data": data}),
|
"variables": _util.json_minimal({"data": data}),
|
||||||
}
|
}
|
||||||
j = self.session._payload_post("/webgraphql/mutation", data)
|
j = self.session._payload_post("/webgraphql/mutation", data)
|
||||||
_util.handle_graphql_errors(j)
|
_exception.handle_graphql_errors(j)
|
||||||
|
|
||||||
def fetch(self) -> "MessageData":
|
def fetch(self) -> "MessageData":
|
||||||
"""Fetch fresh `MessageData` object."""
|
"""Fetch fresh `MessageData` object."""
|
||||||
|
@@ -53,10 +53,7 @@ class Plan:
|
|||||||
}
|
}
|
||||||
j = thread.session._payload_post("/ajax/eventreminder/create", data)
|
j = thread.session._payload_post("/ajax/eventreminder/create", data)
|
||||||
if "error" in j:
|
if "error" in j:
|
||||||
raise _exception.FBchatFacebookError(
|
raise _exception.ExternalError("Failed creating plan", j["error"])
|
||||||
"Failed creating plan: {}".format(j["error"]),
|
|
||||||
fb_error_message=j["error"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def edit(
|
def edit(
|
||||||
self,
|
self,
|
||||||
|
@@ -101,7 +101,7 @@ class Poll:
|
|||||||
"/messaging/group_polling/update_vote/?dpr=1", data
|
"/messaging/group_polling/update_vote/?dpr=1", data
|
||||||
)
|
)
|
||||||
if j.get("status") != "success":
|
if j.get("status") != "success":
|
||||||
raise _exception.FBchatFacebookError(
|
raise _exception.ExternalError(
|
||||||
"Failed updating poll vote: {}".format(j.get("errorTitle")),
|
"Failed updating poll vote: {}".format(j.get("errorTitle")),
|
||||||
fb_error_message=j.get("errorMessage"),
|
j.get("errorMessage"),
|
||||||
)
|
)
|
||||||
|
@@ -13,9 +13,10 @@ FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"')
|
|||||||
|
|
||||||
def get_user_id(session):
|
def get_user_id(session):
|
||||||
# TODO: Optimize this `.get_dict()` call!
|
# 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:
|
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)
|
return str(rtn)
|
||||||
|
|
||||||
|
|
||||||
@@ -153,13 +154,15 @@ class Session:
|
|||||||
password: Facebook account password
|
password: Facebook account password
|
||||||
on_2fa_callback: Function that will be called, in case a 2FA code is needed.
|
on_2fa_callback: Function that will be called, in case a 2FA code is needed.
|
||||||
This should return the requested 2FA code.
|
This should return the requested 2FA code.
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: On failed login
|
|
||||||
"""
|
"""
|
||||||
session = session_factory()
|
session = session_factory()
|
||||||
|
|
||||||
soup = find_input_fields(session.get("https://m.facebook.com/").text)
|
try:
|
||||||
|
r = session.get("https://m.facebook.com/")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
|
soup = find_input_fields(r.text)
|
||||||
|
|
||||||
data = dict(
|
data = dict(
|
||||||
(elem["name"], elem["value"])
|
(elem["name"], elem["value"])
|
||||||
for elem in soup
|
for elem in soup
|
||||||
@@ -169,29 +172,37 @@ class Session:
|
|||||||
data["pass"] = password
|
data["pass"] = password
|
||||||
data["login"] = "Log In"
|
data["login"] = "Log In"
|
||||||
|
|
||||||
r = session.post("https://m.facebook.com/login.php?login_attempt=1", data=data)
|
try:
|
||||||
|
url = "https://m.facebook.com/login.php?login_attempt=1"
|
||||||
|
r = session.post(url, data=data)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
|
|
||||||
# Usually, 'Checkpoint' will refer to 2FA
|
# Usually, 'Checkpoint' will refer to 2FA
|
||||||
if "checkpoint" in r.url and ('id="approvals_code"' in r.text.lower()):
|
if "checkpoint" in r.url and ('id="approvals_code"' in r.text.lower()):
|
||||||
if not on_2fa_callback:
|
if not on_2fa_callback:
|
||||||
raise _exception.FBchatException(
|
raise ValueError(
|
||||||
"2FA code required, please add `on_2fa_callback` to .login"
|
"2FA code required, please add `on_2fa_callback` to .login"
|
||||||
)
|
)
|
||||||
code = on_2fa_callback()
|
code = on_2fa_callback()
|
||||||
|
try:
|
||||||
r = _2fa_helper(session, code, r)
|
r = _2fa_helper(session, code, r)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
|
|
||||||
# Sometimes Facebook tries to show the user a "Save Device" dialog
|
# Sometimes Facebook tries to show the user a "Save Device" dialog
|
||||||
if "save-device" in r.url:
|
if "save-device" in r.url:
|
||||||
|
try:
|
||||||
r = session.get("https://m.facebook.com/login/save-device/cancel/")
|
r = session.get("https://m.facebook.com/login/save-device/cancel/")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
|
|
||||||
if is_home(r.url):
|
if is_home(r.url):
|
||||||
return cls._from_session(session=session)
|
return cls._from_session(session=session)
|
||||||
else:
|
else:
|
||||||
code, msg = get_error_data(r.text, r.url)
|
code, msg = get_error_data(r.text, r.url)
|
||||||
raise _exception.FBchatFacebookError(
|
raise _exception.ExternalError(
|
||||||
"Login failed (Failed on url: {})".format(r.url),
|
"Login failed at url {!r}".format(r.url), msg, code=code
|
||||||
fb_error_code=code,
|
|
||||||
fb_error_message=msg,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_logged_in(self):
|
def is_logged_in(self):
|
||||||
@@ -202,36 +213,42 @@ class Session:
|
|||||||
"""
|
"""
|
||||||
# Send a request to the login url, to see if we're directed to the home page
|
# Send a request to the login url, to see if we're directed to the home page
|
||||||
url = "https://m.facebook.com/login.php?login_attempt=1"
|
url = "https://m.facebook.com/login.php?login_attempt=1"
|
||||||
|
try:
|
||||||
r = self._session.get(url, allow_redirects=False)
|
r = self._session.get(url, allow_redirects=False)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
return "Location" in r.headers and is_home(r.headers["Location"])
|
return "Location" in r.headers and is_home(r.headers["Location"])
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
"""Safely log out the user.
|
"""Safely log out the user.
|
||||||
|
|
||||||
The session object must not be used after this action has been performed!
|
The session object must not be used after this action has been performed!
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: On failed logout
|
|
||||||
"""
|
"""
|
||||||
logout_h = self._logout_h
|
logout_h = self._logout_h
|
||||||
if not logout_h:
|
if not logout_h:
|
||||||
url = _util.prefix_url("/bluebar/modern_settings_menu/")
|
url = _util.prefix_url("/bluebar/modern_settings_menu/")
|
||||||
|
try:
|
||||||
h_r = self._session.post(url, data={"pmid": "4"})
|
h_r = self._session.post(url, data={"pmid": "4"})
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
logout_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1)
|
logout_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1)
|
||||||
|
|
||||||
url = _util.prefix_url("/logout.php")
|
url = _util.prefix_url("/logout.php")
|
||||||
|
try:
|
||||||
r = self._session.get(url, params={"ref": "mb", "h": logout_h})
|
r = self._session.get(url, params={"ref": "mb", "h": logout_h})
|
||||||
if not r.ok:
|
except requests.RequestException as e:
|
||||||
raise exception.FBchatException(
|
_exception.handle_requests_error(e)
|
||||||
"Failed logging out: {}".format(r.status_code)
|
_exception.handle_http_error(r.status_code)
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_session(cls, session):
|
def _from_session(cls, session):
|
||||||
# TODO: Automatically set user_id when the cookie changes in the session
|
# TODO: Automatically set user_id when the cookie changes in the session
|
||||||
user_id = get_user_id(session)
|
user_id = get_user_id(session)
|
||||||
|
|
||||||
|
try:
|
||||||
r = session.get(_util.prefix_url("/"))
|
r = session.get(_util.prefix_url("/"))
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
|
|
||||||
soup = find_input_fields(r.text)
|
soup = find_input_fields(r.text)
|
||||||
|
|
||||||
@@ -242,9 +259,7 @@ class Session:
|
|||||||
# Fall back to searching with a regex
|
# Fall back to searching with a regex
|
||||||
res = FB_DTSG_REGEX.search(r.text)
|
res = FB_DTSG_REGEX.search(r.text)
|
||||||
if not res:
|
if not res:
|
||||||
raise _exception.FBchatException(
|
raise ValueError("Failed loading session, could not find fb_dtsg")
|
||||||
"Failed loading session: Could not find fb_dtsg"
|
|
||||||
)
|
|
||||||
fb_dtsg = res.group(1)
|
fb_dtsg = res.group(1)
|
||||||
|
|
||||||
revision = int(r.text.split('"client_revision":', 1)[1].split(",", 1)[0])
|
revision = int(r.text.split('"client_revision":', 1)[1].split(",", 1)[0])
|
||||||
@@ -274,9 +289,6 @@ class Session:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
cookies (dict): A dictionary containing session cookies
|
cookies (dict): A dictionary containing session cookies
|
||||||
|
|
||||||
Raises:
|
|
||||||
FBchatException: If given invalid cookies
|
|
||||||
"""
|
"""
|
||||||
session = session_factory()
|
session = session_factory()
|
||||||
session.cookies = requests.cookies.merge_cookies(session.cookies, cookies)
|
session.cookies = requests.cookies.merge_cookies(session.cookies, cookies)
|
||||||
@@ -284,13 +296,19 @@ class Session:
|
|||||||
|
|
||||||
def _get(self, url, params, error_retries=3):
|
def _get(self, url, params, error_retries=3):
|
||||||
params.update(self._get_params())
|
params.update(self._get_params())
|
||||||
|
try:
|
||||||
r = self._session.get(_util.prefix_url(url), params=params)
|
r = self._session.get(_util.prefix_url(url), params=params)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
content = _util.check_request(r)
|
content = _util.check_request(r)
|
||||||
return _util.to_json(content)
|
return _util.to_json(content)
|
||||||
|
|
||||||
def _post(self, url, data, files=None, as_graphql=False):
|
def _post(self, url, data, files=None, as_graphql=False):
|
||||||
data.update(self._get_params())
|
data.update(self._get_params())
|
||||||
|
try:
|
||||||
r = self._session.post(_util.prefix_url(url), data=data, files=files)
|
r = self._session.post(_util.prefix_url(url), data=data, files=files)
|
||||||
|
except requests.RequestException as e:
|
||||||
|
_exception.handle_requests_error(e)
|
||||||
content = _util.check_request(r)
|
content = _util.check_request(r)
|
||||||
if as_graphql:
|
if as_graphql:
|
||||||
return _graphql.response_to_json(content)
|
return _graphql.response_to_json(content)
|
||||||
@@ -299,11 +317,11 @@ class Session:
|
|||||||
|
|
||||||
def _payload_post(self, url, data, files=None):
|
def _payload_post(self, url, data, files=None):
|
||||||
j = self._post(url, data, files=files)
|
j = self._post(url, data, files=files)
|
||||||
_util.handle_payload_error(j)
|
_exception.handle_payload_error(j)
|
||||||
try:
|
try:
|
||||||
return j["payload"]
|
return j["payload"]
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError) as e:
|
||||||
raise _exception.FBchatException("Missing payload: {}".format(j))
|
raise _exception.ParseError("Missing payload", data=j) from e
|
||||||
|
|
||||||
def _graphql_requests(self, *queries):
|
def _graphql_requests(self, *queries):
|
||||||
data = {
|
data = {
|
||||||
@@ -330,9 +348,7 @@ class Session:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if len(j["metadata"]) != len(files):
|
if len(j["metadata"]) != len(files):
|
||||||
raise _exception.FBchatException(
|
raise _exception.ParseError("Some files could not be uploaded", data=j)
|
||||||
"Some files could not be uploaded: {}, {}".format(j, files)
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(data[_util.mimetype_to_key(data["filetype"])], data["filetype"])
|
(data[_util.mimetype_to_key(data["filetype"])], data["filetype"])
|
||||||
@@ -351,6 +367,8 @@ class Session:
|
|||||||
data["ephemeral_ttl_mode:"] = "0"
|
data["ephemeral_ttl_mode:"] = "0"
|
||||||
j = self._post("/messaging/send/", data)
|
j = self._post("/messaging/send/", data)
|
||||||
|
|
||||||
|
_exception.handle_payload_error(j)
|
||||||
|
|
||||||
# update JS token if received in response
|
# update JS token if received in response
|
||||||
fb_dtsg = _util.get_jsmods_require(j, 2)
|
fb_dtsg = _util.get_jsmods_require(j, 2)
|
||||||
if fb_dtsg is not None:
|
if fb_dtsg is not None:
|
||||||
@@ -366,7 +384,4 @@ class Session:
|
|||||||
log.warning("Got multiple message ids' back: {}".format(message_ids))
|
log.warning("Got multiple message ids' back: {}".format(message_ids))
|
||||||
return message_ids[0]
|
return message_ids[0]
|
||||||
except (KeyError, IndexError, TypeError) as e:
|
except (KeyError, IndexError, TypeError) as e:
|
||||||
raise _exception.FBchatException(
|
raise _exception.ParseError("No message IDs could be found", data=j) from e
|
||||||
"Error when sending message: "
|
|
||||||
"No message IDs could be found: {}".format(j)
|
|
||||||
)
|
|
||||||
|
@@ -313,9 +313,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if j.get("message_thread") is None:
|
if j.get("message_thread") is None:
|
||||||
raise _exception.FBchatException(
|
raise _exception.ParseError("Could not fetch messages", data=j)
|
||||||
"Could not fetch thread {}: {}".format(self.id, j)
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Should we parse the returned thread data, too?
|
# TODO: Should we parse the returned thread data, too?
|
||||||
|
|
||||||
@@ -361,6 +359,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
|||||||
_graphql.from_query_id("515216185516880", data)
|
_graphql.from_query_id("515216185516880", data)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not j[self.id]:
|
||||||
|
raise _exception.ParseError("Could not find images", data=j)
|
||||||
|
|
||||||
result = j[self.id]["message_shared_media"]
|
result = j[self.id]["message_shared_media"]
|
||||||
|
|
||||||
print(len(result["edges"]))
|
print(len(result["edges"]))
|
||||||
@@ -481,10 +482,7 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
|||||||
}
|
}
|
||||||
j = self.session._payload_post("/mercury/attachments/forward/", data)
|
j = self.session._payload_post("/mercury/attachments/forward/", data)
|
||||||
if not j.get("success"):
|
if not j.get("success"):
|
||||||
raise _exception.FBchatFacebookError(
|
raise _exception.ExternalError("Failed forwarding attachment", j["error"])
|
||||||
"Failed forwarding attachment: {}".format(j["error"]),
|
|
||||||
fb_error_message=j["error"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _set_typing(self, typing):
|
def _set_typing(self, typing):
|
||||||
data = {
|
data = {
|
||||||
@@ -547,9 +545,9 @@ class ThreadABC(metaclass=abc.ABCMeta):
|
|||||||
"/messaging/group_polling/create_poll/?dpr=1", data
|
"/messaging/group_polling/create_poll/?dpr=1", data
|
||||||
)
|
)
|
||||||
if j.get("status") != "success":
|
if j.get("status") != "success":
|
||||||
raise _exception.FBchatFacebookError(
|
raise _exception.ExternalError(
|
||||||
"Failed creating poll: {}".format(j.get("errorTitle")),
|
"Failed creating poll: {}".format(j.get("errorTitle")),
|
||||||
fb_error_message=j.get("errorMessage"),
|
j.get("errorMessage"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def mute(self, duration: datetime.timedelta = None):
|
def mute(self, duration: datetime.timedelta = None):
|
||||||
|
@@ -5,13 +5,7 @@ import random
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from ._core import log
|
from ._core import log
|
||||||
from ._exception import (
|
from . import _exception
|
||||||
FBchatException,
|
|
||||||
FBchatFacebookError,
|
|
||||||
FBchatInvalidParameters,
|
|
||||||
FBchatNotLoggedIn,
|
|
||||||
FBchatPleaseRefresh,
|
|
||||||
)
|
|
||||||
|
|
||||||
from typing import Iterable, Optional
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
@@ -57,8 +51,8 @@ def strip_json_cruft(text):
|
|||||||
"""Removes `for(;;);` (and other cruft) that preceeds JSON responses."""
|
"""Removes `for(;;);` (and other cruft) that preceeds JSON responses."""
|
||||||
try:
|
try:
|
||||||
return text[text.index("{") :]
|
return text[text.index("{") :]
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise FBchatException("No JSON object found: {!r}".format(text))
|
raise _exception.ParseError("No JSON object found", data=text) from e
|
||||||
|
|
||||||
|
|
||||||
def get_decoded_r(r):
|
def get_decoded_r(r):
|
||||||
@@ -72,8 +66,8 @@ def get_decoded(content):
|
|||||||
def parse_json(content):
|
def parse_json(content):
|
||||||
try:
|
try:
|
||||||
return json.loads(content)
|
return json.loads(content)
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
raise FBchatFacebookError("Error while parsing JSON: {!r}".format(content))
|
raise _exception.ParseError("Error while parsing JSON", data=content) from e
|
||||||
|
|
||||||
|
|
||||||
def digit_to_char(digit):
|
def digit_to_char(digit):
|
||||||
@@ -109,67 +103,16 @@ def generate_offline_threading_id():
|
|||||||
return str(int(msgs, 2))
|
return str(int(msgs, 2))
|
||||||
|
|
||||||
|
|
||||||
def handle_payload_error(j):
|
|
||||||
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"]
|
|
||||||
# "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_errors(j):
|
|
||||||
errors = []
|
|
||||||
if j.get("error"):
|
|
||||||
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(
|
|
||||||
"GraphQL error #{}: {} / {!r}".format(
|
|
||||||
error.get("code"), error.get("message"), error.get("debug_info")
|
|
||||||
),
|
|
||||||
fb_error_code=error.get("code"),
|
|
||||||
fb_error_message=error.get("message"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_request(r):
|
def check_request(r):
|
||||||
check_http_code(r.status_code)
|
_exception.handle_http_error(r.status_code)
|
||||||
content = get_decoded_r(r)
|
content = get_decoded_r(r)
|
||||||
check_content(content)
|
check_content(content)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def check_http_code(code):
|
|
||||||
msg = "Error when sending request: Got {} response.".format(code)
|
|
||||||
if code == 404:
|
|
||||||
raise FBchatFacebookError(
|
|
||||||
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):
|
def check_content(content, as_json=True):
|
||||||
if content is None or len(content) == 0:
|
if content is None or len(content) == 0:
|
||||||
raise FBchatFacebookError("Error when sending request: Got empty response")
|
raise _exception.HTTPError("Error when sending request: Got empty response")
|
||||||
|
|
||||||
|
|
||||||
def to_json(content):
|
def to_json(content):
|
||||||
|
@@ -3,7 +3,7 @@ import py_compile
|
|||||||
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from os import path, environ
|
from os import path, environ
|
||||||
from fbchat import FBchatException, Message, Client
|
from fbchat import FacebookError, Message, Client
|
||||||
|
|
||||||
|
|
||||||
def test_examples():
|
def test_examples():
|
||||||
@@ -23,7 +23,7 @@ def test_login(client1):
|
|||||||
|
|
||||||
assert not client1.is_logged_in()
|
assert not client1.is_logged_in()
|
||||||
|
|
||||||
with pytest.raises(FBchatException):
|
with pytest.raises(FacebookError):
|
||||||
client1.login("<invalid email>", "<invalid password>", max_tries=1)
|
client1.login("<invalid email>", "<invalid password>", max_tries=1)
|
||||||
|
|
||||||
client1.login(email, password)
|
client1.login(email, password)
|
||||||
|
228
tests/test_exception.py
Normal file
228
tests/test_exception.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
from fbchat import (
|
||||||
|
FacebookError,
|
||||||
|
HTTPError,
|
||||||
|
ParseError,
|
||||||
|
ExternalError,
|
||||||
|
GraphQLError,
|
||||||
|
InvalidParameters,
|
||||||
|
NotLoggedIn,
|
||||||
|
PleaseRefresh,
|
||||||
|
)
|
||||||
|
from fbchat._exception import (
|
||||||
|
handle_payload_error,
|
||||||
|
handle_graphql_errors,
|
||||||
|
handle_http_error,
|
||||||
|
handle_requests_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_DATA = [
|
||||||
|
(NotLoggedIn, 1357001, "Not logged in", "Please log in to continue."),
|
||||||
|
(
|
||||||
|
PleaseRefresh,
|
||||||
|
1357004,
|
||||||
|
"Sorry, something went wrong",
|
||||||
|
"Please try closing and re-opening your browser window.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
InvalidParameters,
|
||||||
|
1357031,
|
||||||
|
"This content is no longer available",
|
||||||
|
(
|
||||||
|
"The content you requested cannot be displayed at the moment. It may be"
|
||||||
|
" temporarily unavailable, the link you clicked on may have expired or you"
|
||||||
|
" may not have permission to view this page."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
InvalidParameters,
|
||||||
|
1545010,
|
||||||
|
"Messages Unavailable",
|
||||||
|
(
|
||||||
|
"Sorry, messages are temporarily unavailable."
|
||||||
|
" Please try again in a few minutes."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ExternalError,
|
||||||
|
1545026,
|
||||||
|
"Unable to Attach File",
|
||||||
|
(
|
||||||
|
"The type of file you're trying to attach isn't allowed."
|
||||||
|
" Please try again with a different format."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(InvalidParameters, 1545003, "Invalid action", "You cannot perform that action."),
|
||||||
|
(
|
||||||
|
ExternalError,
|
||||||
|
1545012,
|
||||||
|
"Temporary Failure",
|
||||||
|
"There was a temporary error, please try again.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exception,code,summary,description", ERROR_DATA)
|
||||||
|
def test_handle_payload_error(exception, code, summary, description):
|
||||||
|
data = {"error": code, "errorSummary": summary, "errorDescription": description}
|
||||||
|
with pytest.raises(exception, match=r"#\d+ .+:"):
|
||||||
|
handle_payload_error(data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_payload_error_no_error():
|
||||||
|
assert handle_payload_error({}) is None
|
||||||
|
assert handle_payload_error({"payload": {"abc": ["Something", "else"]}}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_crash():
|
||||||
|
error = {
|
||||||
|
"allow_user_retry": False,
|
||||||
|
"api_error_code": -1,
|
||||||
|
"code": 1675030,
|
||||||
|
"debug_info": None,
|
||||||
|
"description": "Error performing query.",
|
||||||
|
"fbtrace_id": "ABCDEFG",
|
||||||
|
"is_silent": False,
|
||||||
|
"is_transient": False,
|
||||||
|
"message": (
|
||||||
|
'Errors while executing operation "MessengerThreadSharedLinks":'
|
||||||
|
" At Query.message_thread: Field implementation threw an exception."
|
||||||
|
" Check your server logs for more information."
|
||||||
|
),
|
||||||
|
"path": ["message_thread"],
|
||||||
|
"query_path": None,
|
||||||
|
"requires_reauth": False,
|
||||||
|
"severity": "CRITICAL",
|
||||||
|
"summary": "Query error",
|
||||||
|
}
|
||||||
|
with pytest.raises(
|
||||||
|
GraphQLError, match="#1675030 Query error: Errors while executing"
|
||||||
|
):
|
||||||
|
handle_graphql_errors({"data": {"message_thread": None}, "errors": [error]})
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_invalid_values():
|
||||||
|
error = {
|
||||||
|
"message": (
|
||||||
|
'Invalid values provided for variables of operation "MessengerThreadlist":'
|
||||||
|
' Value ""as"" cannot be used for variable "$limit": Expected an integer'
|
||||||
|
' value, got "as".'
|
||||||
|
),
|
||||||
|
"severity": "CRITICAL",
|
||||||
|
"code": 1675012,
|
||||||
|
"api_error_code": None,
|
||||||
|
"summary": "Your request couldn't be processed",
|
||||||
|
"description": (
|
||||||
|
"There was a problem with this request."
|
||||||
|
" We're working on getting it fixed as soon as we can."
|
||||||
|
),
|
||||||
|
"is_silent": False,
|
||||||
|
"is_transient": False,
|
||||||
|
"requires_reauth": False,
|
||||||
|
"allow_user_retry": False,
|
||||||
|
"debug_info": None,
|
||||||
|
"query_path": None,
|
||||||
|
"fbtrace_id": "ABCDEFG",
|
||||||
|
"www_request_id": "AABBCCDDEEFFGG",
|
||||||
|
}
|
||||||
|
msg = "#1675012 Your request couldn't be processed: Invalid values"
|
||||||
|
with pytest.raises(GraphQLError, match=msg):
|
||||||
|
handle_graphql_errors({"errors": [error]})
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_no_message():
|
||||||
|
error = {
|
||||||
|
"code": 1675012,
|
||||||
|
"api_error_code": None,
|
||||||
|
"summary": "Your request couldn't be processed",
|
||||||
|
"description": (
|
||||||
|
"There was a problem with this request."
|
||||||
|
" We're working on getting it fixed as soon as we can."
|
||||||
|
),
|
||||||
|
"is_silent": False,
|
||||||
|
"is_transient": False,
|
||||||
|
"requires_reauth": False,
|
||||||
|
"allow_user_retry": False,
|
||||||
|
"debug_info": None,
|
||||||
|
"query_path": None,
|
||||||
|
"fbtrace_id": "ABCDEFG",
|
||||||
|
"www_request_id": "AABBCCDDEEFFGG",
|
||||||
|
"sentry_block_user_info": None,
|
||||||
|
"help_center_id": None,
|
||||||
|
}
|
||||||
|
msg = "#1675012 Your request couldn't be processed: "
|
||||||
|
with pytest.raises(GraphQLError, match=msg):
|
||||||
|
handle_graphql_errors({"errors": [error]})
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_no_summary():
|
||||||
|
error = {
|
||||||
|
"message": (
|
||||||
|
'Errors while executing operation "MessengerViewerContactMethods":'
|
||||||
|
" At Query.viewer:Viewer.all_emails: Field implementation threw an"
|
||||||
|
" exception. Check your server logs for more information."
|
||||||
|
),
|
||||||
|
"severity": "ERROR",
|
||||||
|
"path": ["viewer", "all_emails"],
|
||||||
|
}
|
||||||
|
with pytest.raises(GraphQLError, match="Unknown error: Errors while executing"):
|
||||||
|
handle_graphql_errors(
|
||||||
|
{"data": {"viewer": {"user": None, "all_emails": []}}, "errors": [error]}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_syntax_error():
|
||||||
|
error = {
|
||||||
|
"code": 1675001,
|
||||||
|
"api_error_code": None,
|
||||||
|
"summary": "Query Syntax Error",
|
||||||
|
"description": "Syntax error.",
|
||||||
|
"is_silent": True,
|
||||||
|
"is_transient": False,
|
||||||
|
"requires_reauth": False,
|
||||||
|
"allow_user_retry": False,
|
||||||
|
"debug_info": 'Unexpected ">" at character 328: Expected ")".',
|
||||||
|
"query_path": None,
|
||||||
|
"fbtrace_id": "ABCDEFG",
|
||||||
|
"www_request_id": "AABBCCDDEEFFGG",
|
||||||
|
"sentry_block_user_info": None,
|
||||||
|
"help_center_id": None,
|
||||||
|
}
|
||||||
|
msg = "#1675001 Query Syntax Error: "
|
||||||
|
with pytest.raises(GraphQLError, match=msg):
|
||||||
|
handle_graphql_errors({"response": None, "error": error})
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_errors_singular_error_key():
|
||||||
|
with pytest.raises(GraphQLError, match="#123"):
|
||||||
|
handle_graphql_errors({"error": {"code": 123}})
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_graphql_errors_no_error():
|
||||||
|
assert handle_graphql_errors({"data": {"message_thread": None}}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_http_error():
|
||||||
|
with pytest.raises(HTTPError):
|
||||||
|
handle_http_error(400)
|
||||||
|
with pytest.raises(HTTPError):
|
||||||
|
handle_http_error(500)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_http_error_404_handling():
|
||||||
|
with pytest.raises(HTTPError, match="invalid id"):
|
||||||
|
handle_http_error(404)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_http_error_no_error():
|
||||||
|
assert handle_http_error(200) is None
|
||||||
|
assert handle_http_error(302) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_requests_error():
|
||||||
|
with pytest.raises(HTTPError, match="Connection error"):
|
||||||
|
handle_requests_error(requests.ConnectionError())
|
||||||
|
with pytest.raises(HTTPError, match="Requests error"):
|
||||||
|
handle_requests_error(requests.RequestException())
|
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fbchat import PlanData, FBchatFacebookError
|
from fbchat import PlanData
|
||||||
from utils import random_hex, subset
|
from utils import random_hex, subset
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
from fbchat import FBchatFacebookError, Message, Mention
|
from fbchat import Message, Mention
|
||||||
from utils import subset, STICKER_LIST, EMOJI_LIST, TEXT_LIST
|
from utils import subset, STICKER_LIST, EMOJI_LIST, TEXT_LIST
|
||||||
|
|
||||||
pytestmark = pytest.mark.online
|
pytestmark = pytest.mark.online
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from fbchat import Message, FBchatFacebookError
|
from fbchat import Message, FacebookError
|
||||||
from utils import random_hex, subset
|
from utils import random_hex, subset
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
@@ -60,10 +60,8 @@ def test_change_nickname(client, client_all, catch_event, compare):
|
|||||||
"😂",
|
"😂",
|
||||||
"😕",
|
"😕",
|
||||||
"😍",
|
"😍",
|
||||||
pytest.param("🙃", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
pytest.param("🙃", marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
pytest.param(
|
pytest.param("not an emoji", marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
"not an emoji", marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_change_emoji(client, catch_event, compare, emoji):
|
def test_change_emoji(client, catch_event, compare, emoji):
|
||||||
@@ -97,7 +95,7 @@ def test_change_color(client, catch_event, compare):
|
|||||||
assert compare(x, new_color="#44bec7")
|
assert compare(x, new_color="#44bec7")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(raises=FBchatFacebookError, reason="Should fail, but doesn't")
|
@pytest.mark.xfail(raises=FacebookError, reason="Should fail, but doesn't")
|
||||||
def test_change_color_invalid(client):
|
def test_change_color_invalid(client):
|
||||||
class InvalidColor:
|
class InvalidColor:
|
||||||
value = "#0077ff"
|
value = "#0077ff"
|
||||||
|
@@ -7,9 +7,6 @@ from fbchat._util import (
|
|||||||
str_base,
|
str_base,
|
||||||
generate_message_id,
|
generate_message_id,
|
||||||
get_signature_id,
|
get_signature_id,
|
||||||
handle_payload_error,
|
|
||||||
handle_graphql_errors,
|
|
||||||
check_http_code,
|
|
||||||
get_jsmods_require,
|
get_jsmods_require,
|
||||||
require_list,
|
require_list,
|
||||||
mimetype_to_key,
|
mimetype_to_key,
|
||||||
@@ -33,7 +30,7 @@ def test_strip_json_cruft():
|
|||||||
def test_strip_json_cruft_invalid():
|
def test_strip_json_cruft_invalid():
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
strip_json_cruft(None)
|
strip_json_cruft(None)
|
||||||
with pytest.raises(fbchat.FBchatException, match="No JSON object found"):
|
with pytest.raises(fbchat.ParseError, match="No JSON object found"):
|
||||||
strip_json_cruft("No JSON object here!")
|
strip_json_cruft("No JSON object here!")
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +39,7 @@ def test_parse_json():
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_json_invalid():
|
def test_parse_json_invalid():
|
||||||
with pytest.raises(fbchat.FBchatFacebookError, match="Error while parsing JSON"):
|
with pytest.raises(fbchat.ParseError, match="Error while parsing JSON"):
|
||||||
parse_json("No JSON object here!")
|
parse_json("No JSON object here!")
|
||||||
|
|
||||||
|
|
||||||
@@ -71,125 +68,6 @@ def test_get_signature_id():
|
|||||||
get_signature_id()
|
get_signature_id()
|
||||||
|
|
||||||
|
|
||||||
ERROR_DATA = [
|
|
||||||
(
|
|
||||||
fbchat._exception.FBchatNotLoggedIn,
|
|
||||||
1357001,
|
|
||||||
"Not logged in",
|
|
||||||
"Please log in to continue.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat._exception.FBchatPleaseRefresh,
|
|
||||||
1357004,
|
|
||||||
"Sorry, something went wrong",
|
|
||||||
"Please try closing and re-opening your browser window.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat._exception.FBchatInvalidParameters,
|
|
||||||
1357031,
|
|
||||||
"This content is no longer available",
|
|
||||||
(
|
|
||||||
"The content you requested cannot be displayed at the moment. It may be"
|
|
||||||
" temporarily unavailable, the link you clicked on may have expired or you"
|
|
||||||
" may not have permission to view this page."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat._exception.FBchatInvalidParameters,
|
|
||||||
1545010,
|
|
||||||
"Messages Unavailable",
|
|
||||||
(
|
|
||||||
"Sorry, messages are temporarily unavailable."
|
|
||||||
" Please try again in a few minutes."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat.FBchatFacebookError,
|
|
||||||
1545026,
|
|
||||||
"Unable to Attach File",
|
|
||||||
(
|
|
||||||
"The type of file you're trying to attach isn't allowed."
|
|
||||||
" Please try again with a different format."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat._exception.FBchatInvalidParameters,
|
|
||||||
1545003,
|
|
||||||
"Invalid action",
|
|
||||||
"You cannot perform that action.",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
fbchat.FBchatFacebookError,
|
|
||||||
1545012,
|
|
||||||
"Temporary Failure",
|
|
||||||
"There was a temporary error, please try again.",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exception,code,description,summary", ERROR_DATA)
|
|
||||||
def test_handle_payload_error(exception, code, summary, description):
|
|
||||||
data = {"error": code, "errorSummary": summary, "errorDescription": description}
|
|
||||||
with pytest.raises(exception, match=r"Error #\d+ when sending request"):
|
|
||||||
handle_payload_error(data)
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_payload_error_no_error():
|
|
||||||
assert handle_payload_error({}) is None
|
|
||||||
assert handle_payload_error({"payload": {"abc": ["Something", "else"]}}) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_graphql_errors():
|
|
||||||
error = {
|
|
||||||
"allow_user_retry": False,
|
|
||||||
"api_error_code": -1,
|
|
||||||
"code": 1675030,
|
|
||||||
"debug_info": None,
|
|
||||||
"description": "Error performing query.",
|
|
||||||
"fbtrace_id": "CLkuLR752sB",
|
|
||||||
"is_silent": False,
|
|
||||||
"is_transient": False,
|
|
||||||
"message": (
|
|
||||||
'Errors while executing operation "MessengerThreadSharedLinks":'
|
|
||||||
" At Query.message_thread: Field implementation threw an exception."
|
|
||||||
" Check your server logs for more information."
|
|
||||||
),
|
|
||||||
"path": ["message_thread"],
|
|
||||||
"query_path": None,
|
|
||||||
"requires_reauth": False,
|
|
||||||
"severity": "CRITICAL",
|
|
||||||
"summary": "Query error",
|
|
||||||
}
|
|
||||||
with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error"):
|
|
||||||
handle_graphql_errors({"data": {"message_thread": None}, "errors": [error]})
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_graphql_errors_singular_error_key():
|
|
||||||
with pytest.raises(fbchat.FBchatFacebookError, match="GraphQL error #123"):
|
|
||||||
handle_graphql_errors({"error": {"code": 123}})
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_graphql_errors_no_error():
|
|
||||||
assert handle_graphql_errors({"data": {"message_thread": None}}) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_http_code():
|
|
||||||
with pytest.raises(fbchat.FBchatFacebookError):
|
|
||||||
check_http_code(400)
|
|
||||||
with pytest.raises(fbchat.FBchatFacebookError):
|
|
||||||
check_http_code(500)
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_http_code_404_handling():
|
|
||||||
with pytest.raises(fbchat.FBchatFacebookError, match="invalid id"):
|
|
||||||
check_http_code(404)
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_http_code_no_error():
|
|
||||||
assert check_http_code(200) is None
|
|
||||||
assert check_http_code(302) is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_jsmods_require_get_image_url():
|
def test_get_jsmods_require_get_image_url():
|
||||||
data = {
|
data = {
|
||||||
"__ar": 1,
|
"__ar": 1,
|
||||||
|
@@ -5,7 +5,7 @@ import pytest
|
|||||||
from os import environ
|
from os import environ
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from fbchat import EmojiSize, FBchatFacebookError, Sticker, Client
|
from fbchat import EmojiSize, FacebookError, Sticker, Client
|
||||||
|
|
||||||
log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler())
|
log = logging.getLogger("fbchat.tests").addHandler(logging.NullHandler())
|
||||||
|
|
||||||
@@ -23,12 +23,8 @@ EMOJI_LIST = [
|
|||||||
|
|
||||||
STICKER_LIST = [
|
STICKER_LIST = [
|
||||||
Sticker(id="767334476626295"),
|
Sticker(id="767334476626295"),
|
||||||
pytest.param(
|
pytest.param(Sticker(id="0"), marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
Sticker(id="0"), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
pytest.param(Sticker(id=None), marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
Sticker(id=None), marks=[pytest.mark.xfail(raises=FBchatFacebookError)]
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
TEXT_LIST = [
|
TEXT_LIST = [
|
||||||
@@ -37,8 +33,8 @@ TEXT_LIST = [
|
|||||||
"\\\n\t%?&'\"",
|
"\\\n\t%?&'\"",
|
||||||
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
|
"ˁҭʚ¹Ʋջوװ՞ޱɣࠚԹБɑȑңКએ֭ʗыԈٌʼőԈ×௴nચϚࠖణٔє܅Ԇޑط",
|
||||||
"a" * 20000, # Maximum amount of characters you can send
|
"a" * 20000, # Maximum amount of characters you can send
|
||||||
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
pytest.param("a" * 20001, marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
pytest.param(None, marks=[pytest.mark.xfail(raises=FBchatFacebookError)]),
|
pytest.param(None, marks=[pytest.mark.xfail(raises=FacebookError)]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user