From 902b692dcbed7ba66ef3c75eafd9c56ce14465e1 Mon Sep 17 00:00:00 2001 From: bohd4nx Date: Sat, 21 Mar 2026 16:05:38 +0200 Subject: [PATCH] Refactor methods for Telegram Premium and Stars giveaways - Added new methods for handling giveaways: `giveaway_premium` and `giveaway_stars`. - Refactored existing purchase methods into separate files: `purchase_premium.py` and `purchase_stars.py`. - Removed old premium and stars methods from the codebase. - Updated `__init__.py` to include new giveaway methods in the public API. - Introduced new result types for giveaways: `PremiumGiveawayResult` and `StarsGiveawayResult`. - Updated constants for new giveaway pages. - Enhanced error handling for configuration and user validation in giveaway methods. - Updated tests to cover new functionality and refactored existing tests to match new method locations. --- CHANGELOG.md | 16 ++ examples/purchase_premium.py | 2 +- examples/purchase_stars.py | 4 +- examples/topup_ton.py | 2 +- pyfragment/__init__.py | 4 + pyfragment/client.py | 53 ++++++- pyfragment/methods/__init__.py | 10 +- pyfragment/methods/giveaway_premium.py | 139 ++++++++++++++++++ pyfragment/methods/giveaway_stars.py | 135 +++++++++++++++++ .../{premium.py => purchase_premium.py} | 19 ++- .../methods/{stars.py => purchase_stars.py} | 19 ++- pyfragment/methods/{ton.py => topup_ton.py} | 17 +++ pyfragment/types/__init__.py | 11 +- pyfragment/types/constants.py | 2 + pyfragment/types/exceptions.py | 3 + pyfragment/types/results.py | 48 ++++-- tests/006_test_methods_mock.py | 52 +++---- 17 files changed, 483 insertions(+), 53 deletions(-) create mode 100644 pyfragment/methods/giveaway_premium.py create mode 100644 pyfragment/methods/giveaway_stars.py rename pyfragment/methods/{premium.py => purchase_premium.py} (81%) rename pyfragment/methods/{stars.py => purchase_stars.py} (80%) rename pyfragment/methods/{ton.py => topup_ton.py} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8946978..93c6253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,22 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI --- +## [Unreleased] + +### Added +- `giveaway_stars(channel, winners, amount)` — run a Telegram Stars giveaway for a channel (1–5 winners, 500–1 000 000 stars each) +- `giveaway_premium(channel, winners, months)` — run a Telegram Premium giveaway for a channel (1–24 000 winners, 3/6/12 months each) +- `StarsGiveawayResult` and `PremiumGiveawayResult` result types +- `STARS_GIVEAWAY_PAGE` and `PREMIUM_GIVEAWAY_PAGE` URL constants + +### Changed +- All result types (`PremiumResult`, `StarsResult`, `StarsGiveawayResult`, `PremiumGiveawayResult`) now use a single unified `amount` field instead of `months`, `stars` — consistent API across every method +- `__repr__` on result types now includes the unit (`3 months`, `500 stars`) for clarity +- Method module files renamed to match their function: `premium.py` → `purchase_premium.py`, `stars.py` → `purchase_stars.py`, `ton.py` → `topup_ton.py` +- `timestamp` field removed from all result dataclasses + +--- + ## [2026.0.2] — 2026-03-20 ### Added diff --git a/examples/purchase_premium.py b/examples/purchase_premium.py index d35f7c4..f50ee0f 100644 --- a/examples/purchase_premium.py +++ b/examples/purchase_premium.py @@ -33,7 +33,7 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.months} months of Premium successfully sent to {result.username} | tx: {result.transaction_id}") + print(f"{result.amount} months of Premium successfully sent to {result.username} | tx: {result.transaction_id}") if __name__ == "__main__": diff --git a/examples/purchase_stars.py b/examples/purchase_stars.py index 4f44ab0..97410ca 100644 --- a/examples/purchase_stars.py +++ b/examples/purchase_stars.py @@ -19,7 +19,7 @@ COOKIES = { } USERNAME = "@username" -AMOUNT = 500 # 50–1 000 000 +AMOUNT = 500 # 50 to 1 000 000 stars, integer async def main() -> None: @@ -33,7 +33,7 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.stars} Stars successfully sent to {result.username} | tx: {result.transaction_id}") + print(f"{result.amount} Stars successfully sent to {result.username} | tx: {result.transaction_id}") if __name__ == "__main__": diff --git a/examples/topup_ton.py b/examples/topup_ton.py index 98c891d..96130f4 100644 --- a/examples/topup_ton.py +++ b/examples/topup_ton.py @@ -19,7 +19,7 @@ COOKIES = { } USERNAME = "@username" -AMOUNT = 10 # TON, integer — 1–1 000 000 000 +AMOUNT = 10 # TON, integer — 1 to 1 000 000 000 async def main() -> None: diff --git a/pyfragment/__init__.py b/pyfragment/__init__.py index d08f26e..a3d92e8 100644 --- a/pyfragment/__init__.py +++ b/pyfragment/__init__.py @@ -16,7 +16,9 @@ from pyfragment.types import ( FragmentPageError, OperationError, ParseError, + PremiumGiveawayResult, PremiumResult, + StarsGiveawayResult, StarsResult, TransactionError, UnexpectedError, @@ -32,7 +34,9 @@ __all__ = [ "__version__", "FragmentClient", "AdsTopupResult", + "PremiumGiveawayResult", "PremiumResult", + "StarsGiveawayResult", "StarsResult", "WalletInfo", "ClientError", diff --git a/pyfragment/client.py b/pyfragment/client.py index 066f444..cc01d7a 100644 --- a/pyfragment/client.py +++ b/pyfragment/client.py @@ -1,9 +1,11 @@ import json from typing import cast -from pyfragment.methods.premium import purchase_premium -from pyfragment.methods.stars import purchase_stars -from pyfragment.methods.ton import topup_ton +from pyfragment.methods.giveaway_premium import giveaway_premium +from pyfragment.methods.giveaway_stars import giveaway_stars +from pyfragment.methods.purchase_premium import purchase_premium +from pyfragment.methods.purchase_stars import purchase_stars +from pyfragment.methods.topup_ton import topup_ton from pyfragment.types import ( AdsTopupResult, ConfigurationError, @@ -13,6 +15,7 @@ from pyfragment.types import ( WalletInfo, ) from pyfragment.types.constants import DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS, SUPPORTED_WALLET_VERSIONS, WalletVersion +from pyfragment.types.results import PremiumGiveawayResult, StarsGiveawayResult from pyfragment.utils.wallet import get_wallet_info @@ -108,7 +111,7 @@ class FragmentClient: show_sender: Show your name as the sender. Defaults to ``True``. Returns: - :class:`PremiumResult` with ``transaction_id``, ``username``, ``months``, ``timestamp``. + :class:`PremiumResult` with ``transaction_id``, ``username``, and ``months``. """ return await purchase_premium(self, username, months, show_sender) @@ -121,7 +124,7 @@ class FragmentClient: show_sender: Show your name as the gift sender. Defaults to ``True``. Returns: - :class:`StarsResult` with ``transaction_id``, ``username``, ``stars``, ``timestamp``. + :class:`StarsResult` with ``transaction_id``, ``username``, and ``stars``. """ return await purchase_stars(self, username, amount, show_sender) @@ -134,7 +137,7 @@ class FragmentClient: show_sender: Show your name as the sender. Defaults to ``True``. Returns: - :class:`AdsTopupResult` with ``transaction_id``, ``username``, ``amount``, ``timestamp``. + :class:`AdsTopupResult` with ``transaction_id``, ``username``, and ``amount``. """ return await topup_ton(self, username, amount, show_sender) @@ -146,3 +149,41 @@ class FragmentClient: (``"active"``, ``"uninit"``, or ``"frozen"``), and ``balance`` in TON. """ return await get_wallet_info(self) + + async def giveaway_stars( + self, + channel: str, + winners: int, + amount: int, + ) -> StarsGiveawayResult: + """Run a Telegram Stars giveaway for a channel. + + Args: + 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``. + + Returns: + :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, + ``winners``, and ``amount``. + """ + return await giveaway_stars(self, channel, winners, amount) + + async def giveaway_premium( + self, + channel: str, + winners: int, + months: int = 3, + ) -> PremiumGiveawayResult: + """Run a Telegram Premium giveaway for a channel. + + Args: + channel: Channel username (with or without ``@``). + winners: Number of winners — positive integer. + months: Premium duration per winner — ``3``, ``6``, or ``12``. Defaults to ``3``. + + Returns: + :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, + ``winners``, and ``amount``. + """ + return await giveaway_premium(self, channel, winners, months) diff --git a/pyfragment/methods/__init__.py b/pyfragment/methods/__init__.py index c0f4e2e..21ddeee 100644 --- a/pyfragment/methods/__init__.py +++ b/pyfragment/methods/__init__.py @@ -1,5 +1,7 @@ -from pyfragment.methods.premium import purchase_premium -from pyfragment.methods.stars import purchase_stars -from pyfragment.methods.ton import topup_ton +from pyfragment.methods.giveaway_premium import giveaway_premium +from pyfragment.methods.giveaway_stars import giveaway_stars +from pyfragment.methods.purchase_premium import purchase_premium +from pyfragment.methods.purchase_stars import purchase_stars +from pyfragment.methods.topup_ton import topup_ton -__all__ = ["purchase_premium", "purchase_stars", "topup_ton"] +__all__ = ["giveaway_premium", "giveaway_stars", "purchase_premium", "purchase_stars", "topup_ton"] diff --git a/pyfragment/methods/giveaway_premium.py b/pyfragment/methods/giveaway_premium.py new file mode 100644 index 0000000..25c4ea3 --- /dev/null +++ b/pyfragment/methods/giveaway_premium.py @@ -0,0 +1,139 @@ +import json +from typing import TYPE_CHECKING + +import httpx + +from pyfragment.types import ( + ConfigurationError, + FragmentAPIError, + FragmentError, + UnexpectedError, + UserNotFoundError, +) +from pyfragment.types.constants import BASE_HEADERS, DEVICE, PREMIUM_GIVEAWAY_PAGE +from pyfragment.types.results import PremiumGiveawayResult +from pyfragment.utils import ( + execute_transaction_request, + fragment_post, + get_account_info, + get_fragment_hash, + process_transaction, +) + +if TYPE_CHECKING: + from pyfragment.client import FragmentClient + +# Page-specific headers +HEADERS: dict[str, str] = { + **BASE_HEADERS, + "referer": PREMIUM_GIVEAWAY_PAGE, + "x-aj-referer": PREMIUM_GIVEAWAY_PAGE, +} + + +async def _search_recipient( + session: httpx.AsyncClient, + fragment_hash: str, + channel: str, + winners: int, + months: int, +) -> str: + result = await fragment_post( + 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_post( + 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", + channel: str, + winners: int, + months: int = 3, +) -> PremiumGiveawayResult: + """Run a Telegram Premium giveaway for a channel. + + Args: + client: Authenticated :class:`FragmentClient` instance. + 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``. + + Returns: + :class:`PremiumGiveawayResult` with ``transaction_id``, ``channel``, + ``winners``, and ``months``. + + Raises: + ConfigurationError: If ``winners`` is not 1–24 000 or ``months`` is not 3, 6, or 12. + UserNotFoundError: If the channel is not found on Fragment. + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ + if not isinstance(winners, int) or not (1 <= winners <= 24_000): + raise ConfigurationError(ConfigurationError.INVALID_WINNERS_PREMIUM) + if months not in (3, 6, 12): + raise ConfigurationError(ConfigurationError.INVALID_MONTHS) + + try: + fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_GIVEAWAY_PAGE, client.timeout) + 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 = { + "account": json.dumps(account), + "device": DEVICE, + "transaction": 1, + "id": req_id, + "method": "getGiveawayPremiumLink", + } + transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash) + + tx_hash = await process_transaction(client, transaction) + return PremiumGiveawayResult( + transaction_id=tx_hash, + channel=channel, + winners=winners, + amount=months, + ) + + except FragmentError: + raise + except Exception as exc: + raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/methods/giveaway_stars.py b/pyfragment/methods/giveaway_stars.py new file mode 100644 index 0000000..c3e5c8f --- /dev/null +++ b/pyfragment/methods/giveaway_stars.py @@ -0,0 +1,135 @@ +import json +from typing import TYPE_CHECKING + +import httpx + +from pyfragment.types import ( + ConfigurationError, + FragmentAPIError, + FragmentError, + UnexpectedError, + UserNotFoundError, +) +from pyfragment.types.constants import BASE_HEADERS, DEVICE, STARS_GIVEAWAY_PAGE +from pyfragment.types.results import StarsGiveawayResult +from pyfragment.utils import ( + execute_transaction_request, + fragment_post, + get_account_info, + get_fragment_hash, + process_transaction, +) + +if TYPE_CHECKING: + from pyfragment.client import FragmentClient + +# Page-specific headers +HEADERS: dict[str, str] = { + **BASE_HEADERS, + "referer": STARS_GIVEAWAY_PAGE, + "x-aj-referer": STARS_GIVEAWAY_PAGE, +} + + +async def _search_recipient( + session: httpx.AsyncClient, + fragment_hash: str, + channel: str, +) -> str: + result = await fragment_post( + 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_post( + 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", + channel: str, + winners: int, + amount: int, +) -> StarsGiveawayResult: + """Run a Telegram Stars giveaway for a channel. + + Args: + client: Authenticated :class:`FragmentClient` instance. + 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``. + + Returns: + :class:`StarsGiveawayResult` with ``transaction_id``, ``channel``, + ``winners``, and ``amount``. + + Raises: + ConfigurationError: If ``winners`` is not 1–5 or ``amount`` is not 500–1 000 000. + UserNotFoundError: If the channel is not found on Fragment. + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ + if not isinstance(winners, int) or not (1 <= winners <= 5): + 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) + + try: + fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_GIVEAWAY_PAGE, client.timeout) + 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 = { + "account": json.dumps(account), + "device": DEVICE, + "transaction": 1, + "id": req_id, + "method": "getGiveawayStarsLink", + } + transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash) + + tx_hash = await process_transaction(client, transaction) + return StarsGiveawayResult( + transaction_id=tx_hash, + channel=channel, + winners=winners, + amount=amount, + ) + + except FragmentError: + raise + except Exception as exc: + raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc diff --git a/pyfragment/methods/premium.py b/pyfragment/methods/purchase_premium.py similarity index 81% rename from pyfragment/methods/premium.py rename to pyfragment/methods/purchase_premium.py index 598a787..f6a482a 100644 --- a/pyfragment/methods/premium.py +++ b/pyfragment/methods/purchase_premium.py @@ -88,6 +88,23 @@ async def _init_request( async def purchase_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult: + """Gift Telegram Premium to a user. + + Args: + client: Authenticated :class:`FragmentClient` instance. + 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``. + + Returns: + :class:`PremiumResult` with ``transaction_id``, ``username``, and ``months``. + + Raises: + ConfigurationError: If ``months`` is not ``3``, ``6``, or ``12``. + UserNotFoundError: If the user is not found on Fragment. + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ if months not in (3, 6, 12): raise ConfigurationError(ConfigurationError.INVALID_MONTHS) @@ -110,7 +127,7 @@ async def purchase_premium(client: "FragmentClient", username: str, months: int, transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash) tx_hash = await process_transaction(client, transaction) - return PremiumResult(transaction_id=tx_hash, username=username, months=months) + return PremiumResult(transaction_id=tx_hash, username=username, amount=months) except FragmentError: raise diff --git a/pyfragment/methods/stars.py b/pyfragment/methods/purchase_stars.py similarity index 80% rename from pyfragment/methods/stars.py rename to pyfragment/methods/purchase_stars.py index 874593d..cd9b371 100644 --- a/pyfragment/methods/stars.py +++ b/pyfragment/methods/purchase_stars.py @@ -75,6 +75,23 @@ async def _init_request( async def purchase_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult: + """Send Telegram Stars to a user. + + Args: + client: Authenticated :class:`FragmentClient` instance. + 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``. + + Returns: + :class:`StarsResult` with ``transaction_id``, ``username``, and ``stars``. + + Raises: + ConfigurationError: If ``amount`` is not an integer between 50 and 1 000 000. + UserNotFoundError: If the user is not found on Fragment. + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ if not isinstance(amount, int) or not (50 <= amount <= 1_000_000): raise ConfigurationError(ConfigurationError.INVALID_STARS_AMOUNT) @@ -97,7 +114,7 @@ async def purchase_stars(client: "FragmentClient", username: str, amount: int, s transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash) tx_hash = await process_transaction(client, transaction) - return StarsResult(transaction_id=tx_hash, username=username, stars=amount) + return StarsResult(transaction_id=tx_hash, username=username, amount=amount) except FragmentError: raise diff --git a/pyfragment/methods/ton.py b/pyfragment/methods/topup_ton.py similarity index 80% rename from pyfragment/methods/ton.py rename to pyfragment/methods/topup_ton.py index 44da5bb..ce094c8 100644 --- a/pyfragment/methods/ton.py +++ b/pyfragment/methods/topup_ton.py @@ -75,6 +75,23 @@ async def _init_request( async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult: + """Top up a Telegram Ads account balance with TON. + + Args: + client: Authenticated :class:`FragmentClient` instance. + username: Ads account username (with or without ``@``). + amount: Amount in TON — integer from ``1`` to ``1 000 000 000``. + show_sender: Show your name as the sender. Defaults to ``True``. + + Returns: + :class:`AdsTopupResult` with ``transaction_id``, ``username``, and ``amount``. + + Raises: + ConfigurationError: If ``amount`` is not an integer between 1 and 1 000 000 000. + UserNotFoundError: If the user is not found on Fragment. + FragmentAPIError: If the Fragment API returns an error. + UnexpectedError: For any other unexpected failure. + """ if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000): raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT) diff --git a/pyfragment/types/__init__.py b/pyfragment/types/__init__.py index 042f02b..5de01d6 100644 --- a/pyfragment/types/__init__.py +++ b/pyfragment/types/__init__.py @@ -13,7 +13,14 @@ from pyfragment.types.exceptions import ( VerificationError, WalletError, ) -from pyfragment.types.results import AdsTopupResult, PremiumResult, StarsResult, WalletInfo +from pyfragment.types.results import ( + AdsTopupResult, + PremiumGiveawayResult, + PremiumResult, + StarsGiveawayResult, + StarsResult, + WalletInfo, +) __all__ = [ # client exceptions @@ -33,7 +40,9 @@ __all__ = [ "WalletError", # result types "AdsTopupResult", + "PremiumGiveawayResult", "PremiumResult", + "StarsGiveawayResult", "StarsResult", "WalletInfo", ] diff --git a/pyfragment/types/constants.py b/pyfragment/types/constants.py index 362a850..dd118bc 100644 --- a/pyfragment/types/constants.py +++ b/pyfragment/types/constants.py @@ -21,7 +21,9 @@ REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", " # Fragment page URLs STARS_PAGE: str = "https://fragment.com/stars/buy" +STARS_GIVEAWAY_PAGE: str = "https://fragment.com/stars/giveaway" PREMIUM_PAGE: str = "https://fragment.com/premium/gift" +PREMIUM_GIVEAWAY_PAGE: str = "https://fragment.com/premium/giveaway" TON_PAGE: str = "https://fragment.com/ads/topup" # Tonkeeper device fingerprint — serialized once, reused in every tx_data payload. diff --git a/pyfragment/types/exceptions.py b/pyfragment/types/exceptions.py index 1cc2283..949f002 100644 --- a/pyfragment/types/exceptions.py +++ b/pyfragment/types/exceptions.py @@ -19,6 +19,9 @@ class ConfigurationError(ClientError): INVALID_USERNAME = ( "Invalid username '{username}'. Must be 5–32 characters: letters (A–Z, a–z), digits (0–9), or underscores (_)." ) + INVALID_WINNERS_STARS = "winners must be an integer between 1 and 5." + INVALID_WINNERS_PREMIUM = "winners must be an integer between 1 and 24 000." + INVALID_STARS_PER_WINNER = "amount must be an integer between 500 and 1 000 000 stars." class CookieError(ClientError): diff --git a/pyfragment/types/results.py b/pyfragment/types/results.py index a6ad138..1cf129f 100644 --- a/pyfragment/types/results.py +++ b/pyfragment/types/results.py @@ -1,7 +1,6 @@ -import time -from dataclasses import dataclass, field +from dataclasses import dataclass -__all__ = ["AdsTopupResult", "PremiumResult", "StarsResult", "WalletInfo"] +__all__ = ["AdsTopupResult", "PremiumGiveawayResult", "PremiumResult", "StarsGiveawayResult", "StarsResult", "WalletInfo"] @dataclass @@ -22,11 +21,10 @@ class PremiumResult: transaction_id: str username: str - months: int - timestamp: int = field(default_factory=lambda: int(time.time())) + amount: int def __repr__(self) -> str: - return f"PremiumResult(username='{self.username}', months={self.months}, tx='{self.transaction_id}')" + return f"PremiumResult(username='{self.username}', amount={self.amount} months, tx='{self.transaction_id}')" @dataclass @@ -35,11 +33,10 @@ class StarsResult: transaction_id: str username: str - stars: int - timestamp: int = field(default_factory=lambda: int(time.time())) + amount: int def __repr__(self) -> str: - return f"StarsResult(username='{self.username}', stars={self.stars}, tx='{self.transaction_id}')" + return f"StarsResult(username='{self.username}', amount={self.amount} stars, tx='{self.transaction_id}')" @dataclass @@ -49,7 +46,38 @@ class AdsTopupResult: transaction_id: str username: str amount: int - timestamp: int = field(default_factory=lambda: int(time.time())) def __repr__(self) -> str: return f"AdsTopupResult(username='{self.username}', amount={self.amount} TON, tx='{self.transaction_id}')" + + +@dataclass +class StarsGiveawayResult: + """Result of a successful Telegram Stars giveaway.""" + + transaction_id: str + channel: str + winners: int + amount: int + + def __repr__(self) -> str: + return ( + f"StarsGiveawayResult(channel='{self.channel}', winners={self.winners}, " + f"amount={self.amount} stars per winner, tx='{self.transaction_id}')" + ) + + +@dataclass +class PremiumGiveawayResult: + """Result of a successful Telegram Premium giveaway.""" + + transaction_id: str + channel: str + winners: int + amount: int + + def __repr__(self) -> str: + return ( + f"PremiumGiveawayResult(channel='{self.channel}', winners={self.winners}, " + f"amount={self.amount} months per winner, tx='{self.transaction_id}')" + ) diff --git a/tests/006_test_methods_mock.py b/tests/006_test_methods_mock.py index f86c69e..d67910d 100644 --- a/tests/006_test_methods_mock.py +++ b/tests/006_test_methods_mock.py @@ -31,10 +31,10 @@ def client() -> FragmentClient: @pytest.mark.asyncio async def test_purchase_stars_success(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.purchase_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.purchase_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch( - "pyfragment.methods.stars.fragment_post", + "pyfragment.methods.purchase_stars.fragment_post", AsyncMock( side_effect=[ {"found": {"recipient": FAKE_RECIPIENT}}, # searchStarsRecipient @@ -42,23 +42,23 @@ async def test_purchase_stars_success(client: FragmentClient) -> None: ] ), ), - patch("pyfragment.methods.stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), - patch("pyfragment.methods.stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + patch("pyfragment.methods.purchase_stars.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), + patch("pyfragment.methods.purchase_stars.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), ): result = await client.purchase_stars("testuser", amount=100) assert isinstance(result, StarsResult) assert result.transaction_id == FAKE_TX_HASH assert result.username == "testuser" - assert result.stars == 100 + assert result.amount == 100 @pytest.mark.asyncio async def test_purchase_stars_user_not_found(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), - patch("pyfragment.methods.stars.fragment_post", AsyncMock(return_value={"found": {}})), + patch("pyfragment.methods.purchase_stars.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.purchase_stars.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.purchase_stars.fragment_post", AsyncMock(return_value={"found": {}})), ): with pytest.raises(UserNotFoundError): await client.purchase_stars("ghost", amount=100) @@ -67,10 +67,10 @@ async def test_purchase_stars_user_not_found(client: FragmentClient) -> None: @pytest.mark.asyncio async def test_purchase_premium_success(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.purchase_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.purchase_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch( - "pyfragment.methods.premium.fragment_post", + "pyfragment.methods.purchase_premium.fragment_post", AsyncMock( side_effect=[ {"found": {"recipient": FAKE_RECIPIENT}}, # searchPremiumGiftRecipient @@ -79,23 +79,23 @@ async def test_purchase_premium_success(client: FragmentClient) -> None: ] ), ), - patch("pyfragment.methods.premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), - patch("pyfragment.methods.premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + patch("pyfragment.methods.purchase_premium.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), + patch("pyfragment.methods.purchase_premium.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), ): result = await client.purchase_premium("testuser", months=6) assert isinstance(result, PremiumResult) assert result.transaction_id == FAKE_TX_HASH assert result.username == "testuser" - assert result.months == 6 + assert result.amount == 6 @pytest.mark.asyncio async def test_purchase_premium_user_not_found(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), - patch("pyfragment.methods.premium.fragment_post", AsyncMock(return_value={"found": {}})), + patch("pyfragment.methods.purchase_premium.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.purchase_premium.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.purchase_premium.fragment_post", AsyncMock(return_value={"found": {}})), ): with pytest.raises(UserNotFoundError): await client.purchase_premium("ghost", months=3) @@ -104,10 +104,10 @@ async def test_purchase_premium_user_not_found(client: FragmentClient) -> None: @pytest.mark.asyncio async def test_topup_ton_success(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.topup_ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.topup_ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch( - "pyfragment.methods.ton.fragment_post", + "pyfragment.methods.topup_ton.fragment_post", AsyncMock( side_effect=[ {}, # updateAdsTopupState @@ -116,8 +116,8 @@ async def test_topup_ton_success(client: FragmentClient) -> None: ] ), ), - patch("pyfragment.methods.ton.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), - patch("pyfragment.methods.ton.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), + patch("pyfragment.methods.topup_ton.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)), + patch("pyfragment.methods.topup_ton.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)), ): result = await client.topup_ton("testuser", amount=10) @@ -130,10 +130,10 @@ async def test_topup_ton_success(client: FragmentClient) -> None: @pytest.mark.asyncio async def test_topup_ton_user_not_found(client: FragmentClient) -> None: with ( - patch("pyfragment.methods.ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), - patch("pyfragment.methods.ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), + patch("pyfragment.methods.topup_ton.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)), + patch("pyfragment.methods.topup_ton.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)), patch( - "pyfragment.methods.ton.fragment_post", + "pyfragment.methods.topup_ton.fragment_post", AsyncMock( side_effect=[ {}, # updateAdsTopupState