mirror of
https://github.com/bohd4nx/FragmentAPI.git
synced 2026-07-25 06:14:29 +00:00
feat: implement client.call() method for raw Fragment API requests; add example and unit tests
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
Example: send a raw request to any Fragment API method.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
page_url must match the Fragment page the method belongs to — Fragment
|
||||||
|
requires a hash derived from each specific page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pyfragment import FragmentClient
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD = "anyFragmentMethod" # replace with the actual method name
|
||||||
|
DATA = {"key": "value"} # replace with the actual request payload
|
||||||
|
PAGE_URL = "https://fragment.com/stars/buy" # replace with the matching Fragment page
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
async with FragmentClient(seed=SEED, api_key=API_KEY, cookies=COOKIES) as client:
|
||||||
|
result = await client.call(METHOD, DATA, page_url=PAGE_URL)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
+41
-2
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from typing import cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
from pyfragment.methods.giveaway_premium import giveaway_premium
|
from pyfragment.methods.giveaway_premium import giveaway_premium
|
||||||
from pyfragment.methods.giveaway_stars import giveaway_stars
|
from pyfragment.methods.giveaway_stars import giveaway_stars
|
||||||
@@ -14,8 +16,15 @@ from pyfragment.types import (
|
|||||||
StarsResult,
|
StarsResult,
|
||||||
WalletInfo,
|
WalletInfo,
|
||||||
)
|
)
|
||||||
from pyfragment.types.constants import DEFAULT_TIMEOUT, REQUIRED_COOKIE_KEYS, SUPPORTED_WALLET_VERSIONS, WalletVersion
|
from pyfragment.types.constants import (
|
||||||
|
DEFAULT_TIMEOUT,
|
||||||
|
FRAGMENT_BASE_URL,
|
||||||
|
REQUIRED_COOKIE_KEYS,
|
||||||
|
SUPPORTED_WALLET_VERSIONS,
|
||||||
|
WalletVersion,
|
||||||
|
)
|
||||||
from pyfragment.types.results import PremiumGiveawayResult, StarsGiveawayResult
|
from pyfragment.types.results import PremiumGiveawayResult, StarsGiveawayResult
|
||||||
|
from pyfragment.utils.http import fragment_request, get_fragment_hash, make_headers
|
||||||
from pyfragment.utils.wallet import get_wallet_info
|
from pyfragment.utils.wallet import get_wallet_info
|
||||||
|
|
||||||
|
|
||||||
@@ -187,3 +196,33 @@ class FragmentClient:
|
|||||||
``winners``, and ``amount``.
|
``winners``, and ``amount``.
|
||||||
"""
|
"""
|
||||||
return await giveaway_premium(self, channel, winners, months)
|
return await giveaway_premium(self, channel, winners, months)
|
||||||
|
|
||||||
|
async def call(
|
||||||
|
self, method: str, data: dict[str, Any] | None = None, *, page_url: str = FRAGMENT_BASE_URL
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Send a raw request to the Fragment API.
|
||||||
|
|
||||||
|
Useful for accessing undocumented or future Fragment API methods
|
||||||
|
without waiting for a library update.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: Fragment API method name, e.g. ``"searchPremiumGiftRecipient"``.
|
||||||
|
data: Additional form-data fields to include in the request body.
|
||||||
|
page_url: Fragment page URL used to derive the API hash and headers.
|
||||||
|
Defaults to ``FRAGMENT_BASE_URL`` (``"https://fragment.com"``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Raw parsed JSON response as a dict.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
result = await client.call(
|
||||||
|
"searchPremiumGiftRecipient",
|
||||||
|
{"query": "@username", "months": 3},
|
||||||
|
page_url="https://fragment.com/premium/gift",
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
headers = make_headers(page_url)
|
||||||
|
async with httpx.AsyncClient(cookies=self.cookies, timeout=self.timeout) as session:
|
||||||
|
fragment_hash = await get_fragment_hash(self.cookies, headers, page_url, self.timeout)
|
||||||
|
return await fragment_request(session, fragment_hash, headers, {"method": method, **(data or {})})
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ DEFAULT_TIMEOUT: float = 30.0
|
|||||||
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
|
REQUIRED_COOKIE_KEYS: tuple[str, ...] = ("stel_ssid", "stel_dt", "stel_token", "stel_ton_token")
|
||||||
|
|
||||||
# Fragment page URLs
|
# Fragment page URLs
|
||||||
STARS_PAGE: str = "https://fragment.com/stars/buy"
|
FRAGMENT_BASE_URL: str = "https://fragment.com"
|
||||||
STARS_GIVEAWAY_PAGE: str = "https://fragment.com/stars/giveaway"
|
STARS_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/buy"
|
||||||
PREMIUM_PAGE: str = "https://fragment.com/premium/gift"
|
STARS_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/stars/giveaway"
|
||||||
PREMIUM_GIVEAWAY_PAGE: str = "https://fragment.com/premium/giveaway"
|
PREMIUM_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/gift"
|
||||||
TON_PAGE: str = "https://fragment.com/ads/topup"
|
PREMIUM_GIVEAWAY_PAGE: str = f"{FRAGMENT_BASE_URL}/premium/giveaway"
|
||||||
|
TON_PAGE: str = f"{FRAGMENT_BASE_URL}/ads/topup"
|
||||||
|
|
||||||
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
# Tonkeeper device fingerprint — serialized once, reused in every tx_data payload.
|
||||||
DEVICE: str = json.dumps(
|
DEVICE: str = json.dumps(
|
||||||
@@ -47,7 +48,7 @@ BASE_HEADERS: dict[str, str] = {
|
|||||||
"accept": "application/json, text/javascript, */*; q=0.01",
|
"accept": "application/json, text/javascript, */*; q=0.01",
|
||||||
"accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7",
|
"accept-language": "en-US,en;q=0.9,uk;q=0.8,ru;q=0.7",
|
||||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
"origin": "https://fragment.com",
|
"origin": FRAGMENT_BASE_URL,
|
||||||
"priority": "u=1, i",
|
"priority": "u=1, i",
|
||||||
"sec-fetch-dest": "empty",
|
"sec-fetch-dest": "empty",
|
||||||
"sec-fetch-mode": "cors",
|
"sec-fetch-mode": "cors",
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from typing import Any
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from pyfragment.types import FragmentPageError, ParseError, VerificationError
|
from pyfragment.types import FragmentPageError, ParseError, VerificationError
|
||||||
from pyfragment.types.constants import BASE_HEADERS, DEFAULT_TIMEOUT
|
from pyfragment.types.constants import BASE_HEADERS, DEFAULT_TIMEOUT, FRAGMENT_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
def make_headers(page_url: str) -> dict[str, str]:
|
def make_headers(page_url: str = FRAGMENT_BASE_URL) -> dict[str, str]:
|
||||||
return {**BASE_HEADERS, "referer": page_url, "x-aj-referer": page_url}
|
return {**BASE_HEADERS, "referer": page_url, "x-aj-referer": page_url}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ async def get_fragment_hash(
|
|||||||
page_headers.update(
|
page_headers.update(
|
||||||
{
|
{
|
||||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"referer": "https://fragment.com/",
|
"referer": f"{FRAGMENT_BASE_URL}/",
|
||||||
"sec-fetch-dest": "document",
|
"sec-fetch-dest": "document",
|
||||||
"sec-fetch-mode": "navigate",
|
"sec-fetch-mode": "navigate",
|
||||||
"upgrade-insecure-requests": "1",
|
"upgrade-insecure-requests": "1",
|
||||||
@@ -105,7 +105,7 @@ async def fragment_request(
|
|||||||
Parsed API response as a dict.
|
Parsed API response as a dict.
|
||||||
"""
|
"""
|
||||||
resp = await session.post(
|
resp = await session.post(
|
||||||
f"https://fragment.com/api?hash={fragment_hash}",
|
f"{FRAGMENT_BASE_URL}/api?hash={fragment_hash}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Unit tests for client.call() — raw Fragment API request."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyfragment import FragmentClient
|
||||||
|
from tests.shared import FAKE_HASH
|
||||||
|
|
||||||
|
FAKE_RESPONSE = {"status": "ok", "data": {"value": 42}}
|
||||||
|
|
||||||
|
|
||||||
|
# client.call() mocked tests
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_call_returns_api_response(client: FragmentClient) -> None:
|
||||||
|
with (
|
||||||
|
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||||
|
patch("pyfragment.client.fragment_request", AsyncMock(return_value=FAKE_RESPONSE)),
|
||||||
|
):
|
||||||
|
result = await client.call("anyMethod", {"key": "value"})
|
||||||
|
|
||||||
|
assert result == FAKE_RESPONSE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_call_default_page_url(client: FragmentClient) -> None:
|
||||||
|
"""call() works without explicitly passing page_url (defaults to FRAGMENT_BASE_URL)."""
|
||||||
|
with (
|
||||||
|
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||||
|
patch("pyfragment.client.fragment_request", AsyncMock(return_value=FAKE_RESPONSE)),
|
||||||
|
):
|
||||||
|
result = await client.call("anyMethod")
|
||||||
|
|
||||||
|
assert result == FAKE_RESPONSE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_call_no_data(client: FragmentClient) -> None:
|
||||||
|
"""call() with no extra data passes only the method field."""
|
||||||
|
mock_request = AsyncMock(return_value={})
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||||
|
patch("pyfragment.client.fragment_request", mock_request),
|
||||||
|
):
|
||||||
|
await client.call("anyMethod")
|
||||||
|
|
||||||
|
_, _, _, sent_data = mock_request.call_args.args
|
||||||
|
assert sent_data == {"method": "anyMethod"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_call_merges_extra_data(client: FragmentClient) -> None:
|
||||||
|
"""call() merges caller-supplied data with the method field."""
|
||||||
|
mock_request = AsyncMock(return_value={})
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("pyfragment.client.get_fragment_hash", AsyncMock(return_value=FAKE_HASH)),
|
||||||
|
patch("pyfragment.client.fragment_request", mock_request),
|
||||||
|
):
|
||||||
|
await client.call("anyMethod", {"key": "value", "num": 7})
|
||||||
|
|
||||||
|
_, _, _, sent_data = mock_request.call_args.args
|
||||||
|
assert sent_data == {"method": "anyMethod", "key": "value", "num": 7}
|
||||||
Reference in New Issue
Block a user