feat: add payment method support for premium and stars purchases, including validation and tests

This commit is contained in:
bohd4nx
2026-05-11 11:57:52 +03:00
parent c1ae5f0f28
commit 72918a6dd6
11 changed files with 308 additions and 44 deletions
+23 -15
View File
@@ -15,6 +15,7 @@ from pyfragment.types import (
CookieError,
CookieResult,
FragmentAPIError,
# exceptions
FragmentError,
FragmentPageError,
GiftsResult,
@@ -22,9 +23,12 @@ from pyfragment.types import (
NumbersResult,
OperationError,
ParseError,
# literal types
PaymentMethod,
PremiumGiveawayResult,
PremiumResult,
StarsGiveawayResult,
# results
StarsResult,
TerminateSessionsResult,
TransactionError,
@@ -41,31 +45,35 @@ __version__: str = version("pyfragment")
__all__ = [
"__version__",
"FragmentClient",
"AdsRechargeResult",
# results
"StarsResult",
"StarsGiveawayResult",
"PremiumResult",
"PremiumGiveawayResult",
"WalletInfo",
"AdsTopupResult",
"AdsRechargeResult",
"CookieResult",
"GiftsResult",
"LoginCodeResult",
"NumbersResult",
"PremiumGiveawayResult",
"PremiumResult",
"StarsGiveawayResult",
"StarsResult",
"TerminateSessionsResult",
"UsernamesResult",
"WalletInfo",
"ClientError",
"ConfigurationError",
"CookieError",
"CookieResult",
"FragmentAPIError",
# exceptions
"FragmentError",
"FragmentAPIError",
"FragmentPageError",
"ConfigurationError",
"UserNotFoundError",
"WalletError",
"VerificationError",
"TransactionError",
"AnonymousNumberError",
"ClientError",
"CookieError",
"OperationError",
"ParseError",
"TransactionError",
"UnexpectedError",
"UserNotFoundError",
"VerificationError",
"WalletError",
# literal types
"PaymentMethod",
]
+25 -6
View File
@@ -36,6 +36,7 @@ from pyfragment.types.constants import (
FRAGMENT_BASE_URL,
REQUIRED_COOKIE_KEYS,
SUPPORTED_WALLET_VERSIONS,
PaymentMethod,
WalletVersion,
)
from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers
@@ -125,31 +126,45 @@ class FragmentClient:
def __repr__(self) -> str:
return f"FragmentClient(wallet_version='{self.wallet_version}', cookies={len(self.cookies)} keys)"
async def purchase_premium(self, username: str, months: int, show_sender: bool = True) -> PremiumResult:
async def purchase_premium(
self,
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = "ton",
) -> PremiumResult:
"""Gift Telegram Premium to a user.
Args:
username: Recipient's Telegram username (with or without ``@``).
months: Duration — ``3``, ``6``, or ``12``.
show_sender: Show your name as the sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``.
"""
return await purchase_premium(self, username, months, show_sender)
return await purchase_premium(self, username, months, show_sender, payment_method)
async def purchase_stars(self, username: str, amount: int, show_sender: bool = True) -> StarsResult:
async def purchase_stars(
self,
username: str,
amount: int,
show_sender: bool = True,
payment_method: PaymentMethod = "ton",
) -> StarsResult:
"""Send Telegram Stars to a user.
Args:
username: Recipient's Telegram username (with or without ``@``).
amount: Number of stars — integer from ``50`` to ``1 000 000``.
show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``.
"""
return await purchase_stars(self, username, amount, show_sender)
return await purchase_stars(self, username, amount, show_sender, payment_method)
async def topup_ton(self, username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
"""Top up TON to a recipient's Telegram balance.
@@ -191,6 +206,7 @@ class FragmentClient:
channel: str,
winners: int,
amount: int,
payment_method: PaymentMethod = "ton",
) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel.
@@ -198,18 +214,20 @@ class FragmentClient:
channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``5``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
``winners``, and ``amount``.
"""
return await giveaway_stars(self, channel, winners, amount)
return await giveaway_stars(self, channel, winners, amount, payment_method)
async def giveaway_premium(
self,
channel: str,
winners: int,
months: int = 3,
payment_method: PaymentMethod = "ton",
) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel.
@@ -217,12 +235,13 @@ class FragmentClient:
channel: Channel username (with or without ``@``).
winners: Number of winners — positive integer.
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
``winners``, and ``amount``.
"""
return await giveaway_premium(self, channel, winners, months)
return await giveaway_premium(self, channel, winners, months, payment_method)
async def get_login_code(self, number: str) -> LoginCodeResult:
"""Fetch the current pending login code for an anonymous number.
+16 -2
View File
@@ -12,7 +12,7 @@ from pyfragment.types import (
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE
from pyfragment.types.constants import DEVICE, PREMIUM_GIVEAWAY_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
@@ -24,6 +24,7 @@ async def giveaway_premium(
channel: str,
winners: int,
months: int = 3,
payment_method: PaymentMethod = "ton",
) -> PremiumGiveawayResult:
"""Run a Telegram Premium giveaway for a channel.
@@ -32,6 +33,7 @@ async def giveaway_premium(
channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``24 000``.
months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
@@ -47,6 +49,13 @@ async def giveaway_premium(
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM)
if months not in (3, 6, 12):
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if payment_method not in SUPPORTED_PAYMENT_METHODS:
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)),
)
)
try:
result = await client.call(
@@ -60,7 +69,12 @@ async def giveaway_premium(
result = await client.call(
"initGiveawayPremiumRequest",
{"recipient": recipient, "quantity": str(winners), "months": str(months)},
{
"recipient": recipient,
"quantity": str(winners),
"months": str(months),
"payment_method": payment_method,
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
req_id = result.get("req_id")
+16 -2
View File
@@ -12,7 +12,7 @@ from pyfragment.types import (
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE
from pyfragment.types.constants import DEVICE, STARS_GIVEAWAY_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
@@ -24,6 +24,7 @@ async def giveaway_stars(
channel: str,
winners: int,
amount: int,
payment_method: PaymentMethod = "ton",
) -> StarsGiveawayResult:
"""Run a Telegram Stars giveaway for a channel.
@@ -32,6 +33,7 @@ async def giveaway_stars(
channel: Channel username (with or without ``@``).
winners: Number of winners — integer from ``1`` to ``5``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
@@ -47,6 +49,13 @@ async def giveaway_stars(
raise ConfigurationError(ConfigurationError.INVALID_WINNERS_STARS)
if not isinstance(amount, int) or not (500 <= amount <= 1_000_000):
raise ConfigurationError(ConfigurationError.INVALID_STARS_PER_WINNER)
if payment_method not in SUPPORTED_PAYMENT_METHODS:
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)),
)
)
try:
result = await client.call("searchStarsGiveawayRecipient", {"query": channel}, page_url=STARS_GIVEAWAY_PAGE)
@@ -56,7 +65,12 @@ async def giveaway_stars(
result = await client.call(
"initGiveawayStarsRequest",
{"recipient": recipient, "quantity": str(winners), "stars": str(amount)},
{
"recipient": recipient,
"quantity": str(winners),
"stars": str(amount),
"payment_method": payment_method,
},
page_url=STARS_GIVEAWAY_PAGE,
)
req_id = result.get("req_id")
+21 -3
View File
@@ -13,14 +13,20 @@ from pyfragment.types import (
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, PREMIUM_PAGE
from pyfragment.types.constants import DEVICE, PREMIUM_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
async def purchase_premium(client: FragmentClient, username: str, months: int, show_sender: bool = True) -> PremiumResult:
async def purchase_premium(
client: FragmentClient,
username: str,
months: int,
show_sender: bool = True,
payment_method: PaymentMethod = "ton",
) -> PremiumResult:
"""Gift Telegram Premium to a user.
Args:
@@ -28,6 +34,7 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s
username: Recipient's Telegram username (with or without ``@``).
months: Premium duration — ``3``, ``6``, or ``12``.
show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``.
@@ -40,6 +47,13 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s
"""
if months not in (3, 6, 12):
raise ConfigurationError(ConfigurationError.INVALID_MONTHS)
if payment_method not in SUPPORTED_PAYMENT_METHODS:
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)),
)
)
try:
result = await client.call("searchPremiumGiftRecipient", {"query": username, "months": months}, page_url=PREMIUM_PAGE)
@@ -52,7 +66,11 @@ async def purchase_premium(client: FragmentClient, username: str, months: int, s
{"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)
result = await client.call(
"initGiftPremiumRequest",
{"recipient": recipient, "months": months, "payment_method": payment_method},
page_url=PREMIUM_PAGE,
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
+23 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import time
from typing import TYPE_CHECKING
from pyfragment.types import (
@@ -12,14 +13,16 @@ from pyfragment.types import (
UserNotFoundError,
VerificationError,
)
from pyfragment.types.constants import DEVICE, STARS_PAGE
from pyfragment.types.constants import DEVICE, STARS_PAGE, SUPPORTED_PAYMENT_METHODS, PaymentMethod
from pyfragment.utils import get_account_info, process_transaction
if TYPE_CHECKING:
from pyfragment.client import FragmentClient
async def purchase_stars(client: FragmentClient, username: str, amount: int, show_sender: bool = True) -> StarsResult:
async def purchase_stars(
client: FragmentClient, username: str, amount: int, show_sender: bool = True, payment_method: PaymentMethod = "ton"
) -> StarsResult:
"""Send Telegram Stars to a user.
Args:
@@ -27,6 +30,7 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho
username: Recipient's Telegram username (with or without ``@``).
amount: Number of Stars to send — integer from ``50`` to ``1 000 000``.
show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``.
@@ -39,6 +43,13 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho
"""
if not isinstance(amount, int) or not (50 <= amount <= 1_000_000):
raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT)
if payment_method not in SUPPORTED_PAYMENT_METHODS:
raise ConfigurationError(
ConfigurationError.INVALID_PAYMENT_METHOD.format(
method=payment_method,
supported=", ".join(sorted(SUPPORTED_PAYMENT_METHODS)),
)
)
try:
result = await client.call("searchStarsRecipient", {"query": username, "quantity": ""}, page_url=STARS_PAGE)
@@ -46,7 +57,16 @@ async def purchase_stars(client: FragmentClient, username: str, amount: int, sho
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
result = await client.call("initBuyStarsRequest", {"recipient": recipient, "quantity": amount}, page_url=STARS_PAGE)
await client.call(
"updateStarsBuyState",
{"mode": "new", "lv": "false", "dh": str(int(time.time()))},
page_url=STARS_PAGE,
)
result = await client.call(
"initBuyStarsRequest",
{"recipient": recipient, "quantity": amount, "payment_method": payment_method},
page_url=STARS_PAGE,
)
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
+3
View File
@@ -1,3 +1,4 @@
from pyfragment.types.constants import PaymentMethod
from pyfragment.types.exceptions import (
AnonymousNumberError,
ClientError,
@@ -61,4 +62,6 @@ __all__ = [
"TerminateSessionsResult",
"UsernamesResult",
"WalletInfo",
# literal types
"PaymentMethod",
]
+9 -2
View File
@@ -5,6 +5,10 @@ from typing import Any, Literal, get_args
from tonutils.contracts.wallet import WalletV4R2, WalletV5R1
# Payment methods
PaymentMethod = Literal["ton", "usdt_ton"]
SUPPORTED_PAYMENT_METHODS: frozenset[str] = frozenset(get_args(PaymentMethod))
# Single source of truth for supported wallet versions
WalletVersion = Literal["V4R2", "V5R1"]
SUPPORTED_WALLET_VERSIONS: frozenset[str] = frozenset(get_args(WalletVersion))
@@ -74,12 +78,15 @@ BASE_HEADERS: dict[str, str] = {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"origin": FRAGMENT_BASE_URL,
"priority": "u=1, i",
"sec-ch-ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"',
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": '"Android"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": (
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1"
"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Mobile Safari/537.36"
),
"x-requested-with": "XMLHttpRequest",
}
+1
View File
@@ -28,6 +28,7 @@ class ConfigurationError(ClientError):
INVALID_WINNERS_STARS = "Invalid winners count: must be an integer between 1 and 5."
INVALID_WINNERS_PREMIUM = "Invalid winners count: must be an integer between 1 and 24 000."
INVALID_STARS_PER_WINNER = "Invalid Stars per winner: must be an integer between 500 and 1 000 000."
INVALID_PAYMENT_METHOD = "Invalid payment method '{method}'. Supported values: {supported}."
class CookieError(ClientError):