Merge pull request #466 from carpedm20/various-removals

Various removals
This commit is contained in:
Mads Marquart
2019-09-08 18:51:20 +02:00
committed by GitHub
11 changed files with 57 additions and 348 deletions

View File

@@ -21,8 +21,8 @@ Traceback (most recent call last):
File "[site-packages]/fbchat/client.py", line 78, in __init__ File "[site-packages]/fbchat/client.py", line 78, in __init__
self.login(email, password, max_tries) self.login(email, password, max_tries)
File "[site-packages]/fbchat/client.py", line 407, in login File "[site-packages]/fbchat/client.py", line 407, in login
raise FBchatUserError('Login failed. Check email/password. (Failed on URL: {})'.format(login_url)) raise FBchatException('Login failed. Check email/password. (Failed on URL: {})'.format(login_url))
fbchat.models.FBchatUserError: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1) fbchat.FBchatException: Login failed. Check email/password. (Failed on URL: https://m.facebook.com/login.php?login_attempt=1)
``` ```
## Environment information ## Environment information

View File

@@ -38,7 +38,6 @@ Exceptions
.. autoexception:: FBchatException() .. autoexception:: FBchatException()
.. autoexception:: FBchatFacebookError() .. autoexception:: FBchatFacebookError()
.. autoexception:: FBchatUserError()
Attachments Attachments
----------- -----------

View File

@@ -11,10 +11,10 @@ _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 ._exception import FBchatException, FBchatFacebookError, FBchatUserError from ._exception import FBchatException, FBchatFacebookError
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
from ._user import TypingStatus, User, ActiveStatus from ._user import TypingStatus, User, ActiveStatus
from ._group import Group, Room from ._group import Group
from ._page import Page from ._page import Page
from ._message import EmojiSize, MessageReaction, Mention, Message from ._message import EmojiSize, MessageReaction, Mention, Message
from ._attachment import Attachment, UnsentMessage, ShareAttachment from ._attachment import Attachment, UnsentMessage, ShareAttachment

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
from ._core import log from ._core import log
from . import _util, _graphql, _state from . import _util, _graphql, _state
from ._exception import FBchatException, FBchatFacebookError, FBchatUserError from ._exception import FBchatException, FBchatFacebookError
from ._thread import ThreadType, ThreadLocation, ThreadColor from ._thread import ThreadType, ThreadLocation, ThreadColor
from ._user import TypingStatus, User, ActiveStatus from ._user import TypingStatus, User, ActiveStatus
from ._group import Group from ._group import Group
@@ -43,25 +43,6 @@ class Client:
to provide custom event handling (mainly useful while listening). to provide custom event handling (mainly useful while listening).
""" """
listening = False
"""Whether the client is listening.
Used when creating an external event loop to determine when to stop listening.
"""
@property
def ssl_verify(self):
"""Verify SSL certificate.
Set to False to allow debugging with a proxy.
"""
# TODO: Deprecate this
return self._state._session.verify
@ssl_verify.setter
def ssl_verify(self, value):
self._state._session.verify = value
@property @property
def uid(self): def uid(self):
"""The ID of the client. """The ID of the client.
@@ -70,16 +51,12 @@ class Client:
""" """
return self._uid return self._uid
def __init__( def __init__(self, email, password, session_cookies=None):
self, email, password, user_agent=None, max_tries=5, session_cookies=None
):
"""Initialize and log in the client. """Initialize and log in the client.
Args: Args:
email: Facebook ``email``, ``id`` or ``phone number`` email: Facebook ``email``, ``id`` or ``phone number``
password: Facebook account password password: Facebook account password
user_agent: Custom user agent to use when sending requests. If `None`, user agent will be chosen from a premade list
max_tries (int): Maximum number of times to try logging in
session_cookies (dict): Cookies from a previous session (Will default to login if these are invalid) session_cookies (dict): Cookies from a previous session (Will default to login if these are invalid)
Raises: Raises:
@@ -87,8 +64,6 @@ class Client:
""" """
self._sticky, self._pool = (None, None) self._sticky, self._pool = (None, None)
self._seq = "0" self._seq = "0"
self._default_thread_id = None
self._default_thread_type = None
self._pull_channel = 0 self._pull_channel = 0
self._markAlive = True self._markAlive = True
self._buddylist = dict() self._buddylist = dict()
@@ -96,10 +71,10 @@ class Client:
# If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login # If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login
if ( if (
not session_cookies not session_cookies
or not self.setSession(session_cookies, user_agent=user_agent) or not self.setSession(session_cookies)
or not self.isLoggedIn() or not self.isLoggedIn()
): ):
self.login(email, password, max_tries, user_agent=user_agent) self.login(email, password)
""" """
INTERNAL REQUEST METHODS INTERNAL REQUEST METHODS
@@ -160,7 +135,7 @@ class Client:
""" """
return self._state.get_cookies() return self._state.get_cookies()
def setSession(self, session_cookies, user_agent=None): def setSession(self, session_cookies):
"""Load session cookies. """Load session cookies.
Args: Args:
@@ -171,16 +146,14 @@ class Client:
""" """
try: try:
# Load cookies into current session # Load cookies into current session
self._state = _state.State.from_cookies( self._state = _state.State.from_cookies(session_cookies)
session_cookies, user_agent=user_agent
)
self._uid = self._state.user_id self._uid = self._state.user_id
except Exception as e: except Exception as e:
log.exception("Failed loading session") log.exception("Failed loading session")
return False return False
return True return True
def login(self, email, password, max_tries=5, user_agent=None): def login(self, email, password):
"""Login the user, using ``email`` and ``password``. """Login the user, using ``email`` and ``password``.
If the user is already logged in, this will do a re-login. If the user is already logged in, this will do a re-login.
@@ -188,36 +161,20 @@ class Client:
Args: Args:
email: Facebook ``email`` or ``id`` or ``phone number`` email: Facebook ``email`` or ``id`` or ``phone number``
password: Facebook account password password: Facebook account password
max_tries (int): Maximum number of times to try logging in
Raises: Raises:
FBchatException: On failed login FBchatException: On failed login
""" """
self.onLoggingIn(email=email) self.onLoggingIn(email=email)
if max_tries < 1:
raise FBchatUserError("Cannot login: max_tries should be at least one")
if not (email and password): if not (email and password):
raise FBchatUserError("Email and password not set") raise ValueError("Email and password not set")
for i in range(1, max_tries + 1): self._state = _state.State.login(
try: email, password, on_2fa_callback=self.on2FACode
self._state = _state.State.login( )
email, self._uid = self._state.user_id
password, self.onLoggedIn(email=email)
on_2fa_callback=self.on2FACode,
user_agent=user_agent,
)
self._uid = self._state.user_id
except Exception:
if i >= max_tries:
raise
log.exception("Attempt #{} failed, retrying".format(i))
time.sleep(1)
else:
self.onLoggedIn(email=email)
break
def logout(self): def logout(self):
"""Safely log out the client. """Safely log out the client.
@@ -235,45 +192,6 @@ class Client:
END LOGIN METHODS END LOGIN METHODS
""" """
"""
DEFAULT THREAD METHODS
"""
def _getThread(self, given_thread_id=None, given_thread_type=None):
"""Check if thread ID is given and if default is set, and return correct values.
Returns:
tuple: Thread ID and thread type
Raises:
ValueError: If thread ID is not given and there is no default
"""
if given_thread_id is None:
if self._default_thread_id is not None:
return self._default_thread_id, self._default_thread_type
else:
raise ValueError("Thread ID is not set")
else:
return given_thread_id, given_thread_type
def setDefaultThread(self, thread_id, thread_type):
"""Set default thread to send messages to.
Args:
thread_id: User/Group ID to default to. See :ref:`intro_threads`
thread_type (ThreadType): See :ref:`intro_threads`
"""
self._default_thread_id = thread_id
self._default_thread_type = thread_type
def resetDefaultThread(self):
"""Reset default thread."""
self.setDefaultThread(None, None)
"""
END DEFAULT THREAD METHODS
"""
""" """
FETCH METHODS FETCH METHODS
""" """
@@ -494,8 +412,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
"query": query, "query": query,
"snippetOffset": offset, "snippetOffset": offset,
@@ -624,7 +540,7 @@ class Client:
if thread.type == ThreadType.USER: if thread.type == ThreadType.USER:
users[id_] = thread users[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a user".format(thread)) raise ValueError("Thread {} was not a user".format(thread))
return users return users
@@ -649,7 +565,7 @@ class Client:
if thread.type == ThreadType.PAGE: if thread.type == ThreadType.PAGE:
pages[id_] = thread pages[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a page".format(thread)) raise ValueError("Thread {} was not a page".format(thread))
return pages return pages
@@ -671,7 +587,7 @@ class Client:
if thread.type == ThreadType.GROUP: if thread.type == ThreadType.GROUP:
groups[id_] = thread groups[id_] = thread
else: else:
raise FBchatUserError("Thread {} was not a group".format(thread)) raise ValueError("Thread {} was not a group".format(thread))
return groups return groups
@@ -756,8 +672,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
params = { params = {
"id": thread_id, "id": thread_id,
"message_limit": limit, "message_limit": limit,
@@ -789,12 +703,11 @@ class Client:
return messages return messages
def fetchThreadList( def fetchThreadList(
self, offset=None, limit=20, thread_location=ThreadLocation.INBOX, before=None self, limit=20, thread_location=ThreadLocation.INBOX, before=None
): ):
"""Fetch the client's thread list. """Fetch the client's thread list.
Args: Args:
offset: Deprecated. Do not use!
limit (int): Max. number of threads to retrieve. Capped at 20 limit (int): Max. number of threads to retrieve. Capped at 20
thread_location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER thread_location (ThreadLocation): INBOX, PENDING, ARCHIVED or OTHER
before (datetime.datetime): The point from which to retrieve threads before (datetime.datetime): The point from which to retrieve threads
@@ -805,20 +718,13 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
if offset is not None:
log.warning(
"Using `offset` in `fetchThreadList` is no longer supported, "
"since Facebook migrated to the use of GraphQL in this request. "
"Use `before` instead."
)
if limit > 20 or limit < 1: if limit > 20 or limit < 1:
raise FBchatUserError("`limit` should be between 1 and 20") raise ValueError("`limit` should be between 1 and 20")
if thread_location in ThreadLocation: if thread_location in ThreadLocation:
loc_str = thread_location.value loc_str = thread_location.value
else: else:
raise FBchatUserError('"thread_location" must be a value of ThreadLocation') raise TypeError('"thread_location" must be a value of ThreadLocation')
params = { params = {
"limit": limit, "limit": limit,
@@ -891,6 +797,7 @@ class Client:
image_id = str(image_id) image_id = str(image_id)
data = {"photo_id": str(image_id)} data = {"photo_id": str(image_id)}
j = self._post("/mercury/attachments/photo/", data) j = self._post("/mercury/attachments/photo/", data)
_util.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:
@@ -910,7 +817,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
message_info = self._forcedFetch(thread_id, mid).get("message") message_info = self._forcedFetch(thread_id, mid).get("message")
return Message._from_graphql(message_info) return Message._from_graphql(message_info)
@@ -995,7 +901,6 @@ class Client:
Returns: Returns:
typing.Iterable: :class:`ImageAttachment` or :class:`VideoAttachment` typing.Iterable: :class:`ImageAttachment` or :class:`VideoAttachment`
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"id": thread_id, "first": 48} data = {"id": thread_id, "first": 48}
thread_id = str(thread_id) thread_id = str(thread_id)
j, = self.graphql_requests(_graphql.from_query_id("515216185516880", data)) j, = self.graphql_requests(_graphql.from_query_id("515216185516880", data))
@@ -1057,32 +962,11 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
thread = thread_type._to_class()(thread_id) thread = thread_type._to_class()(thread_id)
data = thread._to_send_data() data = thread._to_send_data()
data.update(message._to_send_data()) data.update(message._to_send_data())
return self._doSendRequest(data) return self._doSendRequest(data)
def sendMessage(self, message, thread_id=None, thread_type=ThreadType.USER):
"""Deprecated. Use :func:`fbchat.Client.send` instead."""
return self.send(
Message(text=message), thread_id=thread_id, thread_type=thread_type
)
def sendEmoji(
self,
emoji=None,
size=EmojiSize.SMALL,
thread_id=None,
thread_type=ThreadType.USER,
):
"""Deprecated. Use :func:`fbchat.Client.send` instead."""
return self.send(
Message(text=emoji, emoji_size=size),
thread_id=thread_id,
thread_type=thread_type,
)
def wave(self, wave_first=True, thread_id=None, thread_type=None): def wave(self, wave_first=True, thread_id=None, thread_type=None):
"""Wave hello to a thread. """Wave hello to a thread.
@@ -1097,7 +981,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
thread = thread_type._to_class()(thread_id) thread = thread_type._to_class()(thread_id)
data = thread._to_send_data() data = thread._to_send_data()
data["action_type"] = "ma-type:user-generated-message" data["action_type"] = "ma-type:user-generated-message"
@@ -1131,7 +1014,7 @@ class Client:
) )
elif isinstance(quick_reply, QuickReplyLocation): elif isinstance(quick_reply, QuickReplyLocation):
if not isinstance(payload, LocationAttachment): if not isinstance(payload, LocationAttachment):
raise ValueError( raise TypeError(
"Payload must be an instance of `fbchat.LocationAttachment`" "Payload must be an instance of `fbchat.LocationAttachment`"
) )
return self.sendLocation( return self.sendLocation(
@@ -1162,7 +1045,6 @@ class Client:
def _sendLocation( def _sendLocation(
self, location, current=True, message=None, thread_id=None, thread_type=None self, location, current=True, message=None, thread_id=None, thread_type=None
): ):
thread_id, thread_type = self._getThread(thread_id, thread_type)
thread = thread_type._to_class()(thread_id) thread = thread_type._to_class()(thread_id)
data = thread._to_send_data() data = thread._to_send_data()
if message is not None: if message is not None:
@@ -1231,7 +1113,6 @@ class Client:
`files` should be a list of tuples, with a file's ID and mimetype. `files` should be a list of tuples, with a file's ID and mimetype.
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
thread = thread_type._to_class()(thread_id) thread = thread_type._to_class()(thread_id)
data = thread._to_send_data() data = thread._to_send_data()
data.update(self._oldMessage(message)._to_send_data()) data.update(self._oldMessage(message)._to_send_data())
@@ -1337,48 +1218,6 @@ class Client:
files=files, message=message, thread_id=thread_id, thread_type=thread_type files=files, message=message, thread_id=thread_id, thread_type=thread_type
) )
def sendImage(
self,
image_id,
message=None,
thread_id=None,
thread_type=ThreadType.USER,
is_gif=False,
):
"""Deprecated."""
if is_gif:
mimetype = "image/gif"
else:
mimetype = "image/png"
return self._sendFiles(
files=[(image_id, mimetype)],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
def sendRemoteImage(
self, image_url, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""Deprecated. Use :func:`fbchat.Client.sendRemoteFiles` instead."""
return self.sendRemoteFiles(
file_urls=[image_url],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
def sendLocalImage(
self, image_path, message=None, thread_id=None, thread_type=ThreadType.USER
):
"""Deprecated. Use :func:`fbchat.Client.sendLocalFiles` instead."""
return self.sendLocalFiles(
file_paths=[image_path],
message=message,
thread_id=thread_id,
thread_type=thread_type,
)
def forwardAttachment(self, attachment_id, thread_id=None): def forwardAttachment(self, attachment_id, thread_id=None):
"""Forward an attachment. """Forward an attachment.
@@ -1389,7 +1228,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
"attachment_id": attachment_id, "attachment_id": attachment_id,
"recipient_map[{}]".format(_util.generateOfflineThreadingID()): thread_id, "recipient_map[{}]".format(_util.generateOfflineThreadingID()): thread_id,
@@ -1417,7 +1255,7 @@ class Client:
data = self._oldMessage(message)._to_send_data() data = self._oldMessage(message)._to_send_data()
if len(user_ids) < 2: if len(user_ids) < 2:
raise FBchatUserError("Error when creating group: Not enough participants") raise ValueError("Error when creating group: Not enough participants")
for i, user_id in enumerate(user_ids + [self._uid]): for i, user_id in enumerate(user_ids + [self._uid]):
data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id) data["specific_to_list[{}]".format(i)] = "fbid:{}".format(user_id)
@@ -1439,7 +1277,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = Group(thread_id)._to_send_data() data = Group(thread_id)._to_send_data()
data["action_type"] = "ma-type:log-message" data["action_type"] = "ma-type:log-message"
@@ -1449,7 +1286,7 @@ class Client:
for i, user_id in enumerate(user_ids): for i, user_id in enumerate(user_ids):
if user_id == self._uid: if user_id == self._uid:
raise FBchatUserError( raise ValueError(
"Error when adding users: Cannot add self to group thread" "Error when adding users: Cannot add self to group thread"
) )
else: else:
@@ -1469,14 +1306,10 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"uid": user_id, "tid": thread_id} data = {"uid": user_id, "tid": thread_id}
j = self._payload_post("/chat/remove_participants/", data) j = self._payload_post("/chat/remove_participants/", data)
def _adminStatus(self, admin_ids, admin, thread_id=None): def _adminStatus(self, admin_ids, admin, thread_id=None):
thread_id, thread_type = self._getThread(thread_id, None)
data = {"add": admin, "thread_fbid": thread_id} data = {"add": admin, "thread_fbid": thread_id}
admin_ids = _util.require_list(admin_ids) admin_ids = _util.require_list(admin_ids)
@@ -1520,14 +1353,10 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id} data = {"set_mode": int(require_admin_approval), "thread_fbid": thread_id}
j = self._payload_post("/messaging/set_approval_mode/?dpr=1", data) j = self._payload_post("/messaging/set_approval_mode/?dpr=1", data)
def _usersApproval(self, user_ids, approve, thread_id=None): def _usersApproval(self, user_ids, approve, thread_id=None):
thread_id, thread_type = self._getThread(thread_id, None)
user_ids = _util.require_list(user_ids) user_ids = _util.require_list(user_ids)
data = { data = {
@@ -1576,8 +1405,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"thread_image_id": image_id, "thread_id": thread_id} data = {"thread_image_id": image_id, "thread_id": thread_id}
j = self._payload_post("/messaging/set_thread_image/?dpr=1", data) j = self._payload_post("/messaging/set_thread_image/?dpr=1", data)
@@ -1625,8 +1452,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
if thread_type == ThreadType.USER: if thread_type == ThreadType.USER:
# The thread is a user, so we change the user's nickname # The thread is a user, so we change the user's nickname
return self.changeNickname( return self.changeNickname(
@@ -1650,8 +1475,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = { data = {
"nickname": nickname, "nickname": nickname,
"participant_id": user_id, "participant_id": user_id,
@@ -1671,8 +1494,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
"color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "", "color_choice": color.value if color != ThreadColor.MESSENGER_BLUE else "",
"thread_or_other_fbid": thread_id, "thread_or_other_fbid": thread_id,
@@ -1695,8 +1516,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id} data = {"emoji_choice": emoji, "thread_or_other_fbid": thread_id}
j = self._payload_post( j = self._payload_post(
"/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data "/messaging/save_thread_emoji/?source=thread_settings&dpr=1", data
@@ -1733,8 +1552,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = { data = {
"event_type": "EVENT", "event_type": "EVENT",
"event_time": _util.datetime_to_seconds(plan.time), "event_time": _util.datetime_to_seconds(plan.time),
@@ -1801,11 +1618,6 @@ class Client:
} }
j = self._payload_post("/ajax/eventreminder/rsvp", data) j = self._payload_post("/ajax/eventreminder/rsvp", data)
def eventReminder(self, thread_id, time, title, location="", location_id=""):
"""Deprecated. Use :func:`fbchat.Client.createPlan` instead."""
plan = Plan(time=time, title=title, location=location, location_id=location_id)
self.createPlan(plan=plan, thread_id=thread_id)
def createPoll(self, poll, thread_id=None): def createPoll(self, poll, thread_id=None):
"""Create poll in a group thread. """Create poll in a group thread.
@@ -1816,8 +1628,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
# We're using ordered dictionaries, because the Facebook endpoint that parses # We're using ordered dictionaries, because the Facebook endpoint that parses
# the POST parameters is badly implemented, and deals with ordering the options # the POST parameters is badly implemented, and deals with ordering the options
# wrongly. If you can find a way to fix this for the endpoint, or if you find # wrongly. If you can find a way to fix this for the endpoint, or if you find
@@ -1874,8 +1684,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, thread_type)
data = { data = {
"typ": status.value, "typ": status.value,
"thread": thread_id, "thread": thread_id,
@@ -2087,7 +1895,6 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
thread_id, thread_type = self._getThread(thread_id, None)
j = self._payload_post("/ajax/mercury/mark_spam.php?dpr=1", {"id": thread_id}) j = self._payload_post("/ajax/mercury/mark_spam.php?dpr=1", {"id": thread_id})
return True return True
@@ -2117,7 +1924,6 @@ class Client:
mute_time (datetime.timedelta): Time to mute, use ``None`` to mute forever mute_time (datetime.timedelta): Time to mute, use ``None`` to mute forever
thread_id: User/Group ID to mute. See :ref:`intro_threads` thread_id: User/Group ID to mute. See :ref:`intro_threads`
""" """
thread_id, thread_type = self._getThread(thread_id, None)
if mute_time is None: if mute_time is None:
mute_settings = -1 mute_settings = -1
else: else:
@@ -2140,7 +1946,6 @@ class Client:
mute: Boolean. True to mute, False to unmute mute: Boolean. True to mute, False to unmute
thread_id: User/Group ID to mute. See :ref:`intro_threads` thread_id: User/Group ID to mute. See :ref:`intro_threads`
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id} data = {"reactions_mute_mode": int(mute), "thread_fbid": thread_id}
j = self._payload_post( j = self._payload_post(
"/ajax/mercury/change_reactions_mute_thread/?dpr=1", data "/ajax/mercury/change_reactions_mute_thread/?dpr=1", data
@@ -2161,7 +1966,6 @@ class Client:
mute: Boolean. True to mute, False to unmute mute: Boolean. True to mute, False to unmute
thread_id: User/Group ID to mute. See :ref:`intro_threads` thread_id: User/Group ID to mute. See :ref:`intro_threads`
""" """
thread_id, thread_type = self._getThread(thread_id, None)
data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id} data = {"mentions_mute_mode": int(mute), "thread_fbid": thread_id}
j = self._payload_post("/ajax/mercury/change_mentions_mute_thread/?dpr=1", data) j = self._payload_post("/ajax/mercury/change_mentions_mute_thread/?dpr=1", data)
@@ -2194,6 +1998,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)
def _pullMessage(self): def _pullMessage(self):
"""Call pull api to fetch message data.""" """Call pull api to fetch message data."""
@@ -2205,9 +2010,11 @@ class Client:
"clientid": self._state._client_id, "clientid": self._state._client_id,
"state": "active" if self._markAlive else "offline", "state": "active" if self._markAlive else "offline",
} }
return self._get( j = self._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)
return j
def _parseDelta(self, m): def _parseDelta(self, m):
def getThreadIdAndThreadType(msg_metadata): def getThreadIdAndThreadType(msg_metadata):
@@ -2728,7 +2535,6 @@ class Client:
self.onMessage( self.onMessage(
mid=message.uid, mid=message.uid,
author_id=message.author, author_id=message.author,
message=message.text,
message_object=message, message_object=message,
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
@@ -2743,7 +2549,6 @@ class Client:
self.onMessage( self.onMessage(
mid=mid, mid=mid,
author_id=author_id, author_id=author_id,
message=delta.get("body", ""),
message_object=Message._from_pull( message_object=Message._from_pull(
delta, delta,
mid=mid, mid=mid,
@@ -2869,29 +2674,7 @@ class Client:
except Exception as e: except Exception as e:
self.onMessageError(exception=e, msg=m) self.onMessageError(exception=e, msg=m)
def startListening(self): def _doOneListen(self):
"""Start listening from an external event loop.
Raises:
FBchatException: If request failed
"""
self.listening = True
def doOneListen(self, markAlive=None):
"""Do one cycle of the listening loop.
This method is useful if you want to control the client from an external event
loop.
Warning:
``markAlive`` parameter is deprecated, use :func:`Client.setActiveStatus`
or ``markAlive`` parameter in :func:`Client.listen` instead.
Returns:
bool: Whether the loop should keep running
"""
if markAlive is not None:
self._markAlive = markAlive
try: try:
if self._markAlive: if self._markAlive:
self._ping() self._ping()
@@ -2910,7 +2693,6 @@ class Client:
if e.request_status_code in [502, 503]: if e.request_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
self.startListening()
else: else:
raise e raise e
except Exception as e: except Exception as e:
@@ -2918,11 +2700,6 @@ class Client:
return True return True
def stopListening(self):
"""Clean up the variables from `Client.startListening`."""
self.listening = False
self._sticky, self._pool = (None, None)
def listen(self, markAlive=None): def listen(self, markAlive=None):
"""Initialize and runs the listening loop continually. """Initialize and runs the listening loop continually.
@@ -2932,13 +2709,12 @@ class Client:
if markAlive is not None: if markAlive is not None:
self.setActiveStatus(markAlive) self.setActiveStatus(markAlive)
self.startListening()
self.onListening() self.onListening()
while self.listening and self.doOneListen(): while self._doOneListen():
pass pass
self.stopListening() self._sticky, self._pool = (None, None)
def setActiveStatus(self, markAlive): def setActiveStatus(self, markAlive):
"""Change active status while listening. """Change active status while listening.
@@ -2996,7 +2772,6 @@ class Client:
self, self,
mid=None, mid=None,
author_id=None, author_id=None,
message=None,
message_object=None, message_object=None,
thread_id=None, thread_id=None,
thread_type=ThreadType.USER, thread_type=ThreadType.USER,
@@ -3009,7 +2784,6 @@ class Client:
Args: Args:
mid: The message ID mid: The message ID
author_id: The ID of the author author_id: The ID of the author
message: (deprecated. Use ``message_object.text`` instead)
message_object (Message): The message (As a `Message` object) message_object (Message): The message (As a `Message` object)
thread_id: Thread ID that the message was sent to. See :ref:`intro_threads` thread_id: Thread ID that the message was sent to. See :ref:`intro_threads`
thread_type (ThreadType): Type of thread that the message was sent to. See :ref:`intro_threads` thread_type (ThreadType): Type of thread that the message was sent to. See :ref:`intro_threads`

View File

@@ -50,7 +50,3 @@ class FBchatPleaseRefresh(FBchatFacebookError):
fb_error_code = "1357004" fb_error_code = "1357004"
fb_error_message = "Please try closing and re-opening your browser window." fb_error_message = "Please try closing and re-opening your browser window."
class FBchatUserError(FBchatException):
"""Thrown by ``fbchat`` when wrong values are entered."""

View File

@@ -103,16 +103,3 @@ class Group(Thread):
def _to_send_data(self): def _to_send_data(self):
return {"thread_fbid": self.uid} return {"thread_fbid": self.uid}
@attr.s(cmp=False, init=False)
class Room(Group):
"""Deprecated. Use `Group` instead."""
# True is room is not discoverable
privacy_mode = attr.ib(None)
def __init__(self, uid, privacy_mode=None, **kwargs):
super(Room, self).__init__(uid, **kwargs)
self.type = ThreadType.ROOM
self.privacy_mode = privacy_mode

View File

@@ -23,11 +23,11 @@ def find_input_fields(html):
return bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("input")) return bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("input"))
def session_factory(user_agent=None): def session_factory():
session = requests.session() session = requests.session()
session.headers["Referer"] = "https://www.facebook.com" session.headers["Referer"] = "https://www.facebook.com"
# TODO: Deprecate setting the user agent manually # TODO: Deprecate setting the user agent manually
session.headers["User-Agent"] = user_agent or random.choice(_util.USER_AGENTS) session.headers["User-Agent"] = random.choice(_util.USER_AGENTS)
return session return session
@@ -120,8 +120,8 @@ class State:
} }
@classmethod @classmethod
def login(cls, email, password, on_2fa_callback, user_agent=None): def login(cls, email, password, on_2fa_callback):
session = session_factory(user_agent=user_agent) session = session_factory()
soup = find_input_fields(session.get("https://m.facebook.com/").text) soup = find_input_fields(session.get("https://m.facebook.com/").text)
data = dict( data = dict(
@@ -147,7 +147,7 @@ class State:
if is_home(r.url): if is_home(r.url):
return cls.from_session(session=session) return cls.from_session(session=session)
else: else:
raise _exception.FBchatUserError( raise _exception.FBchatException(
"Login failed. Check email/password. " "Login failed. Check email/password. "
"(Failed on url: {})".format(r.url) "(Failed on url: {})".format(r.url)
) )
@@ -201,63 +201,29 @@ class State:
return self._session.cookies.get_dict() return self._session.cookies.get_dict()
@classmethod @classmethod
def from_cookies(cls, cookies, user_agent=None): def from_cookies(cls, cookies):
session = session_factory(user_agent=user_agent) session = session_factory()
session.cookies = requests.cookies.merge_cookies(session.cookies, cookies) session.cookies = requests.cookies.merge_cookies(session.cookies, cookies)
return cls.from_session(session=session) return cls.from_session(session=session)
def _do_refresh(self):
# TODO: Raise the error instead, and make the user do the refresh manually
# It may be a bad idea to do this in an exception handler, if you have a better method, please suggest it!
log.warning("Refreshing state and resending request")
new = State.from_session(session=self._session)
self.user_id = new.user_id
self._fb_dtsg = new._fb_dtsg
self._revision = new._revision
self._counter = new._counter
self._logout_h = new._logout_h or self._logout_h
def _get(self, url, params, error_retries=3): def _get(self, url, params, error_retries=3):
params.update(self.get_params()) params.update(self.get_params())
r = self._session.get(_util.prefix_url(url), params=params) r = self._session.get(_util.prefix_url(url), params=params)
content = _util.check_request(r) content = _util.check_request(r)
j = _util.to_json(content) return _util.to_json(content)
try:
_util.handle_payload_error(j)
except _exception.FBchatPleaseRefresh:
if error_retries > 0:
self._do_refresh()
return self._get(url, params, error_retries=error_retries - 1)
raise
return j
def _post(self, url, data, files=None, as_graphql=False, error_retries=3): def _post(self, url, data, files=None, as_graphql=False):
data.update(self.get_params()) data.update(self.get_params())
r = self._session.post(_util.prefix_url(url), data=data, files=files) r = self._session.post(_util.prefix_url(url), data=data, files=files)
content = _util.check_request(r) content = _util.check_request(r)
try: if as_graphql:
if as_graphql: return _graphql.response_to_json(content)
return _graphql.response_to_json(content) else:
else: return _util.to_json(content)
j = _util.to_json(content)
# TODO: Remove this, and move it to _payload_post instead
# We can't yet, since errors raised in here need to be caught below
_util.handle_payload_error(j)
return j
except _exception.FBchatPleaseRefresh:
if error_retries > 0:
self._do_refresh()
return self._post(
url,
data,
files=files,
as_graphql=as_graphql,
error_retries=error_retries - 1,
)
raise
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)
try: try:
return j["payload"] return j["payload"]
except (KeyError, TypeError): except (KeyError, TypeError):

View File

@@ -10,7 +10,6 @@ class ThreadType(Enum):
USER = 1 USER = 1
GROUP = 2 GROUP = 2
ROOM = 2
PAGE = 3 PAGE = 3
def _to_class(self): def _to_class(self):
@@ -20,7 +19,6 @@ class ThreadType(Enum):
return { return {
ThreadType.USER: _user.User, ThreadType.USER: _user.User,
ThreadType.GROUP: _group.Group, ThreadType.GROUP: _group.Group,
ThreadType.ROOM: _group.Room,
ThreadType.PAGE: _page.Page, ThreadType.PAGE: _page.Page,
}[self] }[self]

View File

@@ -45,9 +45,11 @@ def client2(pytestconfig):
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def client(client1, thread): def client(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"]) # TODO: These are commented out since `setDefaultThread` is removed - But now any
# test that use this fixture is likely broken!
# client1.setDefaultThread(thread["id"], thread["type"])
yield client1 yield client1
client1.resetDefaultThread() # client1.resetDefaultThread()
@pytest.fixture(scope="session", params=["client1", "client2"]) @pytest.fixture(scope="session", params=["client1", "client2"])

View File

@@ -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 FBchatUserError, Message, Client from fbchat import FBchatException, Message, Client
@pytest.mark.offline @pytest.mark.offline
@@ -24,7 +24,7 @@ def test_login(client1):
assert not client1.isLoggedIn() assert not client1.isLoggedIn()
with pytest.raises(FBchatUserError): with pytest.raises(FBchatException):
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)
@@ -38,13 +38,3 @@ def test_sessions(client1):
Client("no email needed", "no password needed", session_cookies=session) Client("no email needed", "no password needed", session_cookies=session)
client1.setSession(session) client1.setSession(session)
assert client1.isLoggedIn() assert client1.isLoggedIn()
@pytest.mark.tryfirst
def test_default_thread(client1, thread):
client1.setDefaultThread(thread["id"], thread["type"])
assert client1.send(Message(text="Sent to the specified thread"))
client1.resetDefaultThread()
with pytest.raises(ValueError):
client1.send(Message(text="Should not be sent"))

View File

@@ -39,22 +39,20 @@ TEXT_LIST = [
class ClientThread(threading.Thread): class ClientThread(threading.Thread):
# TODO: Refactor this to work with the new listening setup
def __init__(self, client, *args, **kwargs): def __init__(self, client, *args, **kwargs):
self.client = client self.client = client
self.should_stop = threading.Event() self.should_stop = threading.Event()
super(ClientThread, self).__init__(*args, **kwargs) super(ClientThread, self).__init__(*args, **kwargs)
def start(self): def start(self):
self.client.startListening() self.client._doOneListen() # QPrimer, Facebook now knows we're about to start pulling
self.client.doOneListen() # QPrimer, Facebook now knows we're about to start pulling
super(ClientThread, self).start() super(ClientThread, self).start()
def run(self): def run(self):
while not self.should_stop.is_set() and self.client.doOneListen(): while not self.should_stop.is_set() and self.client._doOneListen():
pass pass
self.client.stopListening()
class CaughtValue(threading.Event): class CaughtValue(threading.Event):
def set(self, res): def set(self, res):
@@ -93,7 +91,6 @@ def load_client(n, cache):
client = Client( client = Client(
load_variable("client{}_email".format(n), cache), load_variable("client{}_email".format(n), cache),
load_variable("client{}_password".format(n), cache), load_variable("client{}_password".format(n), cache),
user_agent="Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
session_cookies=cache.get("client{}_session".format(n), None), session_cookies=cache.get("client{}_session".format(n), None),
max_tries=1, max_tries=1,
) )