Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
43aa16c32d | ||
|
427ae6bc5e | ||
|
d650946531 | ||
|
8ac6dc4ae6 | ||
|
a6cf1d5c89 | ||
|
65b42e6532 | ||
|
8824a1c253 |
@@ -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")
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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):
|
||||||
|
@@ -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():
|
||||||
|
Reference in New Issue
Block a user