adsun 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
adsun/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Adsun — Telegram ad network SDK for Python.
2
+
3
+ Docs: https://adsun.io/docs · Bot monetization: https://adsun.io/telegram-bot-monetization
4
+
5
+ from adsun import AdsunBot
6
+ ads = AdsunBot(token="YOUR_ACCOUNT_TOKEN", block_id="50001")
7
+ ad = ads.get_ad(user_id=123456789, language="en") # None when no ad right now
8
+ """
9
+ from .client import ADSUN_API_URL, AdsunBot, AsyncAdsunBot
10
+ from .errors import AdsunError, BadRequest, BlockNotActive, InvalidToken
11
+ from .models import BotAd
12
+ from .reward import RewardCallback, parse_reward_callback, verify_secret
13
+
14
+ __version__ = "1.0.0"
15
+ __all__ = [
16
+ "ADSUN_API_URL", "AdsunBot", "AsyncAdsunBot", "BotAd",
17
+ "AdsunError", "BadRequest", "BlockNotActive", "InvalidToken",
18
+ "RewardCallback", "parse_reward_callback", "verify_secret",
19
+ ]
adsun/client.py ADDED
@@ -0,0 +1,99 @@
1
+ """HTTP clients for the Adsun bot ads API (``GET /advbot``)."""
2
+ import json
3
+ import re
4
+ from typing import Any, Dict, Optional, Union
5
+ from urllib.error import HTTPError, URLError
6
+ from urllib.parse import urlencode
7
+ from urllib.request import Request, urlopen
8
+
9
+ from .errors import AdsunError, BadRequest, InvalidToken, error_for
10
+ from .models import BotAd
11
+
12
+ ADSUN_API_URL = "https://api.adsun.io"
13
+ _UA = "adsun-python/1.0.0"
14
+
15
+
16
+ def block_number(block_id: Union[str, int]) -> str:
17
+ """``"bot-50001"`` / ``"tw-50001"`` / ``50001`` → ``"50001"`` (the API wants the numeric part)."""
18
+ num = re.sub(r"^(?:bot-|tw-)", "", str(block_id).strip(), flags=re.I)
19
+ if not num.isdigit():
20
+ raise BadRequest("block_id must be numeric, e.g. 50001 (got {!r})".format(block_id), 400)
21
+ return num
22
+
23
+
24
+ class _Base:
25
+ def __init__(self, token: str, block_id: Union[str, int], api_url: str = ADSUN_API_URL, timeout: float = 10.0):
26
+ if not token:
27
+ raise InvalidToken("token is required (Adsun panel → Profile → Token)", 401)
28
+ self.token = token
29
+ self.block_id = block_number(block_id)
30
+ self.api_url = api_url.rstrip("/")
31
+ self.timeout = timeout
32
+
33
+ def _params(self, user_id: Union[int, str], language: str) -> Dict[str, str]:
34
+ uid = str(user_id)
35
+ if not uid.isdigit():
36
+ raise BadRequest("user_id must be the numeric Telegram user id", 400)
37
+ return {"tgid": uid, "blockid": self.block_id, "language": language or "", "token": self.token}
38
+
39
+ @staticmethod
40
+ def _message(body: bytes) -> str:
41
+ try:
42
+ return str(json.loads(body.decode("utf-8")).get("error") or "")
43
+ except Exception:
44
+ return body.decode("utf-8", "replace")[:200]
45
+
46
+
47
+ class AdsunBot(_Base):
48
+ """Synchronous client (standard library only).
49
+
50
+ >>> ads = AdsunBot(token="...", block_id="50001")
51
+ >>> ad = ads.get_ad(user_id=123456789, language="en")
52
+ """
53
+
54
+ def get_ad(self, user_id: Union[int, str], language: str = "") -> Optional[BotAd]:
55
+ """Returns a :class:`BotAd`, or ``None`` when there is no ad right now (HTTP 204)."""
56
+ url = "{}/advbot?{}".format(self.api_url, urlencode(self._params(user_id, language)))
57
+ req = Request(url, headers={"User-Agent": _UA, "Accept": "application/json"})
58
+ try:
59
+ with urlopen(req, timeout=self.timeout) as r: # noqa: S310 (fixed https API)
60
+ if r.status == 204:
61
+ return None
62
+ body = r.read()
63
+ if not body:
64
+ return None
65
+ return BotAd.from_dict(json.loads(body.decode("utf-8")))
66
+ except HTTPError as e:
67
+ raise error_for(e.code, self._message(e.read() or b""))
68
+ except URLError as e:
69
+ raise AdsunError("network error: {}".format(e.reason), 0)
70
+
71
+
72
+ class AsyncAdsunBot(_Base):
73
+ """asyncio client (needs ``pip install adsun[async]``). Pass your own ``aiohttp.ClientSession`` to reuse it."""
74
+
75
+ def __init__(self, token: str, block_id: Union[str, int], api_url: str = ADSUN_API_URL, timeout: float = 10.0, session: Any = None):
76
+ super().__init__(token, block_id, api_url, timeout)
77
+ self._session = session
78
+
79
+ async def get_ad(self, user_id: Union[int, str], language: str = "") -> Optional[BotAd]:
80
+ try:
81
+ import aiohttp
82
+ except ImportError as e: # pragma: no cover
83
+ raise AdsunError("AsyncAdsunBot needs aiohttp: pip install 'adsun[async]'") from e
84
+ params = self._params(user_id, language)
85
+ own = self._session is None
86
+ session = self._session or aiohttp.ClientSession(headers={"User-Agent": _UA})
87
+ try:
88
+ async with session.get(self.api_url + "/advbot", params=params, timeout=aiohttp.ClientTimeout(total=self.timeout)) as r:
89
+ if r.status == 204:
90
+ return None
91
+ body = await r.read()
92
+ if r.status >= 400:
93
+ raise error_for(r.status, self._message(body))
94
+ return BotAd.from_dict(json.loads(body.decode("utf-8"))) if body else None
95
+ except aiohttp.ClientError as e:
96
+ raise AdsunError("network error: {}".format(e), 0)
97
+ finally:
98
+ if own:
99
+ await session.close()
adsun/errors.py ADDED
@@ -0,0 +1,23 @@
1
+ class AdsunError(Exception):
2
+ """Base error. ``status`` is the HTTP status from the Adsun API (0 for network errors)."""
3
+
4
+ def __init__(self, message: str, status: int = 0):
5
+ super().__init__(message)
6
+ self.status = status
7
+
8
+
9
+ class BadRequest(AdsunError):
10
+ """400 — e.g. blockid must be the numeric part only (``50001``, not ``bot-50001``)."""
11
+
12
+
13
+ class InvalidToken(AdsunError):
14
+ """401 — account token is wrong or was regenerated (Adsun panel → Profile → Token)."""
15
+
16
+
17
+ class BlockNotActive(AdsunError):
18
+ """404 — the block does not exist, is not active or belongs to another account."""
19
+
20
+
21
+ def error_for(status: int, message: str) -> AdsunError:
22
+ cls = {400: BadRequest, 401: InvalidToken, 404: BlockNotActive}.get(status, AdsunError)
23
+ return cls(message or "Adsun API error {}".format(status), status)
File without changes
@@ -0,0 +1,21 @@
1
+ """aiogram 3 helper: ``await send_ad(bot, chat_id, ads, user_id, language)``."""
2
+ from typing import Optional
3
+
4
+ from ..client import AsyncAdsunBot
5
+ from ..models import BotAd
6
+
7
+
8
+ async def send_ad(bot, chat_id: int, ads: AsyncAdsunBot, user_id: int, language: str = "") -> Optional[BotAd]:
9
+ """Fetches an Adsun ad and sends it. Returns the ad, or ``None`` when there was no ad."""
10
+ from aiogram.enums import ParseMode
11
+ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
12
+
13
+ ad = await ads.get_ad(user_id=user_id, language=language)
14
+ if ad is None:
15
+ return None
16
+ kb = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text=t, url=u) for t, u in row] for row in ad.buttons()])
17
+ if ad.image_url:
18
+ await bot.send_photo(chat_id, photo=ad.image_url, caption=ad.text_html, parse_mode=ParseMode.HTML, reply_markup=kb, protect_content=True)
19
+ else:
20
+ await bot.send_message(chat_id, ad.text_html, parse_mode=ParseMode.HTML, reply_markup=kb, protect_content=True)
21
+ return ad
@@ -0,0 +1,20 @@
1
+ """python-telegram-bot (v20+) helper: ``await send_ad(context.bot, chat_id, ads, user_id, language)``."""
2
+ from typing import Optional
3
+
4
+ from ..client import AsyncAdsunBot
5
+ from ..models import BotAd
6
+
7
+
8
+ async def send_ad(bot, chat_id: int, ads: AsyncAdsunBot, user_id: int, language: str = "") -> Optional[BotAd]:
9
+ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
10
+ from telegram.constants import ParseMode
11
+
12
+ ad = await ads.get_ad(user_id=user_id, language=language)
13
+ if ad is None:
14
+ return None
15
+ kb = InlineKeyboardMarkup([[InlineKeyboardButton(t, url=u) for t, u in row] for row in ad.buttons()])
16
+ if ad.image_url:
17
+ await bot.send_photo(chat_id, photo=ad.image_url, caption=ad.text_html, parse_mode=ParseMode.HTML, reply_markup=kb, protect_content=True)
18
+ else:
19
+ await bot.send_message(chat_id, ad.text_html, parse_mode=ParseMode.HTML, reply_markup=kb, protect_content=True)
20
+ return ad
adsun/models.py ADDED
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict, Optional
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class BotAd:
7
+ """Sponsored bot message returned by ``GET /advbot``.
8
+
9
+ Send it with ``parse_mode="HTML"`` and ``protect_content=True`` (ads must not be forwardable).
10
+ """
11
+
12
+ text_html: str
13
+ click_url: str
14
+ button_name: str
15
+ image_url: Optional[str] = None
16
+ button_reward_name: Optional[str] = None
17
+ reward_url: Optional[str] = None
18
+ raw: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
19
+
20
+ @classmethod
21
+ def from_dict(cls, data: Dict[str, Any]) -> "BotAd":
22
+ return cls(
23
+ text_html=str(data.get("text_html") or ""),
24
+ click_url=str(data.get("click_url") or ""),
25
+ button_name=str(data.get("button_name") or "Open"),
26
+ image_url=data.get("image_url") or None,
27
+ button_reward_name=data.get("button_reward_name") or None,
28
+ reward_url=data.get("reward_url") or None,
29
+ raw=dict(data),
30
+ )
31
+
32
+ def buttons(self):
33
+ """Inline keyboard rows as ``[(text, url), ...]`` — framework independent."""
34
+ rows = [[(self.button_name, self.click_url)]]
35
+ if self.button_reward_name and self.reward_url:
36
+ rows.append([(self.button_reward_name, self.reward_url)])
37
+ return rows
adsun/py.typed ADDED
File without changes
adsun/reward.py ADDED
@@ -0,0 +1,54 @@
1
+ """Server-side helpers for the Adsun Reward URL callback.
2
+
3
+ In the Adsun panel set the block's Reward URL, for example::
4
+
5
+ https://your-app.com/adsun/reward?userid=[userId]&block=[blockId]&secret=YOUR_LONG_RANDOM_SECRET
6
+
7
+ Adsun calls it (HTTP GET) only after a server-verified full watch (Reward) or task completion (Task).
8
+ ``[userId]`` / ``[blockId]`` are replaced; ``user`` and ``block`` params are added too.
9
+ Failed calls (5xx / network) are retried, so handle them idempotently. Never grant rewards from the
10
+ client-side ``.then()`` alone — grant them here.
11
+ """
12
+ import hmac
13
+ from dataclasses import dataclass
14
+ from typing import Mapping, Optional, Sequence, Union
15
+
16
+ QueryValue = Union[str, Sequence[str]]
17
+
18
+
19
+ def _first(q: Mapping[str, QueryValue], *names: str) -> str:
20
+ for n in names:
21
+ v = q.get(n)
22
+ if v is None:
23
+ continue
24
+ if not isinstance(v, str):
25
+ v = v[0] if len(v) else ""
26
+ if v:
27
+ return str(v)
28
+ return ""
29
+
30
+
31
+ def verify_secret(received: Optional[str], expected: str) -> bool:
32
+ """Constant-time comparison of the ``secret`` you put in your Reward URL."""
33
+ if not expected or not received:
34
+ return False
35
+ return hmac.compare_digest(str(received).encode(), str(expected).encode())
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RewardCallback:
40
+ user_id: str
41
+ block_id: str
42
+
43
+
44
+ def parse_reward_callback(query: Mapping[str, QueryValue], secret: Optional[str] = None, secret_param: str = "secret") -> Optional[RewardCallback]:
45
+ """Parse a Reward URL request. Works with dicts from Flask, FastAPI, Django or ``urllib.parse.parse_qs``.
46
+
47
+ Returns ``None`` when the user id is missing or (if ``secret`` is given) the secret does not match.
48
+ """
49
+ if secret is not None and not verify_secret(_first(query, secret_param), secret):
50
+ return None
51
+ user = _first(query, "userid", "userId", "user", "user_id")
52
+ if not user.isdigit():
53
+ return None
54
+ return RewardCallback(user_id=user, block_id=_first(query, "block", "blockId", "blockid", "block_id"))
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.5
2
+ Name: adsun
3
+ Version: 1.0.0
4
+ Summary: Official Adsun Python SDK: monetize Telegram bots with sponsored ads (aiogram, python-telegram-bot) and verify Telegram Mini App reward callbacks.
5
+ Project-URL: Homepage, https://adsun.io
6
+ Project-URL: Documentation, https://adsun.io/docs
7
+ Project-URL: Source, https://github.com/adsun-io/adsun-sdk
8
+ Project-URL: Issues, https://github.com/adsun-io/adsun-sdk/issues
9
+ Project-URL: Bot monetization, https://adsun.io/telegram-bot-monetization
10
+ Author-email: Adsun <support@adsun.io>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: ad-network,adsgram,adsgram-alternative,adsun,aiogram,bot-monetization,python-telegram-bot,rewarded-ads,telegram,telegram-ad-network,telegram-ads,telegram-bot,telegram-mini-app,telegram-monetization,ton,usdt
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.8
25
+ Provides-Extra: aiogram
26
+ Requires-Dist: aiogram>=3.0; extra == 'aiogram'
27
+ Requires-Dist: aiohttp>=3.8; extra == 'aiogram'
28
+ Provides-Extra: async
29
+ Requires-Dist: aiohttp>=3.8; extra == 'async'
30
+ Provides-Extra: ptb
31
+ Requires-Dist: python-telegram-bot>=20.0; extra == 'ptb'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # adsun — Telegram bot ads for Python
35
+
36
+ Official [Adsun](https://adsun.io) Python SDK. **Monetize your Telegram bot** with sponsored messages and earn **USDT**,
37
+ and verify **Telegram Mini App reward callbacks** on your server. Zero required dependencies; helpers for
38
+ **aiogram 3** and **python-telegram-bot 20+**.
39
+
40
+ ```bash
41
+ pip install adsun # core (standard library only)
42
+ pip install "adsun[aiogram]" # + aiogram helper
43
+ pip install "adsun[ptb]" # + python-telegram-bot helper
44
+ ```
45
+
46
+ 1. Sign up at https://adsun.io/publishers, add your bot as a **Bot** platform and create a block.
47
+ 2. Copy your **account token** (Profile → Token) and the numeric **block id** (e.g. `50001`).
48
+
49
+ ## aiogram 3
50
+
51
+ ```python
52
+ from aiogram.filters import Command
53
+ from adsun import AsyncAdsunBot
54
+ from adsun.integrations.aiogram import send_ad
55
+
56
+ ads = AsyncAdsunBot(token="YOUR_ACCOUNT_TOKEN", block_id="50001")
57
+
58
+ @dp.message(Command("bonus"))
59
+ async def bonus(message):
60
+ ad = await send_ad(message.bot, message.chat.id, ads, message.from_user.id, message.from_user.language_code or "en")
61
+ if ad is None:
62
+ await message.answer("No offer right now")
63
+ ```
64
+
65
+ ## python-telegram-bot
66
+
67
+ ```python
68
+ from adsun import AsyncAdsunBot
69
+ from adsun.integrations.ptb import send_ad
70
+
71
+ ads = AsyncAdsunBot(token="YOUR_ACCOUNT_TOKEN", block_id="50001")
72
+
73
+ async def bonus(update, context):
74
+ u = update.effective_user
75
+ await send_ad(context.bot, update.effective_chat.id, ads, u.id, u.language_code or "en")
76
+ ```
77
+
78
+ ## Any framework (sync, no dependencies)
79
+
80
+ ```python
81
+ from adsun import AdsunBot
82
+
83
+ ads = AdsunBot(token="YOUR_ACCOUNT_TOKEN", block_id="50001")
84
+ ad = ads.get_ad(user_id=123456789, language="en") # None = no ad right now
85
+ if ad:
86
+ print(ad.text_html, ad.image_url, ad.buttons()) # [[(text, url)], [(reward_text, reward_url)]]
87
+ ```
88
+
89
+ Send it yourself with `parse_mode="HTML"` and `protect_content=True` (ad messages must not be forwardable).
90
+
91
+ Errors: `InvalidToken` (401), `BlockNotActive` (404), `BadRequest` (400), `AdsunError` (others / network).
92
+
93
+ ## Reward URL callback (Mini Apps and bots)
94
+
95
+ Set the block's Reward URL in the panel:
96
+ `https://your-app.com/adsun/reward?userid=[userId]&block=[blockId]&secret=LONG_RANDOM_SECRET`
97
+
98
+ ```python
99
+ from adsun import parse_reward_callback
100
+
101
+ @app.get("/adsun/reward") # Flask / FastAPI / Django — any dict-like query works
102
+ def reward():
103
+ cb = parse_reward_callback(request.args, secret=REWARD_SECRET)
104
+ if cb is None:
105
+ return "forbidden", 403
106
+ add_coins(cb.user_id) # make this idempotent: Adsun retries on 5xx / network errors
107
+ return "ok"
108
+ ```
109
+
110
+ ## HTTP API (what the SDK calls)
111
+
112
+ `GET https://api.adsun.io/advbot?tgid=<user id>&blockid=<number>&language=<code>&token=<account token>`
113
+ → `200 {text_html, click_url, button_name, image_url, button_reward_name, reward_url}` or `204` (no ad).
114
+
115
+ Links: https://adsun.io/docs · https://adsun.io/telegram-bot-monetization · support@adsun.io ·
116
+ [@ADSunSupport](https://t.me/ADSunSupport)
117
+
118
+ MIT License. Adsun is not affiliated with Telegram.
@@ -0,0 +1,13 @@
1
+ adsun/__init__.py,sha256=YdrJFVD2i_ARFnVV5HqEZqXppixv3wT0BDS_zYANQrc,784
2
+ adsun/client.py,sha256=aTX3AhJV889ZcbHMk0au6UXZ5Jp2QISsEqutFe_p1D8,4288
3
+ adsun/errors.py,sha256=p6WOi9-Ct77DTpmMD10_eICeIHwa7Q94rMxKQT03Flc,845
4
+ adsun/models.py,sha256=ONcJhmT_rxCFPt2QLecLo1Xk97-vGeCYagam98XkTuQ,1344
5
+ adsun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ adsun/reward.py,sha256=Y8VUKAfuukzVaARAHLDV9raBjshOO3v9CLhAHiqo0UI,2050
7
+ adsun/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ adsun/integrations/aiogram.py,sha256=jQk_YzMJQ7ANmK5S773JgpYHbXVdwH2Mki_dRI_uYa8,1032
9
+ adsun/integrations/ptb.py,sha256=4cz1Dl85pscUyEiL2CkZh6U9p62GF8RV1NJ1j3FthlY,942
10
+ adsun-1.0.0.dist-info/METADATA,sha256=E-4u9mLLultio_G5jTZNOxXzfZ8gZNBhPCoed8JLy04,4657
11
+ adsun-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
12
+ adsun-1.0.0.dist-info/licenses/LICENSE,sha256=qUqhUZUKNNG9Hfms_yxqBfjsgo7zuBnsjAt3pTe-OtU,1081
13
+ adsun-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adsun (https://adsun.io)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.