feat: update payment method options and validation across purchase and giveaway examples; add AlreadySubscribedError exception handling

This commit is contained in:
bohd4nx
2026-05-29 23:14:59 +03:00
parent afaa42776b
commit 695430744a
14 changed files with 109 additions and 28 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ COOKIES = {
CHANNEL = "https://t.me/channel"
WINNERS = 10 # 124 000
MONTHS = 3 # 3, 6 or 12
PAYMENT_METHOD = PaymentMethod.TON # PaymentMethod.TON or PaymentMethod.USDT_TON
PAYMENT_METHOD = PaymentMethod.TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
+1 -1
View File
@@ -28,7 +28,7 @@ COOKIES = {
CHANNEL = "https://t.me/channel"
WINNERS = 3 # 115
AMOUNT = 1000 # 5001 000 000 stars per winner
PAYMENT_METHOD = PaymentMethod.USDT_TON # PaymentMethod.TON or PaymentMethod.USDT_TON
PAYMENT_METHOD = PaymentMethod.USDT_TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
+1 -1
View File
@@ -27,7 +27,7 @@ COOKIES = {
USERNAME = "https://t.me/username"
MONTHS = 3 # 3, 6 or 12
PAYMENT_METHOD = PaymentMethod.TON # PaymentMethod.TON or PaymentMethod.USDT_TON
PAYMENT_METHOD = PaymentMethod.TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
+1 -1
View File
@@ -27,7 +27,7 @@ COOKIES = {
USERNAME = "https://t.me/username"
AMOUNT = 500 # 5010 000 000 stars
PAYMENT_METHOD = PaymentMethod.USDT_TON # PaymentMethod.TON or PaymentMethod.USDT_TON
PAYMENT_METHOD = PaymentMethod.USDT_TON # TON, USDT_TON, USDT_ETH, USDT_POL, USDC_ETH, USDC_BASE, USDC_POL
async def main() -> None:
+2
View File
@@ -4,6 +4,7 @@ from importlib.metadata import version
from pyfragment.client import FragmentClient
from pyfragment.core.cookies import get_cookies_from_browser
from pyfragment.exceptions import (
AlreadySubscribedError,
AnonymousNumberError,
ClientError,
ConfigurationError,
@@ -53,6 +54,7 @@ __all__ = [
"FragmentAPIError",
"FragmentPageError",
"ConfigurationError",
"AlreadySubscribedError",
"UserNotFoundError",
"WalletError",
"VerificationError",
+12 -4
View File
@@ -148,7 +148,9 @@ class FragmentClient:
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
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"``.
payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``,
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``.
Returns:
:class:`PremiumResult` with ``transaction_id``, ``username``, and ``amount``.
@@ -168,7 +170,9 @@ class FragmentClient:
username: Recipient identifier — ``@username``, ``username``, or ``https://t.me/username``.
amount: Number of stars — integer from ``50`` to ``10 000 000``.
show_sender: Show your name as the gift sender. Defaults to ``True``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``,
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``.
Returns:
:class:`StarsResult` with ``transaction_id``, ``username``, and ``amount``.
@@ -224,7 +228,9 @@ class FragmentClient:
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
winners: Number of winners — integer from ``1`` to ``15``.
amount: Stars each winner receives — integer from ``500`` to ``1 000 000``.
payment_method: Payment currency — ``"ton"`` (default) or ``"usdt_ton"``.
payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``,
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``.
Returns:
:class:`StarsGiveawayResult` with ``transaction_id``, ``channel``,
@@ -245,7 +251,9 @@ class FragmentClient:
channel: Channel identifier — ``@channel``, ``channel``, or ``https://t.me/channel``.
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"``.
payment_method: Payment currency — ``"ton"`` (default), ``"usdt_ton"``,
``"usdt_eth"``, ``"usdt_pol"``, ``"usdc_eth"``, ``"usdc_base"``,
or ``"usdc_pol"``.
Returns:
:class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``,
+17 -2
View File
@@ -38,6 +38,11 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _state_nonce() -> str:
# Fragment expects a pseudo-random nonce-like dh value in giveaway state updates.
return str(random.randint(100_000_000, 2_147_483_647))
async def giveaway_stars(
client: FragmentClient,
channel: str,
@@ -65,7 +70,12 @@ async def giveaway_stars(
await client.call(
"updateStarsGiveawayState",
{"mode": "new", "lv": "false", "dh": str(random.randint(100_000_000, 999_999_999))},
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=STARS_GIVEAWAY_PAGE,
)
await client.call(
"updateStarsGiveawayPrices",
{"quantity": winners, "stars": amount},
page_url=STARS_GIVEAWAY_PAGE,
)
@@ -162,11 +172,16 @@ async def giveaway_premium(
{
"mode": "new",
"lv": "false",
"dh": str(random.randint(100_000_000, 999_999_999)),
"dh": _state_nonce(),
"quantity": "",
},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
await client.call(
"updatePremiumGiveawayPrices",
{"quantity": winners},
page_url=PREMIUM_GIVEAWAY_PAGE,
)
result = await client.call(
"initGiveawayPremiumRequest",
+12 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import json
import logging
import time
import random
from typing import TYPE_CHECKING
from pyfragment.core.constants import (
@@ -17,6 +17,7 @@ from pyfragment.domains.payments import parse_required_payment_amount
from pyfragment.domains.tonapi.account import get_account_info
from pyfragment.domains.tonapi.transaction import process_transaction
from pyfragment.exceptions import (
AlreadySubscribedError,
ConfigurationError,
FragmentAPIError,
FragmentError,
@@ -34,6 +35,11 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _state_nonce() -> str:
# Fragment accepts a pseudo-random request nonce in state update methods.
return str(random.randint(100_000_000, 2_147_483_647))
async def purchase_stars(
client: FragmentClient,
username: str,
@@ -59,7 +65,7 @@ async def purchase_stars(
await client.call(
"updateStarsBuyState",
{"mode": "new", "lv": "false", "dh": str(int(time.time()))},
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=STARS_PAGE,
)
result = await client.call(
@@ -140,7 +146,7 @@ async def purchase_premium(
await client.call(
"updatePremiumState",
{"mode": "new", "lv": "false", "dh": str(int(time.time()))},
{"mode": "new", "lv": "false", "dh": _state_nonce()},
page_url=PREMIUM_PAGE,
)
result = await client.call(
@@ -148,6 +154,9 @@ async def purchase_premium(
{"recipient": recipient, "months": months, "payment_method": payment_method},
page_url=PREMIUM_PAGE,
)
error_text = str(result.get("error", "")).strip().lower()
if "already subscribed to telegram premium" in error_text:
raise AlreadySubscribedError(AlreadySubscribedError.PREMIUM_ACTIVE)
required_payment_amount = parse_required_payment_amount(result)
req_id = result.get("req_id")
if not req_id:
+7
View File
@@ -96,6 +96,12 @@ class UserNotFoundError(FragmentAPIError):
)
class AlreadySubscribedError(FragmentAPIError):
"""Raised when trying to gift Premium to a user who already has an active subscription."""
PREMIUM_ACTIVE = "This account is already subscribed to Telegram Premium."
class AnonymousNumberError(FragmentAPIError):
"""Raised for Fragment anonymous number API failures."""
@@ -167,6 +173,7 @@ __all__ = [
"FragmentAPIError",
"FragmentPageError",
"AnonymousNumberError",
"AlreadySubscribedError",
"UserNotFoundError",
"TransactionError",
"ParseError",
+5
View File
@@ -17,6 +17,11 @@ else:
class PaymentMethod(StrEnum):
TON = "ton"
USDT_TON = "usdt_ton"
USDT_ETH = "usdt_eth"
USDT_POL = "usdt_pol"
USDC_ETH = "usdc_eth"
USDC_BASE = "usdc_base"
USDC_POL = "usdc_pol"
class WalletVersion(StrEnum):
+17 -7
View File
@@ -7,6 +7,14 @@ import pytest
import pyfragment.domains.giveaways.giveaway as _giveaway_stars_mod
import pyfragment.domains.purchases.purchase as _purchase_stars_mod
from pyfragment import ConfigurationError, FragmentClient, StarsGiveawayResult, StarsResult, UserNotFoundError
from pyfragment.core.constants.limits import (
STARS_GIVEAWAY_MAX,
STARS_GIVEAWAY_MIN,
STARS_PURCHASE_MAX,
STARS_PURCHASE_MIN,
STARS_WINNERS_MAX,
STARS_WINNERS_MIN,
)
from pyfragment.models.enums import PaymentMethod
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
@@ -16,13 +24,13 @@ from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSAC
@pytest.mark.asyncio
async def test_purchase_stars_amount_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_stars("@user", amount=49)
await client.purchase_stars("@user", amount=STARS_PURCHASE_MIN - 1)
@pytest.mark.asyncio
async def test_purchase_stars_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_stars("@user", amount=1_000_001)
await client.purchase_stars("@user", amount=STARS_PURCHASE_MAX + 1)
@pytest.mark.asyncio
@@ -114,25 +122,25 @@ async def test_purchase_stars_user_not_found(client: FragmentClient) -> None:
@pytest.mark.asyncio
async def test_giveaway_stars_winners_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=0, amount=500)
await client.giveaway_stars("@channel", winners=STARS_WINNERS_MIN - 1, amount=STARS_GIVEAWAY_MIN)
@pytest.mark.asyncio
async def test_giveaway_stars_winners_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=6, amount=500)
await client.giveaway_stars("@channel", winners=STARS_WINNERS_MAX + 1, amount=STARS_GIVEAWAY_MIN)
@pytest.mark.asyncio
async def test_giveaway_stars_amount_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=1, amount=499)
await client.giveaway_stars("@channel", winners=STARS_WINNERS_MIN, amount=STARS_GIVEAWAY_MIN - 1)
@pytest.mark.asyncio
async def test_giveaway_stars_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_stars("@channel", winners=1, amount=1_000_001)
await client.giveaway_stars("@channel", winners=STARS_WINNERS_MIN, amount=STARS_GIVEAWAY_MAX + 1)
@pytest.mark.asyncio
@@ -166,6 +174,7 @@ async def test_giveaway_stars_success(client: FragmentClient) -> None:
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -189,6 +198,7 @@ async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> N
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -201,7 +211,7 @@ async def test_giveaway_stars_passes_payment_method(client: FragmentClient) -> N
):
await client.giveaway_stars("@channel", winners=3, amount=1000, payment_method=PaymentMethod.USDT_TON)
init_call = call_mock.await_args_list[2]
init_call = call_mock.await_args_list[3]
assert init_call.args[0] == "initGiveawayStarsRequest"
assert init_call.args[1]["payment_method"] == "usdt_ton"
assert proc_mock.await_args is not None
+27 -4
View File
@@ -7,6 +7,8 @@ import pytest
import pyfragment.domains.giveaways.giveaway as _giveaway_premium_mod
import pyfragment.domains.purchases.purchase as _purchase_premium_mod
from pyfragment import ConfigurationError, FragmentClient, PremiumGiveawayResult, PremiumResult, UserNotFoundError
from pyfragment.core.constants.limits import PREMIUM_MONTHS_VALID, PREMIUM_WINNERS_MAX, PREMIUM_WINNERS_MIN
from pyfragment.exceptions import AlreadySubscribedError
from pyfragment.models.enums import PaymentMethod
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
@@ -22,7 +24,7 @@ async def test_purchase_premium_invalid_months(client: FragmentClient) -> None:
@pytest.mark.asyncio
async def test_purchase_premium_months_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.purchase_premium("@user", months=0)
await client.purchase_premium("@user", months=min(PREMIUM_MONTHS_VALID) - 1)
@pytest.mark.asyncio
@@ -85,6 +87,25 @@ async def test_purchase_premium_passes_payment_method(client: FragmentClient) ->
assert proc_mock.await_args.kwargs["payment_method"] == "usdt_ton"
@pytest.mark.asyncio
async def test_purchase_premium_already_subscribed_raises(client: FragmentClient) -> None:
with (
patch.object(
client,
"call",
AsyncMock(
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{}, # updatePremiumState
{"error": "This account is already subscribed to Telegram Premium."},
]
),
),
):
with pytest.raises(AlreadySubscribedError):
await client.purchase_premium("@user", months=6)
@pytest.mark.asyncio
@pytest.mark.parametrize("query", ["@user", "monk", "https://t.me/monk"])
async def test_purchase_premium_accepts_query_formats(client: FragmentClient, query: str) -> None:
@@ -111,13 +132,13 @@ async def test_purchase_premium_user_not_found(client: FragmentClient) -> None:
@pytest.mark.asyncio
async def test_giveaway_premium_winners_too_low(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_premium("@channel", winners=0, months=3)
await client.giveaway_premium("@channel", winners=PREMIUM_WINNERS_MIN - 1, months=3)
@pytest.mark.asyncio
async def test_giveaway_premium_winners_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.giveaway_premium("@channel", winners=24_001, months=3)
await client.giveaway_premium("@channel", winners=PREMIUM_WINNERS_MAX + 1, months=3)
@pytest.mark.asyncio
@@ -151,6 +172,7 @@ async def test_giveaway_premium_success(client: FragmentClient) -> None:
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -174,6 +196,7 @@ async def test_giveaway_premium_passes_payment_method(client: FragmentClient) ->
side_effect=[
{"found": {"recipient": FAKE_RECIPIENT}},
{},
{},
{"req_id": FAKE_REQ_ID},
FAKE_TRANSACTION,
]
@@ -186,7 +209,7 @@ async def test_giveaway_premium_passes_payment_method(client: FragmentClient) ->
):
await client.giveaway_premium("@channel", winners=10, months=6, payment_method=PaymentMethod.USDT_TON)
init_call = call_mock.await_args_list[2]
init_call = call_mock.await_args_list[3]
assert init_call.args[0] == "initGiveawayPremiumRequest"
assert init_call.args[1]["payment_method"] == "usdt_ton"
assert proc_mock.await_args is not None
+3 -2
View File
@@ -6,6 +6,7 @@ import pytest
import pyfragment.domains.ads.tonup as _topup_ton_mod
from pyfragment import AdsTopupResult, ConfigurationError, FragmentClient, UserNotFoundError
from pyfragment.core.constants.limits import TON_TOPUP_MAX, TON_TOPUP_MIN
from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
# Topup TON validation tests
@@ -14,13 +15,13 @@ from tests.shared import FAKE_ACCOUNT, FAKE_RECIPIENT, FAKE_REQ_ID, FAKE_TRANSAC
@pytest.mark.asyncio
async def test_topup_ton_amount_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.topup_ton("@user", amount=0)
await client.topup_ton("@user", amount=TON_TOPUP_MIN - 1)
@pytest.mark.asyncio
async def test_topup_ton_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.topup_ton("@user", amount=1_000_000_001)
await client.topup_ton("@user", amount=TON_TOPUP_MAX + 1)
@pytest.mark.asyncio
+3 -2
View File
@@ -6,6 +6,7 @@ import pytest
import pyfragment.domains.ads.recharge as _recharge_ads_mod
from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient
from pyfragment.core.constants.limits import TON_TOPUP_MAX, TON_TOPUP_MIN
from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
# recharge_ads validation tests
@@ -14,13 +15,13 @@ from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_REQ_ID, FAKE_TRANS
@pytest.mark.asyncio
async def test_recharge_ads_amount_zero(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=0)
await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=TON_TOPUP_MIN - 1)
@pytest.mark.asyncio
async def test_recharge_ads_amount_too_high(client: FragmentClient) -> None:
with pytest.raises(ConfigurationError):
await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=1_000_000_001)
await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=TON_TOPUP_MAX + 1)
@pytest.mark.asyncio