mirror of
https://github.com/vibe-existing/pyfragment.git
synced 2026-07-25 06:54:31 +00:00
feat: implement recharge_ads method for Telegram Ads account funding; add associated result type and example; enhance tests for validation and success cases
This commit is contained in:
@@ -16,6 +16,10 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI
|
|||||||
- `giveaway_premium(channel, winners, months)` — run a Telegram Premium giveaway for a channel (1–24 000 winners, 3/6/12 months each); raises `UserNotFoundError` if the channel is not found on Fragment, `ConfigurationError` if `winners` or `months` are invalid
|
- `giveaway_premium(channel, winners, months)` — run a Telegram Premium giveaway for a channel (1–24 000 winners, 3/6/12 months each); raises `UserNotFoundError` if the channel is not found on Fragment, `ConfigurationError` if `winners` or `months` are invalid
|
||||||
- `StarsGiveawayResult` and `PremiumGiveawayResult` result types
|
- `StarsGiveawayResult` and `PremiumGiveawayResult` result types
|
||||||
|
|
||||||
|
**Telegram Ads**
|
||||||
|
- `recharge_ads(account, amount)` — add funds to your own Telegram Ads account; `account` is the channel or bot username linked to your Ads account; raises `ConfigurationError` if `amount` is out of range (1–1 000 000 000 TON)
|
||||||
|
- `AdsRechargeResult` result type
|
||||||
|
|
||||||
**Raw API access**
|
**Raw API access**
|
||||||
- `FragmentClient.call(method, data, *, page_url)` — send a raw request to any Fragment API method without waiting for a library update
|
- `FragmentClient.call(method, data, *, page_url)` — send a raw request to any Fragment API method without waiting for a library update
|
||||||
- `FRAGMENT_BASE_URL` constant — single source of truth for the Fragment base URL used across all page constants and headers
|
- `FRAGMENT_BASE_URL` constant — single source of truth for the Fragment base URL used across all page constants and headers
|
||||||
@@ -32,6 +36,7 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI
|
|||||||
- `examples/giveaway_premium.py` — Premium giveaway with `UserNotFoundError` / `ConfigurationError` handling
|
- `examples/giveaway_premium.py` — Premium giveaway with `UserNotFoundError` / `ConfigurationError` handling
|
||||||
- `examples/call.py` — raw API call via `client.call()`
|
- `examples/call.py` — raw API call via `client.call()`
|
||||||
- `examples/anonymous_number.py` — login code fetch and session termination with `AnonymousNumberError` handling
|
- `examples/anonymous_number.py` — login code fetch and session termination with `AnonymousNumberError` handling
|
||||||
|
- `examples/recharge_ads.py` — self-service Ads recharge with `ConfigurationError` / `WalletError` handling
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- All result types (`PremiumResult`, `StarsResult`, `StarsGiveawayResult`, `PremiumGiveawayResult`) now use a single unified `amount` field instead of `months`, `stars` — consistent API across every method
|
- All result types (`PremiumResult`, `StarsResult`, `StarsGiveawayResult`, `PremiumGiveawayResult`) now use a single unified `amount` field instead of `months`, `stars` — consistent API across every method
|
||||||
@@ -39,6 +44,7 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI
|
|||||||
- Method module files renamed to match their function: `premium.py` → `purchase_premium.py`, `stars.py` → `purchase_stars.py`, `ton.py` → `topup_ton.py`
|
- 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
|
- `timestamp` field removed from all result dataclasses
|
||||||
- All page URL constants (`STARS_PAGE`, `PREMIUM_PAGE`, etc.) now built from `FRAGMENT_BASE_URL` instead of hardcoded strings
|
- All page URL constants (`STARS_PAGE`, `PREMIUM_PAGE`, etc.) now built from `FRAGMENT_BASE_URL` instead of hardcoded strings
|
||||||
|
- `STARS_REVENUE_PAGE` and `ADS_PAGE` constants replaced by a single `ADS_TOPUP_PAGE` shared by both `topup_ton` and `recharge_ads`
|
||||||
- `TransactionError` now includes an SSL hint when a broadcast fails due to certificate verification errors
|
- `TransactionError` now includes an SSL hint when a broadcast fails due to certificate verification errors
|
||||||
- `TransactionError.DUPLICATE_SEQNO` — dedicated error raised after 3 failed attempts due to a `406 Duplicate msg_seqno` response from the TON network; broadcast is automatically retried (up to 2 retries, 2 s apart) before giving up
|
- `TransactionError.DUPLICATE_SEQNO` — dedicated error raised after 3 failed attempts due to a `406 Duplicate msg_seqno` response from the TON network; broadcast is automatically retried (up to 2 retries, 2 s apart) before giving up
|
||||||
- All error message templates rewritten to follow a "what happened → why → what to do" pattern — every template is now actionable and includes a fix hint where applicable
|
- All error message templates rewritten to follow a "what happened → why → what to do" pattern — every template is now actionable and includes a fix hint where applicable
|
||||||
|
|||||||
+4
-3
@@ -4,8 +4,9 @@ Example: send a raw request to any Fragment API method.
|
|||||||
Use client.call() when you need to access a method that is not yet
|
Use client.call() when you need to access a method that is not yet
|
||||||
wrapped by the library, or to inspect raw API responses directly.
|
wrapped by the library, or to inspect raw API responses directly.
|
||||||
|
|
||||||
page_url must match the Fragment page the method belongs to — Fragment
|
page_url is optional — only set it when the target method belongs to a
|
||||||
requires a hash derived from each specific page.
|
specific Fragment page (Fragment derives the API hash per page).
|
||||||
|
Defaults to the Fragment base URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -23,7 +24,7 @@ COOKIES = {
|
|||||||
|
|
||||||
METHOD = "anyFragmentMethod" # replace with the actual method name
|
METHOD = "anyFragmentMethod" # replace with the actual method name
|
||||||
DATA = {"key": "value"} # replace with the actual request payload
|
DATA = {"key": "value"} # replace with the actual request payload
|
||||||
PAGE_URL = "https://fragment.com/stars/buy" # replace with the matching Fragment page
|
PAGE_URL = "https://fragment.com/stars/buy" # replace with the matching Fragment page (optional)
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Example: recharge your own Telegram Ads account with TON.
|
||||||
|
|
||||||
|
Amount must be an integer between 1 and 1 000 000 000 TON.
|
||||||
|
Your wallet must hold at least the recharge amount + ~0.056 TON for gas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient, WalletError
|
||||||
|
|
||||||
|
SEED = "word1 word2 ... word24"
|
||||||
|
API_KEY = "YOUR_TONAPI_KEY"
|
||||||
|
COOKIES = {
|
||||||
|
"stel_ssid": "YOUR_STEL_SSID",
|
||||||
|
"stel_dt": "YOUR_STEL_DT",
|
||||||
|
"stel_token": "YOUR_STEL_TOKEN",
|
||||||
|
"stel_ton_token": "YOUR_STEL_TON_TOKEN",
|
||||||
|
}
|
||||||
|
|
||||||
|
ACCOUNT = "@mychannel" # channel or bot username linked to your Telegram Ads account
|
||||||
|
AMOUNT = 10 # 1–1 000 000 000 TON
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client:
|
||||||
|
try:
|
||||||
|
result: AdsRechargeResult = await client.recharge_ads(ACCOUNT, amount=AMOUNT)
|
||||||
|
except WalletError as e:
|
||||||
|
print(f"Wallet error — insufficient balance or misconfiguration: {e}")
|
||||||
|
return
|
||||||
|
except ConfigurationError as e:
|
||||||
|
print(f"Invalid argument: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"{result.amount} TON recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -7,6 +7,7 @@ from importlib.metadata import version
|
|||||||
|
|
||||||
from pyfragment.client import FragmentClient
|
from pyfragment.client import FragmentClient
|
||||||
from pyfragment.types import (
|
from pyfragment.types import (
|
||||||
|
AdsRechargeResult,
|
||||||
AdsTopupResult,
|
AdsTopupResult,
|
||||||
AnonymousNumberError,
|
AnonymousNumberError,
|
||||||
ClientError,
|
ClientError,
|
||||||
@@ -36,6 +37,7 @@ __version__: str = version("pyfragment")
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"__version__",
|
"__version__",
|
||||||
"FragmentClient",
|
"FragmentClient",
|
||||||
|
"AdsRechargeResult",
|
||||||
"AdsTopupResult",
|
"AdsTopupResult",
|
||||||
"LoginCodeResult",
|
"LoginCodeResult",
|
||||||
"PremiumGiveawayResult",
|
"PremiumGiveawayResult",
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from pyfragment.methods.giveaway_premium import giveaway_premium
|
|||||||
from pyfragment.methods.giveaway_stars import giveaway_stars
|
from pyfragment.methods.giveaway_stars import giveaway_stars
|
||||||
from pyfragment.methods.purchase_premium import purchase_premium
|
from pyfragment.methods.purchase_premium import purchase_premium
|
||||||
from pyfragment.methods.purchase_stars import purchase_stars
|
from pyfragment.methods.purchase_stars import purchase_stars
|
||||||
|
from pyfragment.methods.recharge_ads import recharge_ads
|
||||||
from pyfragment.methods.topup_ton import topup_ton
|
from pyfragment.methods.topup_ton import topup_ton
|
||||||
from pyfragment.types import (
|
from pyfragment.types import (
|
||||||
|
AdsRechargeResult,
|
||||||
AdsTopupResult,
|
AdsTopupResult,
|
||||||
ConfigurationError,
|
ConfigurationError,
|
||||||
CookieError,
|
CookieError,
|
||||||
@@ -153,6 +155,19 @@ class FragmentClient:
|
|||||||
"""
|
"""
|
||||||
return await topup_ton(self, username, amount, show_sender)
|
return await topup_ton(self, username, amount, show_sender)
|
||||||
|
|
||||||
|
async def recharge_ads(self, account: str, amount: int) -> AdsRechargeResult:
|
||||||
|
"""Add funds to your own Telegram Ads account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account: Your Fragment Ads account identifier — the channel or bot username
|
||||||
|
the Ads account is linked to (e.g. ``"@mychannel"``).
|
||||||
|
amount: Amount in TON — integer from ``1`` to ``1 000 000 000``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`AdsRechargeResult` with ``transaction_id`` and ``amount``.
|
||||||
|
"""
|
||||||
|
return await recharge_ads(self, account, amount)
|
||||||
|
|
||||||
async def get_wallet(self) -> WalletInfo:
|
async def get_wallet(self) -> WalletInfo:
|
||||||
"""Return the address, state and balance of the TON wallet.
|
"""Return the address, state and balance of the TON wallet.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from pyfragment.methods.giveaway_premium import giveaway_premium
|
|||||||
from pyfragment.methods.giveaway_stars import giveaway_stars
|
from pyfragment.methods.giveaway_stars import giveaway_stars
|
||||||
from pyfragment.methods.purchase_premium import purchase_premium
|
from pyfragment.methods.purchase_premium import purchase_premium
|
||||||
from pyfragment.methods.purchase_stars import purchase_stars
|
from pyfragment.methods.purchase_stars import purchase_stars
|
||||||
|
from pyfragment.methods.recharge_ads import recharge_ads
|
||||||
from pyfragment.methods.topup_ton import topup_ton
|
from pyfragment.methods.topup_ton import topup_ton
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -11,6 +12,7 @@ __all__ = [
|
|||||||
"giveaway_stars",
|
"giveaway_stars",
|
||||||
"purchase_premium",
|
"purchase_premium",
|
||||||
"purchase_stars",
|
"purchase_stars",
|
||||||
|
"recharge_ads",
|
||||||
"terminate_sessions",
|
"terminate_sessions",
|
||||||
"toggle_login_codes",
|
"toggle_login_codes",
|
||||||
"topup_ton",
|
"topup_ton",
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import json
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from pyfragment.types import (
|
||||||
|
ConfigurationError,
|
||||||
|
FragmentAPIError,
|
||||||
|
FragmentError,
|
||||||
|
UnexpectedError,
|
||||||
|
)
|
||||||
|
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
|
||||||
|
from pyfragment.types.results import AdsRechargeResult
|
||||||
|
from pyfragment.utils import (
|
||||||
|
execute_transaction_request,
|
||||||
|
fragment_request,
|
||||||
|
get_account_info,
|
||||||
|
get_fragment_hash,
|
||||||
|
make_headers,
|
||||||
|
process_transaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pyfragment.client import FragmentClient
|
||||||
|
|
||||||
|
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
|
||||||
|
|
||||||
|
|
||||||
|
async def _init_request(
|
||||||
|
session: httpx.AsyncClient,
|
||||||
|
fragment_hash: str,
|
||||||
|
account: str,
|
||||||
|
amount: int,
|
||||||
|
) -> str:
|
||||||
|
result = await fragment_request(
|
||||||
|
session,
|
||||||
|
fragment_hash,
|
||||||
|
HEADERS,
|
||||||
|
{
|
||||||
|
"account": account,
|
||||||
|
"amount": amount,
|
||||||
|
"method": "initAdsRechargeRequest",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
req_id = result.get("req_id")
|
||||||
|
if not req_id:
|
||||||
|
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Ads recharge"))
|
||||||
|
return req_id
|
||||||
|
|
||||||
|
|
||||||
|
async def recharge_ads(client: "FragmentClient", account: str, amount: int) -> AdsRechargeResult:
|
||||||
|
"""Add funds to your own Telegram Ads account.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: Authenticated :class:`FragmentClient` instance.
|
||||||
|
account: Your Fragment Ads account identifier — the channel or bot username
|
||||||
|
the Ads account is linked to (e.g. ``"@mychannel"``).
|
||||||
|
amount: Amount in TON — integer from ``1`` to ``1 000 000 000``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`AdsRechargeResult` with ``transaction_id`` and ``amount``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConfigurationError: If ``amount`` is not a valid integer in the allowed range.
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
|
||||||
|
account_info = await get_account_info(client)
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||||
|
await fragment_request(session, fragment_hash, HEADERS, {"method": "updateAdsState", "mode": "new"})
|
||||||
|
req_id = await _init_request(session, fragment_hash, account, amount)
|
||||||
|
|
||||||
|
tx_data = {
|
||||||
|
"account": json.dumps(account_info),
|
||||||
|
"device": DEVICE,
|
||||||
|
"transaction": 1,
|
||||||
|
"id": req_id,
|
||||||
|
"method": "getAdsRechargeLink",
|
||||||
|
}
|
||||||
|
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
|
||||||
|
|
||||||
|
tx_hash = await process_transaction(client, transaction)
|
||||||
|
return AdsRechargeResult(transaction_id=tx_hash, amount=amount)
|
||||||
|
|
||||||
|
except FragmentError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
|
||||||
@@ -11,7 +11,7 @@ from pyfragment.types import (
|
|||||||
UnexpectedError,
|
UnexpectedError,
|
||||||
UserNotFoundError,
|
UserNotFoundError,
|
||||||
)
|
)
|
||||||
from pyfragment.types.constants import DEVICE, TON_PAGE
|
from pyfragment.types.constants import ADS_TOPUP_PAGE, DEVICE
|
||||||
from pyfragment.utils import (
|
from pyfragment.utils import (
|
||||||
execute_transaction_request,
|
execute_transaction_request,
|
||||||
fragment_request,
|
fragment_request,
|
||||||
@@ -24,7 +24,7 @@ from pyfragment.utils import (
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pyfragment.client import FragmentClient
|
from pyfragment.client import FragmentClient
|
||||||
|
|
||||||
HEADERS: dict[str, str] = make_headers(TON_PAGE)
|
HEADERS: dict[str, str] = make_headers(ADS_TOPUP_PAGE)
|
||||||
|
|
||||||
|
|
||||||
async def _search_recipient(
|
async def _search_recipient(
|
||||||
@@ -92,7 +92,7 @@ async def topup_ton(client: "FragmentClient", username: str, amount: int, show_s
|
|||||||
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
|
raise ConfigurationError(ConfigurationError.INVALID_TON_AMOUNT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, TON_PAGE, client.timeout)
|
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, ADS_TOPUP_PAGE, client.timeout)
|
||||||
account = await get_account_info(client)
|
account = await get_account_info(client)
|
||||||
|
|
||||||
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
async with httpx.AsyncClient(cookies=client.cookies, timeout=client.timeout) as session:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from pyfragment.types.exceptions import (
|
|||||||
WalletError,
|
WalletError,
|
||||||
)
|
)
|
||||||
from pyfragment.types.results import (
|
from pyfragment.types.results import (
|
||||||
|
AdsRechargeResult,
|
||||||
AdsTopupResult,
|
AdsTopupResult,
|
||||||
LoginCodeResult,
|
LoginCodeResult,
|
||||||
PremiumGiveawayResult,
|
PremiumGiveawayResult,
|
||||||
@@ -43,6 +44,7 @@ __all__ = [
|
|||||||
"VerificationError",
|
"VerificationError",
|
||||||
"WalletError",
|
"WalletError",
|
||||||
# result types
|
# result types
|
||||||
|
"AdsRechargeResult",
|
||||||
"AdsTopupResult",
|
"AdsTopupResult",
|
||||||
"LoginCodeResult",
|
"LoginCodeResult",
|
||||||
"PremiumGiveawayResult",
|
"PremiumGiveawayResult",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
|
|||||||
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
|
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
|
||||||
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
|
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
|
||||||
PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway"
|
PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway"
|
||||||
TON_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
|
ADS_TOPUP_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
|
||||||
NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers"
|
NUMBERS_PAGE: str = f"{FRAGMENT_BASE_URL}/numbers"
|
||||||
|
|
||||||
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
||||||
|
|||||||
+24
-11
@@ -1,16 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AdsTopupResult",
|
|
||||||
"LoginCodeResult",
|
|
||||||
"PremiumGiveawayResult",
|
|
||||||
"PremiumResult",
|
|
||||||
"StarsGiveawayResult",
|
|
||||||
"StarsResult",
|
|
||||||
"TerminateSessionsResult",
|
|
||||||
"WalletInfo",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WalletInfo:
|
class WalletInfo:
|
||||||
@@ -105,6 +94,17 @@ class LoginCodeResult:
|
|||||||
return f"LoginCodeResult(number='{self.number}', code={code_str}, active_sessions={self.active_sessions})"
|
return f"LoginCodeResult(number='{self.number}', code={code_str}, active_sessions={self.active_sessions})"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdsRechargeResult:
|
||||||
|
"""Result of a successful self-recharge of Telegram Ads balance."""
|
||||||
|
|
||||||
|
transaction_id: str
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"AdsRechargeResult(amount={self.amount} TON, tx='{self.transaction_id}')"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TerminateSessionsResult:
|
class TerminateSessionsResult:
|
||||||
"""Result of :meth:`FragmentClient.terminate_sessions`."""
|
"""Result of :meth:`FragmentClient.terminate_sessions`."""
|
||||||
@@ -114,3 +114,16 @@ class TerminateSessionsResult:
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"TerminateSessionsResult(number='{self.number}', message={self.message!r})"
|
return f"TerminateSessionsResult(number='{self.number}', message={self.message!r})"
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AdsRechargeResult",
|
||||||
|
"AdsTopupResult",
|
||||||
|
"LoginCodeResult",
|
||||||
|
"PremiumGiveawayResult",
|
||||||
|
"PremiumResult",
|
||||||
|
"StarsGiveawayResult",
|
||||||
|
"StarsResult",
|
||||||
|
"TerminateSessionsResult",
|
||||||
|
"WalletInfo",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""Unit tests for recharge_ads — self-service Telegram Ads recharge."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyfragment import FragmentClient
|
||||||
|
from pyfragment.types import AdsRechargeResult, ConfigurationError
|
||||||
|
from tests.shared import FAKE_ACCOUNT, FAKE_ADS_ACCOUNT, FAKE_HASH, FAKE_REQ_ID, FAKE_TRANSACTION, FAKE_TX_HASH
|
||||||
|
|
||||||
|
# recharge_ads validation tests
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recharge_ads_float_amount(client: FragmentClient) -> None:
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=5.5) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
# recharge_ads mocked tests
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_recharge_ads_success(client: FragmentClient) -> None:
|
||||||
|
with (
|
||||||
|
patch("pyfragment.methods.recharge_ads.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||||
|
patch("pyfragment.methods.recharge_ads.get_account_info", AsyncMock(return_value=FAKE_ACCOUNT)),
|
||||||
|
patch(
|
||||||
|
"pyfragment.methods.recharge_ads.fragment_request",
|
||||||
|
AsyncMock(
|
||||||
|
side_effect=[
|
||||||
|
{}, # updateAdsState
|
||||||
|
{"req_id": FAKE_REQ_ID}, # initAdsRechargeRequest
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
patch("pyfragment.methods.recharge_ads.execute_transaction_request", AsyncMock(return_value=FAKE_TRANSACTION)),
|
||||||
|
patch("pyfragment.methods.recharge_ads.process_transaction", AsyncMock(return_value=FAKE_TX_HASH)),
|
||||||
|
):
|
||||||
|
result = await client.recharge_ads(FAKE_ADS_ACCOUNT, amount=10)
|
||||||
|
|
||||||
|
assert isinstance(result, AdsRechargeResult)
|
||||||
|
assert result.transaction_id == FAKE_TX_HASH
|
||||||
|
assert result.amount == 10
|
||||||
@@ -32,6 +32,13 @@ FAKE_RESPONSE: dict[str, Any] = {"status": "ok", "data": {"value": 42}}
|
|||||||
FAKE_ADDRESS: str = "UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5"
|
FAKE_ADDRESS: str = "UQCppfw5DxWgdVHf3zkmZS8k1mt9oAUYxQLwq2fz3nhO8No5"
|
||||||
FAKE_BALANCE_NANOTON: int = 1_500_000_000 # 1.5 TON
|
FAKE_BALANCE_NANOTON: int = 1_500_000_000 # 1.5 TON
|
||||||
|
|
||||||
|
# recharge_ads
|
||||||
|
FAKE_ADS_ACCOUNT: str = "@mychannel"
|
||||||
|
|
||||||
|
# Revenue withdrawals
|
||||||
|
FAKE_WITHDRAWAL_WALLET: str = "EQDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
FAKE_REVENUE_TX: str = "revenue_tx_abc123"
|
||||||
|
|
||||||
# Anonymous number
|
# Anonymous number
|
||||||
FAKE_HTML_WITH_CODE: str = """
|
FAKE_HTML_WITH_CODE: str = """
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
Reference in New Issue
Block a user