feat: Enhance cookie handling and browser support

- Added new exceptions in `exceptions.py` for unsupported browsers and cookie read failures.
- Introduced `get_cookies_from_browser` function in `cookies.py` to extract session cookies from various browsers.
- Modified tests to cover new cookie extraction functionality, including success and failure cases.
- Refactored existing tests to use `patch.object` for mocking client calls.
- Updated dependencies in `pyproject.toml` to include `rookiepy` for cookie extraction.
This commit is contained in:
bohd4nx
2026-04-14 00:12:22 +03:00
parent 503baa9a94
commit 733d138fcc
28 changed files with 507 additions and 737 deletions
+2
View File
@@ -34,12 +34,14 @@ from pyfragment.types import (
WalletError,
WalletInfo,
)
from pyfragment.utils.cookies import get_cookies_from_browser
__version__: str = version("pyfragment")
__all__ = [
"__version__",
"FragmentClient",
"get_cookies_from_browser",
"AdsRechargeResult",
"AdsTopupResult",
"GiftsResult",
+31 -47
View File
@@ -1,18 +1,14 @@
import html
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import AnonymousNumberError, FragmentAPIError, FragmentError, UnexpectedError
from pyfragment.types.constants import NUMBERS_PAGE
from pyfragment.types.results import LoginCodeResult, TerminateSessionsResult
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_login_code
from pyfragment.utils import parse_login_code
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(NUMBERS_PAGE)
def _strip_plus(number: str) -> str:
return number.lstrip("+") if isinstance(number, str) else number
@@ -35,14 +31,11 @@ async def get_login_code(client: "FragmentClient", number: str) -> LoginCodeResu
"""
try:
clean = _strip_plus(number)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{"number": clean, "lt": "0", "from_app": "1", "method": "updateLoginCodes"},
)
result = await client.call(
"updateLoginCodes",
{"number": clean, "lt": "0", "from_app": "1"},
page_url=NUMBERS_PAGE,
)
if result.get("html"):
code, active_sessions = parse_login_code(result["html"])
@@ -71,14 +64,11 @@ async def toggle_login_codes(client: "FragmentClient", number: str, can_receive:
"""
try:
clean = _strip_plus(number)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{"number": clean, "can_receive": 1 if can_receive else 0, "method": "toggleLoginCodes"},
)
result = await client.call(
"toggleLoginCodes",
{"number": clean, "can_receive": 1 if can_receive else 0},
page_url=NUMBERS_PAGE,
)
if result.get("error"):
raise FragmentAPIError(html.unescape(result["error"]))
@@ -110,39 +100,33 @@ async def terminate_sessions(client: "FragmentClient", number: str) -> Terminate
"""
try:
clean = _strip_plus(number)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
# Step 1: initiate — Fragment returns a confirmation hash.
confirmation = await fragment_request(
session,
fragment_hash,
HEADERS,
{"number": clean, "method": "terminatePhoneSessions"},
confirmation = await client.call(
"terminatePhoneSessions",
{"number": clean},
page_url=NUMBERS_PAGE,
)
if confirmation.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(confirmation["error"]))
)
if confirmation.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(confirmation["error"]))
)
terminate_hash = confirmation.get("terminate_hash")
if not terminate_hash:
raise AnonymousNumberError(AnonymousNumberError.NOT_OWNED.format(number=number))
terminate_hash = confirmation.get("terminate_hash")
if not terminate_hash:
raise AnonymousNumberError(AnonymousNumberError.NOT_OWNED.format(number=number))
result = await client.call(
"terminatePhoneSessions",
{"number": clean, "terminate_hash": terminate_hash},
page_url=NUMBERS_PAGE,
)
# Step 2: confirm with the hash.
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{"number": clean, "terminate_hash": terminate_hash, "method": "terminatePhoneSessions"},
if result.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(result["error"]))
)
if result.get("error"):
raise AnonymousNumberError(
AnonymousNumberError.TERMINATE_FAILED.format(number=number, error=html.unescape(result["error"]))
)
return TerminateSessionsResult(number=number, message=result.get("msg"))
except FragmentError:
+28 -70
View File
@@ -1,79 +1,21 @@
import json
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE
from pyfragment.types.results import PremiumGiveawayResult
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(PREMIUM_GIVEAWAY_PAGE)
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
channel: str,
winners: int,
months: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"query": channel,
"quantity": winners,
"months": months,
"method": "searchPremiumGiveawayRecipient",
},
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
winners: int,
months: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"recipient": recipient,
"quantity": str(winners),
"months": str(months),
"method": "initGiveawayPremiumRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium giveaway"))
return req_id
async def giveaway_premium(
client: "FragmentClient",
@@ -105,21 +47,37 @@ async def giveaway_premium(
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_GIVEAWAY_PAGE, client.timeout)
result = await client.call(
"searchPremiumGiveawayRecipient",
{"query": channel, "quantity": winners, "months": months},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
result = await client.call(
"initGiveawayPremiumRequest",
{"recipient": recipient, "quantity": str(winners), "months": str(months)},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium giveaway"))
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
recipient = await _search_recipient(session, fragment_hash, channel, winners, months)
req_id = await _init_request(session, fragment_hash, recipient, winners, months)
tx_data = {
transaction = await client.call(
"getGiveawayPremiumLink",
{
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"method": "getGiveawayPremiumLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return PremiumGiveawayResult(
+24 -66
View File
@@ -1,75 +1,21 @@
import json
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE
from pyfragment.types.results import StarsGiveawayResult
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(STARS_GIVEAWAY_PAGE)
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
channel: str,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"query": channel,
"method": "searchStarsGiveawayRecipient",
},
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
winners: int,
amount: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"recipient": recipient,
"quantity": str(winners),
"stars": str(amount),
"method": "initGiveawayStarsRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars giveaway"))
return req_id
async def giveaway_stars(
client: "FragmentClient",
@@ -101,21 +47,33 @@ async def giveaway_stars(
raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_GIVEAWAY_PAGE, client.timeout)
result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=channel))
result = await client.call(
"initGiveawayStarsRequest",
{"recipient": recipient, "quantity": str(winners), "stars": str(amount)},
page_url=STARS_GIVEAWAY_PAGE,
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars giveaway"))
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
recipient = await _search_recipient(session, fragment_hash, channel)
req_id = await _init_request(session, fragment_hash, recipient, winners, amount)
tx_data = {
transaction = await client.call(
"getGiveawayStarsLink",
{
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"method": "getGiveawayStarsLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=STARS_GIVEAWAY_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return StarsGiveawayResult(
+25 -77
View File
@@ -2,8 +2,6 @@ import json
import time
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
ConfigurationError,
FragmentAPIError,
@@ -11,77 +9,14 @@ from pyfragment.types import (
PremiumResult,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, PREMIUM_PAGE
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(PREMIUM_PAGE)
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
months: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"query": username,
"months": months,
"method": "searchPremiumGiftRecipient",
},
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
months: int,
) -> str:
await fragment_request(
session,
fragment_hash,
HEADERS,
{
"mode": "new",
"lv": "false",
"dh": str(int(time.time())),
"method": "updatePremiumState",
},
)
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"recipient": recipient,
"months": months,
"method": "initGiftPremiumRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
return req_id
async def purchase_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult:
"""Gift Telegram Premium to a user.
@@ -105,22 +40,35 @@ async def purchase_premium(client: "FragmentClient", username: str, months: int,
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_PAGE, client.timeout)
result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
await client.call(
"updatePremiumState",
{"mode": "new", "lv": "false", "dh": str(int(time.time()))},
page_url=PREMIUM_PAGE,
)
result = await client.call("initGiftPremiumRequest", {"recipient": recipient, "months": months}, page_url=PREMIUM_PAGE)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
recipient = await _search_recipient(session, fragment_hash, username, months)
req_id = await _init_request(session, fragment_hash, recipient, months)
tx_data = {
transaction = await client.call(
"getGiftPremiumLink",
{
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getGiftPremiumLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=PREMIUM_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return PremiumResult(transaction_id=tx_hash, username=username, amount=months)
+20 -65
View File
@@ -1,8 +1,6 @@
import json
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
ConfigurationError,
FragmentAPIError,
@@ -10,65 +8,14 @@ from pyfragment.types import (
StarsResult,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, STARS_PAGE
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(STARS_PAGE)
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"query": username,
"quantity": "",
"method": "searchStarsRecipient",
},
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
amount: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"recipient": recipient,
"quantity": amount,
"method": "initBuyStarsRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
return req_id
async def purchase_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult:
"""Send Telegram Stars to a user.
@@ -92,22 +39,30 @@ async def purchase_stars(client: "FragmentClient", username: str, amount: int, s
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_PAGE, client.timeout)
result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
result = await client.call("initBuyStarsRequest", {"recipient": recipient, "quantity": amount}, page_url=STARS_PAGE)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
recipient = await _search_recipient(session, fragment_hash, username)
req_id = await _init_request(session, fragment_hash, recipient, amount)
tx_data = {
transaction = await client.call(
"getBuyStarsLink",
{
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getBuyStarsLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=STARS_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return StarsResult(transaction_id=tx_hash, username=username, amount=amount)
+17 -44
View File
@@ -1,52 +1,20 @@
import json
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
ConfigurationError,
FragmentAPIError,
FragmentError,
UnexpectedError,
VerificationError,
)
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
from pyfragment.types.results import AdsRechargeResult
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
account: str,
amount: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"account": account,
"amount": amount,
"method": "initAdsRechargeRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
return req_id
async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> AdsRechargeResult:
"""Add funds to your own Telegram Ads account.
@@ -69,21 +37,26 @@ async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> A
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
await client.call("updateAdsState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE)
result = await client.call("initAdsRechargeRequest", {"account": account, "amount": amount}, page_url=ADS_TOPUP_PAGE)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
account_info = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
await fragment_request(session, fragment_hash, HEADERS, {"method": "updateAdsState", "mode": "new"})
req_id = await _init_request(session, fragment_hash, account, amount)
tx_data = {
transaction = await client.call(
"getAdsRechargeLink",
{
"account": json.dumps(account_info),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"method": "getAdsRechargeLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=ADS_TOPUP_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
+2 -8
View File
@@ -1,17 +1,13 @@
from typing import TYPE_CHECKING, Any
import httpx
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
from pyfragment.types.constants import GIFTS_PAGE
from pyfragment.types.results import GiftsResult
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_gift_items
from pyfragment.utils import parse_gift_items
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(GIFTS_PAGE)
async def search_gifts(
client: "FragmentClient",
@@ -64,9 +60,7 @@ async def search_gifts(
data["offset"] = offset
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, GIFTS_PAGE, client.timeout)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
result = await fragment_request(session, fragment_hash, HEADERS, data)
result = await client.call("searchAuctions", data, page_url=GIFTS_PAGE)
if result.get("error"):
raise FragmentAPIError(result["error"])
+2 -8
View File
@@ -1,17 +1,13 @@
from typing import TYPE_CHECKING, Any
import httpx
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
from pyfragment.types.constants import NUMBERS_PAGE
from pyfragment.types.results import NumbersResult
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows
from pyfragment.utils import parse_auction_rows
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(NUMBERS_PAGE)
async def search_numbers(
client: "FragmentClient",
@@ -49,9 +45,7 @@ async def search_numbers(
data["offset_id"] = offset_id
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, NUMBERS_PAGE, client.timeout)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
result = await fragment_request(session, fragment_hash, HEADERS, data)
result = await client.call("searchAuctions", data, page_url=NUMBERS_PAGE)
if result.get("error"):
raise FragmentAPIError(result["error"])
+2 -8
View File
@@ -1,17 +1,13 @@
from typing import TYPE_CHECKING, Any
import httpx
from pyfragment.types import FragmentAPIError, FragmentError, UnexpectedError
from pyfragment.types.constants import FRAGMENT_BASE_URL
from pyfragment.types.results import UsernamesResult
from pyfragment.utils import fragment_request, get_fragment_hash, make_headers, parse_auction_rows
from pyfragment.utils import parse_auction_rows
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(FRAGMENT_BASE_URL)
async def search_usernames(
client: "FragmentClient",
@@ -49,9 +45,7 @@ async def search_usernames(
data["offset_id"] = offset_id
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, FRAGMENT_BASE_URL, client.timeout)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
result = await fragment_request(session, fragment_hash, HEADERS, data)
result = await client.call("searchAuctions", data, page_url=FRAGMENT_BASE_URL)
if result.get("error"):
raise FragmentAPIError(result["error"])
+22 -65
View File
@@ -1,8 +1,6 @@
import json
from typing import TYPE_CHECKING
import httpx
from pyfragment.types import (
AdsTopupResult,
ConfigurationError,
@@ -10,65 +8,14 @@ from pyfragment.types import (
FragmentError,
UnexpectedError,
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
from pyfragment.utils import (
execute_transaction_request,
fragment_request,
get_account_info,
get_fragment_hash,
make_headers,
process_transaction,
)
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
) -> str:
await fragment_request(session, fragment_hash, HEADERS, {"mode": "new", "method": "updateAdsTopupState"})
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"query": username,
"method": "searchAdsTopupRecipient",
},
)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
amount: int,
) -> str:
result = await fragment_request(
session,
fragment_hash,
HEADERS,
{
"recipient": recipient,
"amount": amount,
"method": "initAdsTopupRequest",
},
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup"))
return req_id
async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
"""Topup ton to recipient's Telegram balance.
@@ -92,22 +39,32 @@ async def topup_ton(client: "FragmentClient", username: str, amount: int, show_s
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
await client.call("updateAdsTopupState", {"mode": "new"}, page_url=ADS_TOPUP_PAGE)
result = await client.call("searchAdsTopupRecipient", {"query": username}, page_url=ADS_TOPUP_PAGE)
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
result = await client.call("initAdsTopupRequest", {"recipient": recipient, "amount": amount}, page_url=ADS_TOPUP_PAGE)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup"))
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
recipient = await _search_recipient(session, fragment_hash, username)
req_id = await _init_request(session, fragment_hash, recipient, amount)
tx_data = {
transaction = await client.call(
"getAdsTopupLink",
{
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getAdsTopupLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
},
page_url=ADS_TOPUP_PAGE,
)
if transaction.get("need_verify"):
raise VerificationError(VerificationError.KYC_REQUIRED)
tx_hash = await process_transaction(client, transaction)
return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
+22 -2
View File
@@ -19,8 +19,9 @@ DEFAULT_TIMEOUT: float = 30.0
# Required Fragment session cookie keys
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
# Fragment page URLs
FRAGMENT_BASE_URL: str = "https://fragment.com"
# Fragment domain and page URLs
FRAGMENT_DOMAIN: str = "fragment.com" # for rookiepy
FRAGMENT_BASE_URL: str = f"https://{FRAGMENT_DOMAIN}"
STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
@@ -29,6 +30,25 @@ ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers"
GIFTS_PAGE: str = f"{FRAGMENT_BASE_URL}/gifts"
# Browsers supported by get_cookies_from_browser()
SUPPORTED_BROWSERS: frozenset[str] = frozenset(
{
"arc",
"brave",
"chrome",
"chromium",
"chromium_based",
"edge",
"firefox",
"firefox_based",
"librewolf",
"opera",
"opera_gx",
"safari",
"vivaldi",
}
)
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
DEVICE: str = json.dumps(
{
+8
View File
@@ -35,6 +35,14 @@ class CookieError(ClientError):
"Fragment cookies are missing or empty for key(s): {keys}. "
"Open fragment.com in your browser, log in, and copy fresh cookies."
)
UNSUPPORTED_BROWSER = "Unsupported browser: '{browser}'. Supported: {supported}."
BROWSER_READ_FAILED = (
"Failed to read {browser} cookies: {exc}. " "Make sure {browser} is installed and you are logged in to {url}."
)
MISSING_BROWSER_KEYS = (
"Fragment cookies not found in {browser}: {keys}. "
"Make sure you are logged in to {url} and have connected your TON wallet in {browser}."
)
class FragmentAPIError(FragmentError):
+2
View File
@@ -1,3 +1,4 @@
from pyfragment.utils.cookies import 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 (
@@ -11,6 +12,7 @@ from pyfragment.utils.wallet import get_account_info, process_transaction
__all__ = [
"clean_decode",
"get_cookies_from_browser",
"parse_auction_rows",
"parse_gift_items",
"parse_login_code",
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
import rookiepy
from pyfragment.types import CookieError
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]:
"""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`.
Args:
browser: Browser name to read cookies from — case-insensitive. Supported values:
``"chrome"`` (default), ``"firefox"``, ``"edge"``, ``"brave"``, ``"arc"``,
``"opera"``, ``"opera_gx"``, ``"chromium"``, ``"chromium_based"``,
``"firefox_based"``, ``"vivaldi"``, ``"librewolf"``, ``"safari"``.
Returns:
A dict with the four required Fragment cookie keys:
``stel_ssid``, ``stel_dt``, ``stel_token``, ``stel_ton_token``.
Raises:
CookieError: If the browser is not supported, cookies cannot be read,
or required keys are missing.
"""
key = browser.lower()
if key not in SUPPORTED_BROWSERS:
supported = ", ".join(sorted(SUPPORTED_BROWSERS))
raise CookieError(CookieError.UNSUPPORTED_BROWSER.format(browser=browser, supported=supported))
try:
jar: list[dict] = getattr(rookiepy, key)([FRAGMENT_DOMAIN])
except Exception as exc:
raise CookieError(CookieError.BROWSER_READ_FAILED.format(browser=browser, exc=exc, url=FRAGMENT_BASE_URL)) from exc
cookie_map: dict[str, str] = {c["name"]: c["value"] for c in jar if c.get("name") and c.get("value")}
missing = [k for k in REQUIRED_COOKIE_KEYS if not str(cookie_map.get(k, "")).strip()]
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}
+1 -1
View File
@@ -1,6 +1,6 @@
import base64
from pytoniq_core import Cell
from ton_core import Cell
from pyfragment.types import ParseError
+1 -1
View File
@@ -3,9 +3,9 @@ import base64
import ssl
from typing import TYPE_CHECKING, Any
from ton_core import NetworkGlobalID
from tonutils.clients import TonapiClient
from tonutils.exceptions import ProviderResponseError
from tonutils.types import NetworkGlobalID
from pyfragment.types import TransactionError, WalletError
from pyfragment.types.constants import MIN_TON_BALANCE, WALLET_CLASSES