mirror of
https://github.com/vibe-existing/pyfragment.git
synced 2026-07-25 06:54:31 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
+47
-6
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
+38
-10
@@ -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}')"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user