mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
feat: enhance cookie handling by introducing CookieResult and updating get_cookies_from_browser to return structured results
This commit is contained in:
@@ -29,7 +29,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
COOKIES_JSON: ${{ secrets.COOKIES_JSON }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -13,6 +13,7 @@ from pyfragment.types import (
|
||||
ClientError,
|
||||
ConfigurationError,
|
||||
CookieError,
|
||||
CookieResult,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
FragmentPageError,
|
||||
@@ -55,6 +56,7 @@ __all__ = [
|
||||
"ClientError",
|
||||
"ConfigurationError",
|
||||
"CookieError",
|
||||
"CookieResult",
|
||||
"FragmentAPIError",
|
||||
"FragmentError",
|
||||
"FragmentPageError",
|
||||
|
||||
@@ -21,7 +21,9 @@ from pyfragment.types import (
|
||||
GiftsResult,
|
||||
LoginCodeResult,
|
||||
NumbersResult,
|
||||
PremiumGiveawayResult,
|
||||
PremiumResult,
|
||||
StarsGiveawayResult,
|
||||
StarsResult,
|
||||
TerminateSessionsResult,
|
||||
UsernamesResult,
|
||||
@@ -34,7 +36,6 @@ from pyfragment.types.constants import (
|
||||
SUPPORTED_WALLET_VERSIONS,
|
||||
WalletVersion,
|
||||
)
|
||||
from pyfragment.types.results import PremiumGiveawayResult, StarsGiveawayResult
|
||||
from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers
|
||||
from pyfragment.utils.wallet import get_wallet_info
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import html
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyfragment.types import AnonymousNumberError, FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types import (
|
||||
AnonymousNumberError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
LoginCodeResult,
|
||||
TerminateSessionsResult,
|
||||
UnexpectedError,
|
||||
)
|
||||
from pyfragment.types.constants import NUMBERS_PAGE
|
||||
from pyfragment.types.results import LoginCodeResult, TerminateSessionsResult
|
||||
from pyfragment.utils import parse_login_code
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -5,12 +5,12 @@ from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
PremiumGiveawayResult,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE
|
||||
from pyfragment.types.results import PremiumGiveawayResult
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -5,12 +5,12 @@ from pyfragment.types import (
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
StarsGiveawayResult,
|
||||
UnexpectedError,
|
||||
UserNotFoundError,
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE
|
||||
from pyfragment.types.results import StarsGiveawayResult
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pyfragment.types import (
|
||||
AdsRechargeResult,
|
||||
ConfigurationError,
|
||||
FragmentAPIError,
|
||||
FragmentError,
|
||||
@@ -9,7 +10,6 @@ from pyfragment.types import (
|
||||
VerificationError,
|
||||
)
|
||||
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
|
||||
from pyfragment.types.results import AdsRechargeResult
|
||||
from pyfragment.utils import get_account_info, process_transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, GiftsResult, UnexpectedError
|
||||
from pyfragment.types.constants import GIFTS_PAGE
|
||||
from pyfragment.types.results import GiftsResult
|
||||
from pyfragment.utils import parse_gift_items
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, NumbersResult, UnexpectedError
|
||||
from pyfragment.types.constants import NUMBERS_PAGE
|
||||
from pyfragment.types.results import NumbersResult
|
||||
from pyfragment.utils import parse_auction_rows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
|
||||
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError, UsernamesResult
|
||||
from pyfragment.types.constants import FRAGMENT_BASE_URL
|
||||
from pyfragment.types.results import UsernamesResult
|
||||
from pyfragment.utils import parse_auction_rows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -17,6 +17,7 @@ from pyfragment.types.exceptions import (
|
||||
from pyfragment.types.results import (
|
||||
AdsRechargeResult,
|
||||
AdsTopupResult,
|
||||
CookieResult,
|
||||
GiftsResult,
|
||||
LoginCodeResult,
|
||||
NumbersResult,
|
||||
@@ -49,6 +50,7 @@ __all__ = [
|
||||
# result types
|
||||
"AdsRechargeResult",
|
||||
"AdsTopupResult",
|
||||
"CookieResult",
|
||||
"GiftsResult",
|
||||
"LoginCodeResult",
|
||||
"NumbersResult",
|
||||
|
||||
@@ -54,7 +54,7 @@ DEVICE: str = json.dumps(
|
||||
{
|
||||
"platform": "iphone",
|
||||
"appName": "Tonkeeper",
|
||||
"appVersion": "5.5.2",
|
||||
"appVersion": "26.04.0",
|
||||
"maxProtocolVersion": 2,
|
||||
"features": [
|
||||
"SendTransaction",
|
||||
|
||||
@@ -2,6 +2,23 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class CookieResult:
|
||||
"""Result returned by :func:`~pyfragment.utils.get_cookies_from_browser`.
|
||||
|
||||
Attributes:
|
||||
cookies: Dict with the four required Fragment cookie keys.
|
||||
expires: Expiry of the ``stel_ssid`` session cookie in ISO 8601 format (UTC),
|
||||
or ``None`` for session cookies.
|
||||
"""
|
||||
|
||||
cookies: dict[str, str]
|
||||
expires: str | None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CookieResult(expires={self.expires!r})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class WalletInfo:
|
||||
"""Wallet state returned by :meth:`FragmentClient.get_wallet`."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pyfragment.utils.cookies import get_cookies_from_browser
|
||||
from pyfragment.utils.cookies import CookieResult, get_cookies_from_browser
|
||||
from pyfragment.utils.decoder import clean_decode
|
||||
from pyfragment.utils.html import parse_auction_rows, parse_gift_items, parse_login_code
|
||||
from pyfragment.utils.http import (
|
||||
@@ -12,6 +12,7 @@ from pyfragment.utils.wallet import get_account_info, process_transaction
|
||||
|
||||
__all__ = [
|
||||
"clean_decode",
|
||||
"CookieResult",
|
||||
"get_cookies_from_browser",
|
||||
"parse_auction_rows",
|
||||
"parse_gift_items",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import rookiepy
|
||||
|
||||
from pyfragment.types import CookieError
|
||||
from pyfragment.types import CookieError, CookieResult
|
||||
from pyfragment.types.constants import FRAGMENT_BASE_URL, FRAGMENT_DOMAIN, REQUIRED_COOKIE_KEYS, SUPPORTED_BROWSERS
|
||||
|
||||
|
||||
def get_cookies_from_browser(browser: str = "chrome") -> dict[str, str]:
|
||||
def get_cookies_from_browser(browser: str = "chrome") -> CookieResult:
|
||||
"""Extract Fragment session cookies directly from an installed browser.
|
||||
|
||||
Reads the browser's on-disk cookie store (no extension required) and
|
||||
returns the four cookies required by :class:`~pyfragment.FragmentClient`.
|
||||
returns the four cookies required by :class:`~pyfragment.FragmentClient`
|
||||
along with the session expiry timestamp.
|
||||
|
||||
Args:
|
||||
browser: Browser name to read cookies from — case-insensitive. Supported values:
|
||||
@@ -19,8 +22,7 @@ def get_cookies_from_browser(browser: str = "chrome") -> dict[str, str]:
|
||||
``"firefox_based"``, ``"vivaldi"``, ``"librewolf"``, ``"safari"``.
|
||||
|
||||
Returns:
|
||||
A dict with the four required Fragment cookie keys:
|
||||
``stel_ssid``, ``stel_dt``, ``stel_token``, ``stel_ton_token``.
|
||||
:class:`CookieResult` with ``.cookies`` (dict) and ``.expires`` (ISO 8601 string or ``None``).
|
||||
|
||||
Raises:
|
||||
CookieError: If the browser is not supported, cookies cannot be read,
|
||||
@@ -42,4 +44,22 @@ def get_cookies_from_browser(browser: str = "chrome") -> dict[str, str]:
|
||||
if missing:
|
||||
raise CookieError(CookieError.MISSING_BROWSER_KEYS.format(browser=browser, keys=missing, url=FRAGMENT_BASE_URL))
|
||||
|
||||
return {k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS}
|
||||
expires_iso: str | None = None
|
||||
for cookie in jar:
|
||||
if cookie.get("name") == "stel_ssid":
|
||||
raw = cookie.get("expires")
|
||||
if isinstance(raw, (int, float)):
|
||||
expires_iso = datetime.fromtimestamp(raw, tz=timezone.utc).isoformat()
|
||||
elif isinstance(raw, str) and raw:
|
||||
for fmt in ("%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"):
|
||||
try:
|
||||
expires_iso = datetime.strptime(raw, fmt).replace(tzinfo=timezone.utc).isoformat()
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
break
|
||||
|
||||
return CookieResult(
|
||||
cookies={k: cookie_map[k] for k in REQUIRED_COOKIE_KEYS},
|
||||
expires=expires_iso,
|
||||
)
|
||||
|
||||
@@ -7,9 +7,8 @@ from ton_core import NetworkGlobalID
|
||||
from tonutils.clients import TonapiClient
|
||||
from tonutils.exceptions import ProviderResponseError
|
||||
|
||||
from pyfragment.types import TransactionError, WalletError
|
||||
from pyfragment.types import TransactionError, WalletError, WalletInfo
|
||||
from pyfragment.types.constants import MIN_TON_BALANCE, WALLET_CLASSES
|
||||
from pyfragment.types.results import WalletInfo
|
||||
from pyfragment.utils.decoder import clean_decode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
+32
-14
@@ -9,7 +9,7 @@ from pyfragment.types.constants import REQUIRED_COOKIE_KEYS
|
||||
from pyfragment.utils import get_cookies_from_browser
|
||||
|
||||
FAKE_JAR = [
|
||||
{"name": "stel_ssid", "value": "abc123", "domain": "fragment.com"},
|
||||
{"name": "stel_ssid", "value": "abc123", "domain": "fragment.com", "expires": "2027-04-03T20:52:16.375Z"},
|
||||
{"name": "stel_dt", "value": "-120", "domain": "fragment.com"},
|
||||
{"name": "stel_token", "value": "tok_xyz", "domain": "fragment.com"},
|
||||
{"name": "stel_ton_token", "value": "ton_xyz", "domain": "fragment.com"},
|
||||
@@ -23,6 +23,9 @@ def _mock_rookiepy(jar: list[dict] | None = None) -> MagicMock:
|
||||
return mock
|
||||
|
||||
|
||||
PATCH = "pyfragment.utils.cookies.rookiepy"
|
||||
|
||||
|
||||
# unsupported browser tests
|
||||
|
||||
|
||||
@@ -35,20 +38,35 @@ def test_unsupported_browser_raises() -> None:
|
||||
|
||||
|
||||
def test_returns_required_keys_only() -> None:
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy()}):
|
||||
with patch(PATCH, _mock_rookiepy()):
|
||||
result = get_cookies_from_browser("chrome")
|
||||
|
||||
assert set(result.keys()) == set(REQUIRED_COOKIE_KEYS)
|
||||
assert result["stel_ssid"] == "abc123"
|
||||
assert result["stel_dt"] == "-120"
|
||||
assert result["stel_token"] == "tok_xyz"
|
||||
assert result["stel_ton_token"] == "ton_xyz"
|
||||
assert "unrelated" not in result
|
||||
assert set(result.cookies.keys()) == set(REQUIRED_COOKIE_KEYS)
|
||||
assert result.cookies["stel_ssid"] == "abc123"
|
||||
assert result.cookies["stel_dt"] == "-120"
|
||||
assert result.cookies["stel_token"] == "tok_xyz"
|
||||
assert result.cookies["stel_ton_token"] == "ton_xyz"
|
||||
assert "unrelated" not in result.cookies
|
||||
|
||||
|
||||
def test_returns_expiry() -> None:
|
||||
with patch(PATCH, _mock_rookiepy()):
|
||||
result = get_cookies_from_browser("chrome")
|
||||
|
||||
assert result.expires == "2027-04-03T20:52:16.375000+00:00"
|
||||
|
||||
|
||||
def test_returns_none_expiry_when_missing() -> None:
|
||||
jar = [{"name": k, "value": "v"} for k in REQUIRED_COOKIE_KEYS]
|
||||
with patch(PATCH, _mock_rookiepy(jar)):
|
||||
result = get_cookies_from_browser("chrome")
|
||||
|
||||
assert result.expires is None
|
||||
|
||||
|
||||
def test_default_browser_is_chrome() -> None:
|
||||
mock_rp = _mock_rookiepy()
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
with patch(PATCH, mock_rp):
|
||||
get_cookies_from_browser()
|
||||
|
||||
mock_rp.chrome.assert_called_once_with(["fragment.com"])
|
||||
@@ -56,10 +74,10 @@ def test_default_browser_is_chrome() -> None:
|
||||
|
||||
def test_browser_name_is_case_insensitive() -> None:
|
||||
mock_rp = _mock_rookiepy()
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
with patch(PATCH, mock_rp):
|
||||
result = get_cookies_from_browser("Chrome")
|
||||
|
||||
assert result["stel_ssid"] == "abc123"
|
||||
assert result.cookies["stel_ssid"] == "abc123"
|
||||
|
||||
|
||||
# missing cookies tests
|
||||
@@ -71,7 +89,7 @@ def test_missing_cookies_raises() -> None:
|
||||
{"name": "stel_dt", "value": "-120"},
|
||||
# stel_token and stel_ton_token missing
|
||||
]
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy(partial_jar)}):
|
||||
with patch(PATCH, _mock_rookiepy(partial_jar)):
|
||||
with pytest.raises(CookieError, match="Fragment cookies not found in chrome"):
|
||||
get_cookies_from_browser("chrome")
|
||||
|
||||
@@ -83,7 +101,7 @@ def test_empty_cookie_value_treated_as_missing() -> None:
|
||||
{"name": "stel_token", "value": "tok_xyz"},
|
||||
{"name": "stel_ton_token", "value": "ton_xyz"},
|
||||
]
|
||||
with patch.dict("sys.modules", {"rookiepy": _mock_rookiepy(jar_with_empty)}):
|
||||
with patch(PATCH, _mock_rookiepy(jar_with_empty)):
|
||||
with pytest.raises(CookieError, match="Fragment cookies not found in chrome"):
|
||||
get_cookies_from_browser("chrome")
|
||||
|
||||
@@ -94,6 +112,6 @@ def test_empty_cookie_value_treated_as_missing() -> None:
|
||||
def test_browser_read_error_raises() -> None:
|
||||
mock_rp = MagicMock()
|
||||
mock_rp.chrome.side_effect = PermissionError("locked")
|
||||
with patch.dict("sys.modules", {"rookiepy": mock_rp}):
|
||||
with patch(PATCH, mock_rp):
|
||||
with pytest.raises(CookieError, match="Failed to read chrome cookies"):
|
||||
get_cookies_from_browser("chrome")
|
||||
|
||||
Reference in New Issue
Block a user