Compare commits

...

7 Commits

Author SHA1 Message Date
Mads Marquart
43aa16c32d Remove stupid, obviously flaky test 2020-06-14 23:18:07 +02:00
Mads Marquart
427ae6bc5e Bump version: 2.0.0a4 -> 2.0.0a5 2020-06-14 23:15:50 +02:00
Mads Marquart
d650946531 Add act cookie on login 2020-06-14 23:00:40 +02:00
Mads Marquart
8ac6dc4ae6 Update SERVER_JS_DEFINE_REGEX, so logging in on newer FB versions work 2020-06-14 22:27:26 +02:00
Mads Marquart
a6cf1d5c89 Add _util.now, fixing a few places where datetimes were incorrectly used 2020-06-14 22:26:52 +02:00
Mads Marquart
65b42e6532 Add example of replying to a message 2020-06-07 14:41:05 +02:00
Mads Marquart
8824a1c253 Set override Facebook's auto-locale detection during login
The locale is only used during error handling, and makes it harder for
users to report errors
2020-06-07 13:59:27 +02:00
7 changed files with 59 additions and 20 deletions

View File

@@ -118,7 +118,7 @@ from ._listen import Listener
from ._client import Client from ._client import Client
__version__ = "2.0.0a4" __version__ = "2.0.0a5"
__all__ = ("Session", "Listener", "Client") __all__ = ("Session", "Listener", "Client")

View File

@@ -419,7 +419,7 @@ class Client:
Warning: Warning:
This is not finished, and the API may change at any point! This is not finished, and the API may change at any point!
""" """
at = datetime.datetime.utcnow() at = _util.now()
form = { form = {
"folders[0]": "inbox", "folders[0]": "inbox",
"client": "mercury", "client": "mercury",

View File

@@ -16,7 +16,7 @@ from typing import Optional, Mapping, Callable, Any
SERVER_JS_DEFINE_REGEX = re.compile( SERVER_JS_DEFINE_REGEX = re.compile(
r'require(?:\("ServerJS"\).{,100}\.handle\({.*"define":)|(?:\("ServerJSDefine"\)\)?\.handleDefines\()' r'(?:"ServerJS".{,100}\.handle\({.*"define":)|(?:require\("ServerJSDefine"\)\)?\.handleDefines\()'
) )
SERVER_JS_DEFINE_JSON_DECODER = json.JSONDecoder() SERVER_JS_DEFINE_JSON_DECODER = json.JSONDecoder()
@@ -93,6 +93,12 @@ def session_factory() -> requests.Session:
from . import __version__ from . import __version__
session = requests.session() session = requests.session()
# Override Facebook's locale detection during the login process.
# The locale is only used when giving errors back to the user, so giving the errors
# back in English makes it easier for users to report.
session.cookies = session.cookies = requests.cookies.merge_cookies(
session.cookies, {"locale": "en_US"}
)
session.headers["Referer"] = "https://www.messenger.com/" session.headers["Referer"] = "https://www.messenger.com/"
# We won't try to set a fake user agent to mask our presence! # We won't try to set a fake user agent to mask our presence!
# Facebook allows us access anyhow, and it makes our motives clearer: # Facebook allows us access anyhow, and it makes our motives clearer:
@@ -101,6 +107,10 @@ def session_factory() -> requests.Session:
return session return session
def login_cookies(at: datetime.datetime):
return {"act": "{}/0".format(_util.datetime_to_millis(at))}
def client_id_factory() -> str: def client_id_factory() -> str:
return hex(int(random.random() * 2 ** 31))[2:] return hex(int(random.random() * 2 ** 31))[2:]
@@ -137,7 +147,9 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
while "approvals_code" in data: while "approvals_code" in data:
data["approvals_code"] = on_2fa_callback() data["approvals_code"] = on_2fa_callback()
log.info("Submitting 2FA code") log.info("Submitting 2FA code")
r = session.post(url, data=data, allow_redirects=False) r = session.post(
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
)
log.debug("2FA location: %s", r.headers.get("Location")) log.debug("2FA location: %s", r.headers.get("Location"))
url, data = find_form_request(r.content.decode("utf-8")) url, data = find_form_request(r.content.decode("utf-8"))
@@ -145,7 +157,9 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
if "name_action_selected" in data: if "name_action_selected" in data:
data["name_action_selected"] = "save_device" data["name_action_selected"] = "save_device"
log.info("Saving browser") log.info("Saving browser")
r = session.post(url, data=data, allow_redirects=False) r = session.post(
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
)
log.debug("2FA location: %s", r.headers.get("Location")) log.debug("2FA location: %s", r.headers.get("Location"))
url = r.headers.get("Location") url = r.headers.get("Location")
if url and url.startswith("https://www.messenger.com/login/auth_token/"): if url and url.startswith("https://www.messenger.com/login/auth_token/"):
@@ -153,7 +167,9 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
url, data = find_form_request(r.content.decode("utf-8")) url, data = find_form_request(r.content.decode("utf-8"))
log.info("Starting Facebook checkup flow") log.info("Starting Facebook checkup flow")
r = session.post(url, data=data, allow_redirects=False) r = session.post(
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
)
log.debug("2FA location: %s", r.headers.get("Location")) log.debug("2FA location: %s", r.headers.get("Location"))
url, data = find_form_request(r.content.decode("utf-8")) url, data = find_form_request(r.content.decode("utf-8"))
@@ -166,7 +182,9 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
data["submit[This was me]"] = "[any value]" data["submit[This was me]"] = "[any value]"
del data["submit[This wasn't me]"] del data["submit[This wasn't me]"]
log.info("Verifying login attempt") log.info("Verifying login attempt")
r = session.post(url, data=data, allow_redirects=False) r = session.post(
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
)
log.debug("2FA location: %s", r.headers.get("Location")) log.debug("2FA location: %s", r.headers.get("Location"))
url, data = find_form_request(r.content.decode("utf-8")) url, data = find_form_request(r.content.decode("utf-8"))
@@ -174,7 +192,9 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
raise _exception.ParseError("Could not fill out form properly (3)", data=data) raise _exception.ParseError("Could not fill out form properly (3)", data=data)
data["name_action_selected"] = "save_device" data["name_action_selected"] = "save_device"
log.info("Saving device again") log.info("Saving device again")
r = session.post(url, data=data, allow_redirects=False) r = session.post(
url, data=data, allow_redirects=False, cookies=login_cookies(_util.now())
)
log.debug("2FA location: %s", r.headers.get("Location")) log.debug("2FA location: %s", r.headers.get("Location"))
return r.headers.get("Location") return r.headers.get("Location")
@@ -185,7 +205,6 @@ def get_error_data(html: str) -> Optional[str]:
html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form") html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form")
) )
# Attempt to extract and format the error string # Attempt to extract and format the error string
# The error message is in the user's own language!
return " ".join(list(soup.stripped_strings)[1:3]) or None return " ".join(list(soup.stripped_strings)[1:3]) or None
@@ -291,6 +310,7 @@ class Session:
"https://www.messenger.com/login/password/", "https://www.messenger.com/login/password/",
data=data, data=data,
allow_redirects=False, allow_redirects=False,
cookies=login_cookies(_util.now()),
) )
except requests.RequestException as e: except requests.RequestException as e:
_exception.handle_requests_error(e) _exception.handle_requests_error(e)
@@ -314,18 +334,24 @@ class Session:
if not url.startswith("https://www.facebook.com/checkpoint/start/"): if not url.startswith("https://www.facebook.com/checkpoint/start/"):
raise _exception.ParseError("Failed 2fa flow (1)", data=url) raise _exception.ParseError("Failed 2fa flow (1)", data=url)
r = session.get(url, allow_redirects=False) r = session.get(
url, allow_redirects=False, cookies=login_cookies(_util.now())
)
url = r.headers.get("Location") url = r.headers.get("Location")
if not url or not url.startswith("https://www.facebook.com/checkpoint/"): if not url or not url.startswith("https://www.facebook.com/checkpoint/"):
raise _exception.ParseError("Failed 2fa flow (2)", data=url) raise _exception.ParseError("Failed 2fa flow (2)", data=url)
r = session.get(url, allow_redirects=False) r = session.get(
url, allow_redirects=False, cookies=login_cookies(_util.now())
)
url = two_factor_helper(session, r, on_2fa_callback) url = two_factor_helper(session, r, on_2fa_callback)
if not url.startswith("https://www.messenger.com/login/auth_token/"): if not url.startswith("https://www.messenger.com/login/auth_token/"):
raise _exception.ParseError("Failed 2fa flow (3)", data=url) raise _exception.ParseError("Failed 2fa flow (3)", data=url)
r = session.get(url, allow_redirects=False) r = session.get(
url, allow_redirects=False, cookies=login_cookies(_util.now())
)
url = r.headers.get("Location") url = r.headers.get("Location")
if url != "https://www.messenger.com/": if url != "https://www.messenger.com/":
@@ -481,7 +507,7 @@ class Session:
return self._post("/api/graphqlbatch/", data, as_graphql=True) return self._post("/api/graphqlbatch/", data, as_graphql=True)
def _do_send_request(self, data): def _do_send_request(self, data):
now = datetime.datetime.utcnow() now = _util.now()
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)

View File

@@ -116,8 +116,14 @@ class ThreadABC(metaclass=abc.ABCMeta):
reply_to_id: Optional message to reply to reply_to_id: Optional message to reply to
Example: Example:
Send a message with a mention to a thread.
>>> mention = fbchat.Mention(thread_id="1234", offset=5, length=2) >>> mention = fbchat.Mention(thread_id="1234", offset=5, length=2)
>>> thread.send_text("A message", mentions=[mention]) >>> message_id = thread.send_text("A message", mentions=[mention])
Reply to the message.
>>> thread.send_text("A reply", reply_to_id=message_id)
Returns: Returns:
The sent message The sent message

View File

@@ -56,7 +56,7 @@ def parse_json(text: str) -> Any:
def generate_offline_threading_id(): def generate_offline_threading_id():
ret = datetime_to_millis(datetime.datetime.utcnow()) ret = datetime_to_millis(now())
value = int(random.random() * 4294967295) value = int(random.random() * 4294967295)
string = ("0000000000000000000000" + format(value, "b"))[-22:] string = ("0000000000000000000000" + format(value, "b"))[-22:]
msgs = format(ret, "b") + string msgs = format(ret, "b") + string
@@ -158,3 +158,11 @@ def timedelta_to_seconds(td: datetime.timedelta) -> int:
The returned seconds will be rounded to the nearest whole number. The returned seconds will be rounded to the nearest whole number.
""" """
return round(td.total_seconds()) return round(td.total_seconds())
def now() -> datetime.datetime:
"""The current time.
Similar to datetime.datetime.now(), but returns a non-naive datetime.
"""
return datetime.datetime.now(tz=datetime.timezone.utc)

View File

@@ -1,6 +1,5 @@
import pytest import pytest
import fbchat import fbchat
import datetime
import os import os
pytestmark = pytest.mark.online pytestmark = pytest.mark.online
@@ -95,11 +94,11 @@ def test_upload_many(client, open_resource):
def test_mark_as_read(client, user, group): def test_mark_as_read(client, user, group):
client.mark_as_read([user, group], datetime.datetime.now()) client.mark_as_read([user, group], fbchat._util.now())
def test_mark_as_unread(client, user, group): def test_mark_as_unread(client, user, group):
client.mark_as_unread([user, group], datetime.datetime.now()) client.mark_as_unread([user, group], fbchat._util.now())
def test_move_threads(client, user, group): def test_move_threads(client, user, group):

View File

@@ -1,6 +1,6 @@
import datetime import datetime
import pytest import pytest
from fbchat import ParseError from fbchat import ParseError, _util
from fbchat._session import ( from fbchat._session import (
parse_server_js_define, parse_server_js_define,
base36encode, base36encode,
@@ -73,7 +73,7 @@ def test_prefix_url():
def test_generate_message_id(): def test_generate_message_id():
# Returns random output, so hard to test more thoroughly # Returns random output, so hard to test more thoroughly
assert generate_message_id(datetime.datetime.utcnow(), "def") assert generate_message_id(_util.now(), "def")
def test_session_factory(): def test_session_factory():