diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a0934..16c7559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,51 +12,43 @@ and this project uses [Calendar Versioning](https://calver.org/) (`YYYY.MINOR.MI ### Added **Giveaways** -- `giveaway_stars(channel, winners, amount)` — run a Telegram Stars giveaway for a channel (1–5 winners, 500–1 000 000 stars each); raises `UserNotFoundError` if the channel is not found on Fragment, `ConfigurationError` if `winners` or `amount` are out of range -- `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 +- `giveaway_stars(channel, winners, amount)` — Stars giveaway; 1–5 winners, 500–1 000 000 stars each +- `giveaway_premium(channel, winners, months)` — Premium giveaway; 1–24 000 winners, 3/6/12 months each +- `StarsGiveawayResult`, `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) +- `recharge_ads(account, amount)` — top up a Telegram Ads account; 1–1 000 000 000 TON - `AdsRechargeResult` result type **Marketplace** -- `search_usernames(query?, sort?, filter?, offset_id?)` — search the Fragment marketplace for Telegram usernames; `sort` is one of `"price_desc"`, `"price_asc"`, `"listed"`, `"ending"`; `filter` is one of `""`, `"auction"`, `"sale"`, `"sold"`; supports pagination via `offset_id`; returns parsed item dicts with `slug`, `name`, `status`, `price`, `date` -- `search_numbers(query?, sort?, filter?, offset_id?)` — search the Fragment marketplace for anonymous Telegram numbers; same `sort` / `filter` / `offset_id` semantics as `search_usernames` -- `search_gifts(query?, collection?, sort?, filter?, view?, attr?, offset?)` — search the Fragment gifts marketplace; `collection` is the gift collection slug (e.g. `"artisanbrick"`); `attr` accepts trait filters as `{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}`; supports integer-based pagination via `offset`; returns parsed item dicts with `slug`, `name`, `status`, `price`, `date` +- `search_usernames(query?, sort?, filter?, offset_id?)` — search Fragment usernames; `sort`: `price_desc / price_asc / listed / ending`, `filter`: `auction / sale / sold` +- `search_numbers(query?, sort?, filter?, offset_id?)` — search Fragment anonymous numbers; same `sort` / `filter` / pagination semantics +- `search_gifts(query?, collection?, sort?, filter?, view?, attr?, offset?)` — search Fragment gifts; `attr` accepts `{"Model": ["Foosball"], "Backdrop": ["Celtic Blue"]}` - `UsernamesResult`, `NumbersResult`, `GiftsResult` result types -**Raw API access** -- `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 +**Anonymous numbers** +- `get_login_code(number)` — fetch the current pending login code +- `toggle_login_codes(number, can_receive)` — enable or disable login code delivery +- `terminate_sessions(number)` — terminate all active Telegram sessions (two-step flow handled internally) +- `LoginCodeResult`, `TerminateSessionsResult` result types; `AnonymousNumberError` exception -**Anonymous number management** -- `get_login_code(number)` — fetch the current pending login code for an anonymous number -- `toggle_login_codes(number, can_receive)` — enable or disable login code delivery for an anonymous number -- `terminate_sessions(number)` — terminate all active Telegram sessions for an anonymous number (two-step flow handled internally) -- `LoginCodeResult` and `TerminateSessionsResult` result types -- `AnonymousNumberError` exception — raised when a number is not owned or session termination fails +**Raw API** +- `FragmentClient.call(method, data, *, page_url)` — raw request to any Fragment API method +- `FRAGMENT_BASE_URL` constant — base URL shared across all page constants and headers **Examples** -- `examples/giveaway_stars.py` — Stars 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/anonymous_number.py` — login code fetch and session termination with `AnonymousNumberError` handling -- `examples/recharge_ads.py` — self-service Ads recharge with `ConfigurationError` / `WalletError` handling -- `examples/search_usernames.py` — username marketplace search with pagination -- `examples/search_numbers.py` — number marketplace search with pagination -- `examples/search_gifts.py` — gifts marketplace search with collection, attr, and pagination +- `examples/client/` — `wallet_info.py` (wallet info), `raw_api_call.py` (raw API call) +- `examples/numbers/` — `manage_number.py` (login code fetch, session termination) +- `examples/auctions/` — `search_usernames.py`, `search_numbers.py`, `search_gifts.py` (marketplace search with pagination) +- `examples/purchase/` — `send_stars.py`, `send_premium.py`, `topup_ton_balance.py`, `run_stars_giveaway.py`, `run_premium_giveaway.py`, `recharge_ads_balance.py` ### 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 -- 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.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 result types now expose a unified `amount` field (`months` and `stars` removed) +- `__repr__` includes the unit — `3 months`, `500 stars`, etc. +- `timestamp` removed from all result dataclasses +- All page URL constants built from `FRAGMENT_BASE_URL`; +- `TransactionError` includes an SSL hint; `DUPLICATE_SEQNO` variant auto-retried up to 2 times (2 s apart) +- Error messages rewritten: "what happened → why → what to do" --- diff --git a/examples/search_gifts.py b/examples/auctions/search_gifts.py similarity index 82% rename from examples/search_gifts.py rename to examples/auctions/search_gifts.py index a6d63a9..ec68a03 100644 --- a/examples/search_gifts.py +++ b/examples/auctions/search_gifts.py @@ -28,12 +28,16 @@ FILTER = "" # "", "auction", "sale", "sold" — or omit async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: - result: GiftsResult = await client.search_gifts(QUERY, collection=COLLECTION, sort=SORT, filter=FILTER) + result: GiftsResult = await client.search_gifts( + QUERY, collection=COLLECTION, sort=SORT, filter=FILTER + ) print(f"Found {len(result.items)} result(s):") for item in result.items: price = f"{item['price']} TON" if item["price"] else "n/a" - print(f" {item['name']:30s} {item['status'] or '':15s} {price:12s} {item['date'] or '—'}") + print( + f" {item['name']:30s} {item['status'] or '':15s} {price:12s} {item['date'] or '—'}" + ) if result.next_offset: print(f"\nMore results available — next page offset: {result.next_offset}") diff --git a/examples/search_numbers.py b/examples/auctions/search_numbers.py similarity index 73% rename from examples/search_numbers.py rename to examples/auctions/search_numbers.py index c24a18e..37e80fb 100644 --- a/examples/search_numbers.py +++ b/examples/auctions/search_numbers.py @@ -26,15 +26,21 @@ FILTER = "" # "", "auction", "sale", "sold" — or omit async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: - result: NumbersResult = await client.search_numbers(QUERY, sort=SORT, filter=FILTER) + result: NumbersResult = await client.search_numbers( + QUERY, sort=SORT, filter=FILTER + ) print(f"Found {len(result.items)} result(s):") for item in result.items: price = f"{item['price']} TON" if item["price"] else "n/a" - print(f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}") + print( + f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}" + ) if result.next_offset_id: - print(f"\nMore results available — next page offset: {result.next_offset_id}") + print( + f"\nMore results available — next page offset: {result.next_offset_id}" + ) if __name__ == "__main__": diff --git a/examples/search_usernames.py b/examples/auctions/search_usernames.py similarity index 77% rename from examples/search_usernames.py rename to examples/auctions/search_usernames.py index c139785..c71b6fe 100644 --- a/examples/search_usernames.py +++ b/examples/auctions/search_usernames.py @@ -26,15 +26,21 @@ FILTER = "auction" # "", "auction", "sale", "sold" — or omit async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: - result: UsernamesResult = await client.search_usernames(QUERY, sort=SORT, filter=FILTER) + result: UsernamesResult = await client.search_usernames( + QUERY, sort=SORT, filter=FILTER + ) print(f"Found {len(result.items)} result(s):") for item in result.items: price = f"{item['price']} TON" if item["price"] else "n/a" - print(f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}") + print( + f" {item['name']:20s} {item['status'] or '':15s} {price:10s} {item['date'] or '—'}" + ) if result.next_offset_id: - print(f"\nMore results available — next page offset: {result.next_offset_id}") + print( + f"\nMore results available — next page offset: {result.next_offset_id}" + ) if __name__ == "__main__": diff --git a/examples/call.py b/examples/client/raw_api_call.py similarity index 100% rename from examples/call.py rename to examples/client/raw_api_call.py diff --git a/examples/get_wallet.py b/examples/client/wallet_info.py similarity index 100% rename from examples/get_wallet.py rename to examples/client/wallet_info.py diff --git a/examples/anonymous_number.py b/examples/numbers/manage_number.py similarity index 71% rename from examples/anonymous_number.py rename to examples/numbers/manage_number.py index 3355b22..e9f60a4 100644 --- a/examples/anonymous_number.py +++ b/examples/numbers/manage_number.py @@ -27,14 +27,21 @@ async def main() -> None: # Fetch the latest login code result = await client.get_login_code(NUMBER) if result.code: - print(f"Login code for {result.number}: {result.code} ({result.active_sessions} active session(s))") + print( + f"Login code for {result.number}: {result.code} ({result.active_sessions} active session(s))" + ) else: - print(f"No pending login code for {result.number} ({result.active_sessions} active session(s))") + print( + f"No pending login code for {result.number} ({result.active_sessions} active session(s))" + ) # Terminate all active sessions try: terminated = await client.terminate_sessions(NUMBER) - print(f"Sessions terminated for {terminated.number}" + (f": {terminated.message}" if terminated.message else "")) + print( + f"Sessions terminated for {terminated.number}" + + (f": {terminated.message}" if terminated.message else "") + ) except AnonymousNumberError as e: print(f"Could not terminate sessions: {e}") diff --git a/examples/recharge_ads.py b/examples/purchase/recharge_ads_balance.py similarity index 78% rename from examples/recharge_ads.py rename to examples/purchase/recharge_ads_balance.py index 50d0a44..3b02de2 100644 --- a/examples/recharge_ads.py +++ b/examples/purchase/recharge_ads_balance.py @@ -7,7 +7,12 @@ Your wallet must hold at least the recharge amount + ~0.056 TON for gas. import asyncio -from pyfragment import AdsRechargeResult, ConfigurationError, FragmentClient, WalletError +from pyfragment import ( + AdsRechargeResult, + ConfigurationError, + FragmentClient, + WalletError, +) SEED = "word1 word2 ... word24" API_KEY = "YOUR_TONAPI_KEY" @@ -25,7 +30,9 @@ 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) + result: AdsRechargeResult = await client.recharge_ads( + ACCOUNT, amount=AMOUNT + ) except WalletError as e: print(f"Wallet error — insufficient balance or misconfiguration: {e}") return @@ -33,7 +40,9 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.amount} TON recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}") + print( + f"{result.amount} TON recharged to Ads account {ACCOUNT} | tx: {result.transaction_id}" + ) if __name__ == "__main__": diff --git a/examples/giveaway_premium.py b/examples/purchase/run_premium_giveaway.py similarity index 80% rename from examples/giveaway_premium.py rename to examples/purchase/run_premium_giveaway.py index e2a5818..3a88f26 100644 --- a/examples/giveaway_premium.py +++ b/examples/purchase/run_premium_giveaway.py @@ -26,9 +26,13 @@ MONTHS = 3 # 3, 6 or 12 async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: try: - result = await client.giveaway_premium(CHANNEL, winners=WINNERS, months=MONTHS) + result = await client.giveaway_premium( + CHANNEL, winners=WINNERS, months=MONTHS + ) except UserNotFoundError: - print(f"Channel {CHANNEL} was not found on fragment.com — check the username and try again.") + print( + f"Channel {CHANNEL} was not found on fragment.com — check the username and try again." + ) return except ConfigurationError as e: print(f"Invalid argument: {e}") diff --git a/examples/giveaway_stars.py b/examples/purchase/run_stars_giveaway.py similarity index 80% rename from examples/giveaway_stars.py rename to examples/purchase/run_stars_giveaway.py index 400a707..19627d3 100644 --- a/examples/giveaway_stars.py +++ b/examples/purchase/run_stars_giveaway.py @@ -26,9 +26,13 @@ AMOUNT = 1000 # 500–1 000 000 stars per winner async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: try: - result = await client.giveaway_stars(CHANNEL, winners=WINNERS, amount=AMOUNT) + result = await client.giveaway_stars( + CHANNEL, winners=WINNERS, amount=AMOUNT + ) except UserNotFoundError: - print(f"Channel {CHANNEL} was not found on fragment.com — check the username and try again.") + print( + f"Channel {CHANNEL} was not found on fragment.com — check the username and try again." + ) return except ConfigurationError as e: print(f"Invalid argument: {e}") diff --git a/examples/purchase_premium.py b/examples/purchase/send_premium.py similarity index 68% rename from examples/purchase_premium.py rename to examples/purchase/send_premium.py index fc90f3b..a2d571e 100644 --- a/examples/purchase_premium.py +++ b/examples/purchase/send_premium.py @@ -25,15 +25,21 @@ MONTHS = 3 # 3, 6 or 12 async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: try: - result = await client.purchase_premium(USERNAME, months=MONTHS, show_sender=True) + result = await client.purchase_premium( + USERNAME, months=MONTHS, show_sender=True + ) except UserNotFoundError: - print(f"User {USERNAME} was not found on fragment.com — check the username and try again.") + print( + f"User {USERNAME} was not found on fragment.com — check the username and try again." + ) return except ConfigurationError as e: print(f"Invalid argument: {e}") return - print(f"{result.amount} 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/send_stars.py similarity index 69% rename from examples/purchase_stars.py rename to examples/purchase/send_stars.py index 594f883..5106732 100644 --- a/examples/purchase_stars.py +++ b/examples/purchase/send_stars.py @@ -25,15 +25,21 @@ AMOUNT = 500 # 50–1 000 000 stars async def main() -> None: async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client: try: - result = await client.purchase_stars(USERNAME, amount=AMOUNT, show_sender=True) + result = await client.purchase_stars( + USERNAME, amount=AMOUNT, show_sender=True + ) except UserNotFoundError: - print(f"User {USERNAME} was not found on fragment.com — check the username and try again.") + print( + f"User {USERNAME} was not found on fragment.com — check the username and try again." + ) return except ConfigurationError as e: print(f"Invalid argument: {e}") return - print(f"{result.amount} 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/purchase/topup_ton_balance.py similarity index 75% rename from examples/topup_ton.py rename to examples/purchase/topup_ton_balance.py index e614d58..12ce171 100644 --- a/examples/topup_ton.py +++ b/examples/purchase/topup_ton_balance.py @@ -9,7 +9,12 @@ Your wallet must hold at least the topup amount + ~0.056 TON for gas. import asyncio -from pyfragment import ConfigurationError, FragmentClient, UserNotFoundError, WalletError +from pyfragment import ( + ConfigurationError, + FragmentClient, + UserNotFoundError, + WalletError, +) SEED = "word1 word2 ... word24" API_KEY = "YOUR_TONAPI_KEY" @@ -29,7 +34,9 @@ async def main() -> None: try: result = await client.topup_ton(USERNAME, amount=AMOUNT, show_sender=True) except UserNotFoundError: - print(f"User {USERNAME} was not found on fragment.com — check the username and try again.") + print( + f"User {USERNAME} was not found on fragment.com — check the username and try again." + ) return except WalletError as e: print(f"Wallet error — insufficient balance or misconfiguration: {e}") @@ -38,7 +45,9 @@ async def main() -> None: print(f"Invalid argument: {e}") return - print(f"{result.amount} TON successfully topped up for {result.username} | tx: {result.transaction_id}") + print( + f"{result.amount} TON successfully topped up for {result.username} | tx: {result.transaction_id}" + ) if __name__ == "__main__":