refactor: restructure as installable PyPI package

- Rename app/ → fragmentapi/ for proper package naming
- Add FragmentClient class with gift_premium, gift_stars, topup_ton methods
- Restructure core/ → types/ (exceptions, results, constants)
- Merge utils/hash.py into utils/client.py
- Replace _version.py with importlib.metadata
- Add input validation with min/max bounds
- Add unit tests: decode, client init/cookies
- Rename 002_test_hash → 003_test_hash
- Clean up pyproject.toml: pin deps, production classifiers
This commit is contained in:
bohd4nx
2026-03-15 21:03:03 +02:00
parent 4e017cb7a1
commit d2d046a2b9
38 changed files with 1051 additions and 959 deletions
+5
View File
@@ -0,0 +1,5 @@
from fragmentapi.methods.premium import gift_premium
from fragmentapi.methods.stars import gift_stars
from fragmentapi.methods.ton import topup_ton
__all__ = ["gift_premium", "gift_stars", "topup_ton"]
+119
View File
@@ -0,0 +1,119 @@
import json
import time
from typing import TYPE_CHECKING
import httpx
from fragmentapi.types import (
BASE_HEADERS,
DEVICE,
PREMIUM_PAGE,
ConfigError,
FragmentAPIError,
FragmentError,
PremiumResult,
UnexpectedError,
UserNotFoundError,
)
from fragmentapi.utils import (
execute_transaction_request,
get_account_info,
get_fragment_hash,
parse_json_response,
process_transaction,
)
if TYPE_CHECKING:
from fragmentapi.client import FragmentClient
# Page-specific headers
HEADERS: dict[str, str] = {
**BASE_HEADERS,
"referer": PREMIUM_PAGE,
"x-aj-referer": PREMIUM_PAGE,
}
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
months: int,
) -> str:
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={
"query": username,
"months": months,
"method": "searchPremiumGiftRecipient",
},
)
result = parse_json_response(resp, "searchPremiumGiftRecipient")
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
months: int,
) -> str:
await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={
"mode": "new",
"lv": "false",
"dh": str(int(time.time())),
"method": "updatePremiumState",
},
)
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={
"recipient": recipient,
"months": months,
"method": "initGiftPremiumRequest",
},
)
result = parse_json_response(resp, "initGiftPremiumRequest")
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Premium purchase"))
return req_id
async def gift_premium(client: "FragmentClient", username: str, months: int, show_sender: bool = True) -> PremiumResult:
if months not in (3, 6, 12):
raise ConfigError(ConfigError.INVALID_MONTHS)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, PREMIUM_PAGE)
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies) as session:
recipient = await _search_recipient(session, fragment_hash, username, months)
req_id = await _init_request(session, fragment_hash, recipient, months)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getGiftPremiumLink",
}
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)
except FragmentError:
raise
except Exception as exc:
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+103
View File
@@ -0,0 +1,103 @@
import json
from typing import TYPE_CHECKING
import httpx
from fragmentapi.types import (
BASE_HEADERS,
DEVICE,
STARS_PAGE,
ConfigError,
FragmentAPIError,
FragmentError,
StarsResult,
UnexpectedError,
UserNotFoundError,
)
from fragmentapi.utils import (
execute_transaction_request,
get_account_info,
get_fragment_hash,
parse_json_response,
process_transaction,
)
if TYPE_CHECKING:
from fragmentapi.client import FragmentClient
# Page-specific headers
HEADERS: dict[str, str] = {
**BASE_HEADERS,
"referer": STARS_PAGE,
"x-aj-referer": STARS_PAGE,
}
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
) -> str:
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={"query": username, "quantity": "", "method": "searchStarsRecipient"},
)
result = parse_json_response(resp, "searchStarsRecipient")
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
amount: int,
) -> str:
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={
"recipient": recipient,
"quantity": amount,
"method": "initBuyStarsRequest",
},
)
result = parse_json_response(resp, "initBuyStarsRequest")
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="Stars purchase"))
return req_id
async def gift_stars(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> StarsResult:
if not isinstance(amount, int) or not (50 <= amount <= 1_000_000):
raise ConfigError(ConfigError.INVALID_STARS_AMOUNT)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, STARS_PAGE)
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies) as session:
recipient = await _search_recipient(session, fragment_hash, username)
req_id = await _init_request(session, fragment_hash, recipient, amount)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getBuyStarsLink",
}
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)
except FragmentError:
raise
except Exception as exc:
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc
+108
View File
@@ -0,0 +1,108 @@
import json
from typing import TYPE_CHECKING
import httpx
from fragmentapi.types import (
BASE_HEADERS,
DEVICE,
TON_PAGE,
AdsTopupResult,
ConfigError,
FragmentAPIError,
FragmentError,
UnexpectedError,
UserNotFoundError,
)
from fragmentapi.utils import (
execute_transaction_request,
get_account_info,
get_fragment_hash,
parse_json_response,
process_transaction,
)
if TYPE_CHECKING:
from fragmentapi.client import FragmentClient
# Page-specific headers
HEADERS: dict[str, str] = {
**BASE_HEADERS,
"referer": TON_PAGE,
"x-aj-referer": TON_PAGE,
}
async def _search_recipient(
session: httpx.AsyncClient,
fragment_hash: str,
username: str,
) -> str:
await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={"mode": "new", "method": "updateAdsTopupState"},
)
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={"query": username, "method": "searchAdsTopupRecipient"},
)
result = parse_json_response(resp, "searchAdsTopupRecipient")
recipient = result.get("found", {}).get("recipient")
if not recipient:
raise UserNotFoundError(UserNotFoundError.NOT_FOUND.format(username=username))
return recipient
async def _init_request(
session: httpx.AsyncClient,
fragment_hash: str,
recipient: str,
amount: int,
) -> str:
resp = await session.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=HEADERS,
data={
"recipient": recipient,
"amount": amount,
"method": "initAdsTopupRequest",
},
)
result = parse_json_response(resp, "initAdsTopupRequest")
req_id = result.get("req_id")
if not req_id:
raise FragmentAPIError(FragmentAPIError.NO_REQUEST_ID.format(context="TON topup"))
return req_id
async def topup_ton(client: "FragmentClient", username: str, amount: int, show_sender: bool = True) -> AdsTopupResult:
if not isinstance(amount, int) or not (1 <= amount <= 1_000_000_000):
raise ConfigError(ConfigError.INVALID_TON_AMOUNT)
try:
fragment_hash = await get_fragment_hash(client.cookies, HEADERS, TON_PAGE)
account = await get_account_info(client)
async with httpx.AsyncClient(cookies=client.cookies) as session:
recipient = await _search_recipient(session, fragment_hash, username)
req_id = await _init_request(session, fragment_hash, recipient, amount)
tx_data = {
"account": json.dumps(account),
"device": DEVICE,
"transaction": 1,
"id": req_id,
"show_sender": int(show_sender),
"method": "getAdsTopupLink",
}
transaction = await execute_transaction_request(session, HEADERS, tx_data, fragment_hash)
tx_hash = await process_transaction(client, transaction)
return AdsTopupResult(transaction_id=tx_hash, username=username, amount=amount)
except FragmentError:
raise
except Exception as exc:
raise UnexpectedError(UnexpectedError.UNEXPECTED.format(exc=exc)) from exc