Merge pull request #497 from carpedm20/public-session

Rename State -> Session, and make the class public.
This commit is contained in:
Mads Marquart
2020-01-09 10:49:43 +01:00
committed by GitHub
9 changed files with 155 additions and 175 deletions

View File

@@ -1,10 +1,19 @@
from fbchat import Client import fbchat
from fbchat.models import *
client = Client("<email>", "<password>") # Log the user in
session = fbchat.Session.login("<email>", "<password>")
print("Own id: {}".format(client.uid)) print("Own id: {}".format(sesion.user_id))
client.send(Message(text="Hi me!"), thread_id=client.uid, thread_type=ThreadType.USER) # Create helper client class
client = fbchat.Client(session)
client.logout() # Send a message to yourself
client.send(
fbchat.Message(text="Hi me!"),
thread_id=session.user_id,
thread_type=fbchat.ThreadType.USER,
)
# Log the user out
session.logout()

View File

@@ -1,7 +1,7 @@
from fbchat import Client import fbchat
# Subclass fbchat.Client and override required methods # Subclass fbchat.Client and override required methods
class EchoBot(Client): class EchoBot(fbchat.Client):
def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs): def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs):
self.mark_as_delivered(thread_id, message_object.uid) self.mark_as_delivered(thread_id, message_object.uid)
self.mark_as_read(thread_id) self.mark_as_read(thread_id)
@@ -13,5 +13,7 @@ class EchoBot(Client):
self.send(message_object, thread_id=thread_id, thread_type=thread_type) self.send(message_object, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
client.listen()
echo_bot = EchoBot(session)
echo_bot.listen()

View File

@@ -1,8 +1,9 @@
from itertools import islice import itertools
from fbchat import Client import fbchat
from fbchat.models import *
client = Client("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
client = fbchat.Client(session)
# Fetches a list of all users you're currently chatting with, as `User` objects # Fetches a list of all users you're currently chatting with, as `User` objects
users = client.fetch_all_users() users = client.fetch_all_users()
@@ -65,5 +66,5 @@ print("thread's type: {}".format(thread.type))
# Print image url for 20 last images from thread. # Print image url for 20 last images from thread.
images = client.fetch_thread_images("<thread id>") images = client.fetch_thread_images("<thread id>")
for image in islice(image, 20): for image in itertools.islice(image, 20):
print(image.large_preview_url) print(image.large_preview_url)

View File

@@ -1,37 +1,43 @@
from fbchat import Client import fbchat
from fbchat.models import *
client = Client("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
client = fbchat.Client(session)
thread_id = "1234567890" thread_id = "1234567890"
thread_type = ThreadType.GROUP thread_type = fbchat.ThreadType.GROUP
# Will send a message to the thread # Will send a message to the thread
client.send(Message(text="<message>"), thread_id=thread_id, thread_type=thread_type) client.send(
fbchat.Message(text="<message>"), thread_id=thread_id, thread_type=thread_type
)
# Will send the default `like` emoji # Will send the default `like` emoji
client.send( client.send(
Message(emoji_size=EmojiSize.LARGE), thread_id=thread_id, thread_type=thread_type fbchat.Message(emoji_size=fbchat.EmojiSize.LARGE),
thread_id=thread_id,
thread_type=thread_type,
) )
# Will send the emoji `👍` # Will send the emoji `👍`
client.send( client.send(
Message(text="👍", emoji_size=EmojiSize.LARGE), fbchat.Message(text="👍", emoji_size=fbchat.EmojiSize.LARGE),
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
) )
# Will send the sticker with ID `767334476626295` # Will send the sticker with ID `767334476626295`
client.send( client.send(
Message(sticker=Sticker("767334476626295")), fbchat.Message(sticker=fbchat.Sticker("767334476626295")),
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
) )
# Will send a message with a mention # Will send a message with a mention
client.send( client.send(
Message( fbchat.Message(
text="This is a @mention", mentions=[Mention(thread_id, offset=10, length=8)] text="This is a @mention",
mentions=[fbchat.Mention(thread_id, offset=10, length=8)],
), ),
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
@@ -40,7 +46,7 @@ client.send(
# Will send the image located at `<image path>` # Will send the image located at `<image path>`
client.send_local_image( client.send_local_image(
"<image path>", "<image path>",
message=Message(text="This is a local image"), message=fbchat.Message(text="This is a local image"),
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
) )
@@ -48,14 +54,14 @@ client.send_local_image(
# Will download the image at the URL `<image url>`, and then send it # Will download the image at the URL `<image url>`, and then send it
client.send_remote_image( client.send_remote_image(
"<image url>", "<image url>",
message=Message(text="This is a remote image"), message=fbchat.Message(text="This is a remote image"),
thread_id=thread_id, thread_id=thread_id,
thread_type=thread_type, thread_type=thread_type,
) )
# Only do these actions if the thread is a group # Only do these actions if the thread is a group
if thread_type == ThreadType.GROUP: if thread_type == fbchat.ThreadType.GROUP:
# Will remove the user with ID `<user id>` from the thread # Will remove the user with ID `<user id>` from the thread
client.remove_user_from_group("<user id>", thread_id=thread_id) client.remove_user_from_group("<user id>", thread_id=thread_id)
@@ -78,14 +84,14 @@ client.change_thread_title("<title>", thread_id=thread_id, thread_type=thread_ty
# Will set the typing status of the thread to `TYPING` # Will set the typing status of the thread to `TYPING`
client.set_typing_status( client.set_typing_status(
TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type fbchat.TypingStatus.TYPING, thread_id=thread_id, thread_type=thread_type
) )
# Will change the thread color to `MESSENGER_BLUE` # Will change the thread color to `MESSENGER_BLUE`
client.change_thread_color(ThreadColor.MESSENGER_BLUE, thread_id=thread_id) client.change_thread_color(fbchat.ThreadColor.MESSENGER_BLUE, thread_id=thread_id)
# Will change the thread emoji to `👍` # Will change the thread emoji to `👍`
client.change_thread_emoji("👍", thread_id=thread_id) client.change_thread_emoji("👍", thread_id=thread_id)
# Will react to a message with a 😍 emoji # Will react to a message with a 😍 emoji
client.react_to_message("<message id>", MessageReaction.LOVE) client.react_to_message("<message id>", fbchat.MessageReaction.LOVE)

View File

@@ -1,11 +1,10 @@
from fbchat import Client import fbchat
from fbchat.models import *
# Change this to your group id # Change this to your group id
old_thread_id = "1234567890" old_thread_id = "1234567890"
# Change these to match your liking # Change these to match your liking
old_color = ThreadColor.MESSENGER_BLUE old_color = fbchat.ThreadColor.MESSENGER_BLUE
old_emoji = "👍" old_emoji = "👍"
old_title = "Old group chat name" old_title = "Old group chat name"
old_nicknames = { old_nicknames = {
@@ -16,7 +15,7 @@ old_nicknames = {
} }
class KeepBot(Client): class KeepBot(fbchat.Client):
def on_color_change(self, author_id, new_color, thread_id, thread_type, **kwargs): def on_color_change(self, author_id, new_color, thread_id, thread_type, **kwargs):
if old_thread_id == thread_id and old_color != new_color: if old_thread_id == thread_id and old_color != new_color:
print( print(
@@ -77,5 +76,7 @@ class KeepBot(Client):
) )
client = KeepBot("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
client.listen()
keep_bot = KeepBot(session)
keep_bot.listen()

View File

@@ -1,11 +1,13 @@
from fbchat import Client import fbchat
from fbchat.models import *
class RemoveBot(Client): class RemoveBot(fbchat.Client):
def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs): def on_message(self, author_id, message_object, thread_id, thread_type, **kwargs):
# We can only kick people from group chats, so no need to try if it's a user chat # We can only kick people from group chats, so no need to try if it's a user chat
if message_object.text == "Remove me!" and thread_type == ThreadType.GROUP: if (
message_object.text == "Remove me!"
and thread_type == fbchat.ThreadType.GROUP
):
print("{} will be removed from {}".format(author_id, thread_id)) print("{} will be removed from {}".format(author_id, thread_id))
self.remove_user_from_group(author_id, thread_id=thread_id) self.remove_user_from_group(author_id, thread_id=thread_id)
else: else:
@@ -19,5 +21,7 @@ class RemoveBot(Client):
) )
client = RemoveBot("<email>", "<password>") session = fbchat.Session.login("<email>", "<password>")
client.listen()
remove_bot = RemoveBot(session)
remove_bot.listen()

View File

@@ -13,6 +13,7 @@ _logging.getLogger(__name__).addHandler(_logging.NullHandler())
from . import _core, _util from . import _core, _util
from ._core import Image from ._core import Image
from ._exception import FBchatException, FBchatFacebookError from ._exception import FBchatException, FBchatFacebookError
from ._session import Session
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 from ._group import Group
@@ -44,4 +45,4 @@ __license__ = "BSD 3-Clause"
__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart" __author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
__email__ = "carpedm20@gmail.com" __email__ = "carpedm20@gmail.com"
__all__ = ("Client",) __all__ = ("Session", "Client")

View File

@@ -5,7 +5,7 @@ import requests
from collections import OrderedDict from collections import OrderedDict
from ._core import log from ._core import log
from . import _util, _graphql, _state from . import _util, _graphql, _session
from ._exception import FBchatException, FBchatFacebookError from ._exception import FBchatException, FBchatFacebookError
from ._thread import ThreadType, ThreadLocation, ThreadColor from ._thread import ThreadType, ThreadLocation, ThreadColor
@@ -38,9 +38,9 @@ ACONTEXT = {
class Client: class Client:
"""A client for the Facebook Chat (Messenger). """A client for the Facebook Chat (Messenger).
This is the main class, which contains all the methods you use to interact with This contains all the methods you use to interact with Facebook. You can extend this
Facebook. You can extend this class, and overwrite the ``on`` methods, to provide class, and overwrite the ``on`` methods, to provide custom event handling (mainly
custom event handling (mainly useful while listening). useful while listening).
""" """
@property @property
@@ -51,7 +51,7 @@ class Client:
""" """
return self._uid return self._uid
def __init__(self, email, password, session_cookies=None): def __init__(self, session):
"""Initialize and log in the client. """Initialize and log in the client.
Args: Args:
@@ -67,27 +67,24 @@ class Client:
self._pull_channel = 0 self._pull_channel = 0
self._mark_alive = True self._mark_alive = True
self._buddylist = dict() self._buddylist = dict()
self._session = session
self._uid = session.user_id
# If session cookies aren't set, not properly loaded or gives us an invalid session, then do the login def __repr__(self):
if ( return "Client(session={!r})".format(self._session)
not session_cookies
or not self.set_session(session_cookies)
or not self.is_logged_in()
):
self.login(email, password)
""" """
INTERNAL REQUEST METHODS INTERNAL REQUEST METHODS
""" """
def _get(self, url, params): def _get(self, url, params):
return self._state._get(url, params) return self._session._get(url, params)
def _post(self, url, params, files=None): def _post(self, url, params, files=None):
return self._state._post(url, params, files=files) return self._session._post(url, params, files=files)
def _payload_post(self, url, data, files=None): def _payload_post(self, url, data, files=None):
return self._state._payload_post(url, data, files=files) return self._session._payload_post(url, data, files=files)
def graphql_requests(self, *queries): def graphql_requests(self, *queries):
"""Execute GraphQL queries. """Execute GraphQL queries.
@@ -101,7 +98,7 @@ class Client:
Raises: Raises:
FBchatException: If request failed FBchatException: If request failed
""" """
return tuple(self._state._graphql_requests(*queries)) return tuple(self._session._graphql_requests(*queries))
def graphql_request(self, query): def graphql_request(self, query):
"""Shorthand for ``graphql_requests(query)[0]``. """Shorthand for ``graphql_requests(query)[0]``.
@@ -115,83 +112,6 @@ class Client:
END INTERNAL REQUEST METHODS END INTERNAL REQUEST METHODS
""" """
"""
LOGIN METHODS
"""
def is_logged_in(self):
"""Send a request to Facebook to check the login status.
Returns:
bool: True if the client is still logged in
"""
return self._state.is_logged_in()
def get_session(self):
"""Retrieve session cookies.
Returns:
dict: A dictionary containing session cookies
"""
return self._state.get_cookies()
def set_session(self, session_cookies):
"""Load session cookies.
Args:
session_cookies (dict): A dictionary containing session cookies
Returns:
bool: False if ``session_cookies`` does not contain proper cookies
"""
try:
# Load cookies into current session
self._state = _state.State.from_cookies(session_cookies)
self._uid = self._state.user_id
except Exception as e:
log.exception("Failed loading session")
return False
return True
def login(self, email, password):
"""Login the user, using ``email`` and ``password``.
If the user is already logged in, this will do a re-login.
Args:
email: Facebook ``email`` or ``id`` or ``phone number``
password: Facebook account password
Raises:
FBchatException: On failed login
"""
self.on_logging_in(email=email)
if not (email and password):
raise ValueError("Email and password not set")
self._state = _state.State.login(
email, password, on_2fa_callback=self.on_2fa_code
)
self._uid = self._state.user_id
self.on_logged_in(email=email)
def logout(self):
"""Safely log out the client.
Returns:
bool: True if the action was successful
"""
if self._state.logout():
self._state = None
self._uid = None
return True
return False
"""
END LOGIN METHODS
"""
""" """
FETCH METHODS FETCH METHODS
""" """
@@ -936,7 +856,7 @@ class Client:
def _do_send_request(self, data, get_thread_id=False): def _do_send_request(self, data, get_thread_id=False):
"""Send the data to `SendURL`, and returns the message ID or None on failure.""" """Send the data to `SendURL`, and returns the message ID or None on failure."""
mid, thread_id = self._state._do_send_request(data) mid, thread_id = self._session._do_send_request(data)
if get_thread_id: if get_thread_id:
return mid, thread_id return mid, thread_id
else: else:
@@ -1107,7 +1027,7 @@ class Client:
) )
def _upload(self, files, voice_clip=False): def _upload(self, files, voice_clip=False):
return self._state._upload(files, voice_clip=voice_clip) return self._session._upload(files, voice_clip=voice_clip)
def _send_files( def _send_files(
self, files, message=None, thread_id=None, thread_type=ThreadType.USER self, files, message=None, thread_id=None, thread_type=ThreadType.USER
@@ -1997,7 +1917,7 @@ class Client:
data = { data = {
"seq": self._seq, "seq": self._seq,
"channel": "p_" + self._uid, "channel": "p_" + self._uid,
"clientid": self._state._client_id, "clientid": self._session._client_id,
"partition": -2, "partition": -2,
"cap": 0, "cap": 0,
"uid": self._uid, "uid": self._uid,
@@ -2019,7 +1939,7 @@ class Client:
"msgs_recv": 0, "msgs_recv": 0,
"sticky_token": self._sticky, "sticky_token": self._sticky,
"sticky_pool": self._pool, "sticky_pool": self._pool,
"clientid": self._state._client_id, "clientid": self._session._client_id,
"state": "active" if self._mark_alive else "offline", "state": "active" if self._mark_alive else "offline",
} }
j = self._get( j = self._get(
@@ -2743,26 +2663,6 @@ class Client:
EVENTS EVENTS
""" """
def on_logging_in(self, email=None):
"""Called when the client is logging in.
Args:
email: The email of the client
"""
log.info("Logging in {}...".format(email))
def on_2fa_code(self):
"""Called when a 2FA code is needed to progress."""
return input("Please enter your 2FA code --> ")
def on_logged_in(self, email=None):
"""Called when the client is successfully logged in.
Args:
email: The email of the client
"""
log.info("Login of {} successful.".format(email))
def on_listening(self): def on_listening(self):
"""Called when the client is listening.""" """Called when the client is listening."""
log.info("Listening...") log.info("Listening...")

View File

@@ -5,7 +5,7 @@ import requests
import random import random
import urllib.parse import urllib.parse
from ._core import log, attrs_default from ._core import log, kw_only
from . import _graphql, _util, _exception from . import _graphql, _util, _exception
FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"') FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"')
@@ -98,11 +98,14 @@ def _2fa_helper(session, code, r):
return r return r
@attrs_default @attr.s(slots=True, kw_only=kw_only, repr=False)
class State: class Session:
"""Stores and manages state required for most Facebook requests.""" """Stores and manages state required for most Facebook requests.
user_id = attr.ib() This is the main class, which is used to login to Facebook.
"""
_user_id = attr.ib()
_fb_dtsg = attr.ib() _fb_dtsg = attr.ib()
_revision = attr.ib() _revision = attr.ib()
_session = attr.ib(factory=session_factory) _session = attr.ib(factory=session_factory)
@@ -110,7 +113,16 @@ class State:
_client_id = attr.ib(factory=client_id_factory) _client_id = attr.ib(factory=client_id_factory)
_logout_h = attr.ib(None) _logout_h = attr.ib(None)
def get_params(self): @property
def user_id(self):
"""The logged in user's ID."""
return self._user_id
def __repr__(self):
# An alternative repr, to illustrate that you can't create the class directly
return "<fbchat.Session user_id={}>".format(self._user_id)
def _get_params(self):
self._counter += 1 # TODO: Make this operation atomic / thread-safe self._counter += 1 # TODO: Make this operation atomic / thread-safe
return { return {
"__a": 1, "__a": 1,
@@ -120,7 +132,18 @@ class State:
} }
@classmethod @classmethod
def login(cls, email, password, on_2fa_callback): def login(cls, email, password, on_2fa_callback=None):
"""Login the user, using ``email`` and ``password``.
Args:
email: Facebook ``email`` or ``id`` or ``phone number``
password: Facebook account password
on_2fa_callback: Function that will be called, in case a 2FA code is needed.
This should return the requested 2FA code.
Raises:
FBchatException: On failed login
"""
session = session_factory() 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)
@@ -137,6 +160,10 @@ class State:
# 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:
raise _exception.FBchatException(
"2FA code required, please add `on_2fa_callback` to .login"
)
code = on_2fa_callback() code = on_2fa_callback()
r = _2fa_helper(session, code, r) r = _2fa_helper(session, code, r)
@@ -145,7 +172,7 @@ class State:
r = session.get("https://m.facebook.com/login/save-device/cancel/") r = session.get("https://m.facebook.com/login/save-device/cancel/")
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.FBchatException( raise _exception.FBchatException(
"Login failed. Check email/password. " "Login failed. Check email/password. "
@@ -153,12 +180,24 @@ class State:
) )
def is_logged_in(self): def is_logged_in(self):
"""Send a request to Facebook to check the login status.
Returns:
bool: Whether the user is still logged in
"""
# 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"
r = self._session.get(url, allow_redirects=False) r = self._session.get(url, allow_redirects=False)
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.
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/")
@@ -166,10 +205,14 @@ class State:
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")
return self._session.get(url, params={"ref": "mb", "h": logout_h}).ok r = self._session.get(url, params={"ref": "mb", "h": logout_h})
if not r.ok:
raise exception.FBchatException(
"Failed logging out: {}".format(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)
@@ -198,22 +241,35 @@ class State:
) )
def get_cookies(self): def get_cookies(self):
"""Retrieve session cookies, that can later be used in `from_cookies`.
Returns:
dict: A dictionary containing session cookies
"""
return self._session.cookies.get_dict() return self._session.cookies.get_dict()
@classmethod @classmethod
def from_cookies(cls, cookies): def from_cookies(cls, cookies):
"""Load a session from session cookies.
Args:
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)
return cls.from_session(session=session) return cls._from_session(session=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())
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)
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())
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)
if as_graphql: if as_graphql:
@@ -266,7 +322,7 @@ class State:
def _do_send_request(self, data): def _do_send_request(self, data):
offline_threading_id = _util.generate_offline_threading_id() offline_threading_id = _util.generate_offline_threading_id()
data["client"] = "mercury" data["client"] = "mercury"
data["author"] = "fbid:{}".format(self.user_id) data["author"] = "fbid:{}".format(self._user_id)
data["timestamp"] = _util.now() data["timestamp"] = _util.now()
data["source"] = "source:chat:web" data["source"] = "source:chat:web"
data["offline_threading_id"] = offline_threading_id data["offline_threading_id"] = offline_threading_id