easyyad 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.
- easyyad/__init__.py +23 -0
- easyyad/client.py +261 -0
- easyyad/py.typed +0 -0
- easyyad-1.0.0.dist-info/METADATA +172 -0
- easyyad-1.0.0.dist-info/RECORD +8 -0
- easyyad-1.0.0.dist-info/WHEEL +5 -0
- easyyad-1.0.0.dist-info/licenses/LICENSE +21 -0
- easyyad-1.0.0.dist-info/top_level.txt +1 -0
easyyad/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK EasyAd pour les bots — Python 3.9+.
|
|
3
|
+
|
|
4
|
+
Deux appels suffisent : demander une annonce, puis confirmer qu'elle a bien été envoyée.
|
|
5
|
+
Par défaut, rien ne lève d'exception : un réseau coupé ou un quota atteint renvoie simplement
|
|
6
|
+
``None``, pour qu'une panne chez nous ne casse jamais votre bot.
|
|
7
|
+
|
|
8
|
+
from easyyad import EasyAd
|
|
9
|
+
|
|
10
|
+
easyad = EasyAd(api_key=os.environ["EASYAD_API_KEY"])
|
|
11
|
+
|
|
12
|
+
ad = easyad.next_ad(user_id=message.from_user.id, locale=message.from_user.language_code)
|
|
13
|
+
if ad:
|
|
14
|
+
await bot.send_message(chat_id, ad.text, parse_mode="HTML", reply_markup=ad.reply_markup)
|
|
15
|
+
easyad.mark_shown(ad.placement_id)
|
|
16
|
+
|
|
17
|
+
Une variante asynchrone (``AsyncEasyAd``) est disponible pour aiogram et les bots async.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import AsyncEasyAd, EasyAd, EasyAdError, Ad, hash_user_id
|
|
21
|
+
|
|
22
|
+
__all__ = ["EasyAd", "AsyncEasyAd", "EasyAdError", "Ad", "hash_user_id"]
|
|
23
|
+
__version__ = "1.0.0"
|
easyyad/client.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Client HTTP du SDK EasyAd : version synchrone et version asynchrone."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE = "https://api.easyyad.com/api/v1/sdk/bot"
|
|
15
|
+
LOGGER = logging.getLogger("easyyad")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EasyAdError(Exception):
|
|
19
|
+
"""Erreur renvoyée par EasyAd, ou impossibilité de le joindre."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, *, status: int = 0, code: str = "unknown",
|
|
22
|
+
retry_after_seconds: Optional[int] = None) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.status = status
|
|
25
|
+
self.code = code
|
|
26
|
+
self.retry_after_seconds = retry_after_seconds
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Ad:
|
|
31
|
+
"""Une annonce prête à envoyer."""
|
|
32
|
+
|
|
33
|
+
placement_id: str
|
|
34
|
+
text: str
|
|
35
|
+
reply_markup: Dict[str, Any]
|
|
36
|
+
parse_mode: str = "HTML"
|
|
37
|
+
image_url: Optional[str] = None
|
|
38
|
+
label: str = "#Ad #EasyAd"
|
|
39
|
+
confirm_within_seconds: int = 1800
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_payload(cls, payload: Dict[str, Any]) -> "Ad":
|
|
43
|
+
return cls(
|
|
44
|
+
placement_id=payload["placement_id"],
|
|
45
|
+
text=payload["text"],
|
|
46
|
+
reply_markup=payload.get("reply_markup") or {},
|
|
47
|
+
parse_mode=payload.get("parse_mode") or "HTML",
|
|
48
|
+
image_url=payload.get("image_url"),
|
|
49
|
+
label=payload.get("label") or "#Ad #EasyAd",
|
|
50
|
+
confirm_within_seconds=int(payload.get("confirm_within_seconds") or 1800),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def hash_user_id(user_id: Any, salt: str) -> str:
|
|
55
|
+
"""Empreinte stable d'un identifiant : nous n'avons jamais besoin de l'identifiant réel."""
|
|
56
|
+
digest = hashlib.sha256(f"{salt}:{user_id}".encode("utf-8")).hexdigest()
|
|
57
|
+
return digest[:32]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Base:
|
|
61
|
+
"""Réglages et traitement des réponses, partagés par les deux clients."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
api_key: str,
|
|
66
|
+
*,
|
|
67
|
+
base_url: str = DEFAULT_BASE,
|
|
68
|
+
timeout: float = 4.0,
|
|
69
|
+
raise_on_error: bool = False,
|
|
70
|
+
user_salt: Optional[str] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
if not api_key:
|
|
73
|
+
raise EasyAdError("Clé EasyAd manquante : passez api_key=…", code="missing_api_key")
|
|
74
|
+
self.api_key = api_key
|
|
75
|
+
self.base_url = base_url.rstrip("/")
|
|
76
|
+
self.timeout = timeout
|
|
77
|
+
self.raise_on_error = raise_on_error
|
|
78
|
+
self.user_salt = user_salt or api_key
|
|
79
|
+
|
|
80
|
+
# -- préparation ----------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def _headers(self, with_body: bool) -> Dict[str, str]:
|
|
83
|
+
headers = {"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"}
|
|
84
|
+
if with_body:
|
|
85
|
+
headers["Content-Type"] = "application/json"
|
|
86
|
+
return headers
|
|
87
|
+
|
|
88
|
+
def _next_payload(self, user_id: Any, locale: Optional[str]) -> Dict[str, Any]:
|
|
89
|
+
if user_id is None or user_id == "":
|
|
90
|
+
raise EasyAdError(
|
|
91
|
+
"user_id est requis : il sert à limiter la fréquence par utilisateur.",
|
|
92
|
+
code="missing_user_id",
|
|
93
|
+
)
|
|
94
|
+
payload: Dict[str, Any] = {"user_ref": hash_user_id(user_id, self.user_salt)}
|
|
95
|
+
if locale:
|
|
96
|
+
payload["locale"] = locale
|
|
97
|
+
return payload
|
|
98
|
+
|
|
99
|
+
# -- traitement des réponses ----------------------------------------
|
|
100
|
+
|
|
101
|
+
def _handle(self, status: int, text: str) -> Optional[Dict[str, Any]]:
|
|
102
|
+
body: Optional[Dict[str, Any]]
|
|
103
|
+
try:
|
|
104
|
+
body = json.loads(text) if text else None
|
|
105
|
+
except ValueError:
|
|
106
|
+
body = None
|
|
107
|
+
|
|
108
|
+
if status >= 400:
|
|
109
|
+
code = (body or {}).get("error") or f"http_{status}"
|
|
110
|
+
message = (body or {}).get("message") or f"EasyAd a répondu {status}."
|
|
111
|
+
error = EasyAdError(
|
|
112
|
+
message, status=status, code=code,
|
|
113
|
+
retry_after_seconds=(body or {}).get("retry_after_seconds"),
|
|
114
|
+
)
|
|
115
|
+
# Un quota atteint est une réponse normale, pas une panne.
|
|
116
|
+
if code == "frequency_capped":
|
|
117
|
+
LOGGER.debug("EasyAd : quota de fréquence atteint pour cet utilisateur.")
|
|
118
|
+
return None
|
|
119
|
+
if self.raise_on_error:
|
|
120
|
+
raise error
|
|
121
|
+
LOGGER.warning("EasyAd : %s (%s)", message, code)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
return body
|
|
125
|
+
|
|
126
|
+
def _failure(self, exc: BaseException) -> None:
|
|
127
|
+
timeout = isinstance(exc, (TimeoutError, urllib.error.URLError)) and "timed out" in str(exc).lower()
|
|
128
|
+
error = EasyAdError(
|
|
129
|
+
f"EasyAd n'a pas répondu en {self.timeout} s." if timeout else f"EasyAd est injoignable : {exc}",
|
|
130
|
+
code="timeout" if timeout else "network_error",
|
|
131
|
+
)
|
|
132
|
+
if self.raise_on_error:
|
|
133
|
+
raise error
|
|
134
|
+
LOGGER.warning("%s", error)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _ad_from(body: Optional[Dict[str, Any]]) -> Optional[Ad]:
|
|
139
|
+
if not body or not body.get("ad"):
|
|
140
|
+
return None
|
|
141
|
+
return Ad.from_payload(body["ad"])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class EasyAd(_Base):
|
|
145
|
+
"""Client synchrone, sans dépendance : il n'utilise que la bibliothèque standard."""
|
|
146
|
+
|
|
147
|
+
def next_ad(self, *, user_id: Any, locale: Optional[str] = None) -> Optional[Ad]:
|
|
148
|
+
"""Une annonce à montrer, ou ``None`` : rien à montrer, quota atteint, ou souci réseau."""
|
|
149
|
+
return self._ad_from(self._request("POST", "/ads/next", self._next_payload(user_id, locale)))
|
|
150
|
+
|
|
151
|
+
def mark_shown(self, placement_id: str) -> bool:
|
|
152
|
+
"""À appeler **après** l'envoi : c'est cette confirmation qui déclenche votre rémunération."""
|
|
153
|
+
if not placement_id:
|
|
154
|
+
return False
|
|
155
|
+
quoted = urllib.parse.quote(placement_id, safe="")
|
|
156
|
+
body = self._request("POST", f"/ads/{quoted}/shown", {})
|
|
157
|
+
return bool(body and body.get("status") == "published")
|
|
158
|
+
|
|
159
|
+
def health(self) -> Optional[Dict[str, Any]]:
|
|
160
|
+
"""Vérifie la clé et renvoie les réglages du bot. Pratique au démarrage."""
|
|
161
|
+
return self._request("GET", "/health", None)
|
|
162
|
+
|
|
163
|
+
def serve(self, *, user_id: Any, send: Callable[[Ad], Any], locale: Optional[str] = None) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Raccourci : demande, envoie via ``send``, confirme.
|
|
166
|
+
|
|
167
|
+
Si ``send`` lève une exception, rien n'est confirmé — donc rien n'est facturé ni payé.
|
|
168
|
+
"""
|
|
169
|
+
ad = self.next_ad(user_id=user_id, locale=locale)
|
|
170
|
+
if ad is None:
|
|
171
|
+
return False
|
|
172
|
+
send(ad)
|
|
173
|
+
return self.mark_shown(ad.placement_id)
|
|
174
|
+
|
|
175
|
+
def _request(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
176
|
+
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
177
|
+
request = urllib.request.Request(
|
|
178
|
+
self.base_url + path, data=data, method=method, headers=self._headers(payload is not None)
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
182
|
+
return self._handle(response.status, response.read().decode("utf-8"))
|
|
183
|
+
except urllib.error.HTTPError as exc: # réponse reçue, mais code d'erreur
|
|
184
|
+
return self._handle(exc.code, exc.read().decode("utf-8", errors="replace"))
|
|
185
|
+
except EasyAdError:
|
|
186
|
+
raise
|
|
187
|
+
except Exception as exc: # réseau, DNS, TLS, délai dépassé
|
|
188
|
+
return self._failure(exc)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AsyncEasyAd(_Base):
|
|
192
|
+
"""
|
|
193
|
+
Client asynchrone pour aiogram, python-telegram-bot et autres bots async.
|
|
194
|
+
|
|
195
|
+
Nécessite ``httpx`` ou ``aiohttp``. Si aucune des deux n'est installée, l'erreur le dit
|
|
196
|
+
clairement au démarrage plutôt qu'au premier appel en production.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
200
|
+
super().__init__(*args, **kwargs)
|
|
201
|
+
self._client_factory = self._detect_backend()
|
|
202
|
+
|
|
203
|
+
async def next_ad(self, *, user_id: Any, locale: Optional[str] = None) -> Optional[Ad]:
|
|
204
|
+
return self._ad_from(await self._request("POST", "/ads/next", self._next_payload(user_id, locale)))
|
|
205
|
+
|
|
206
|
+
async def mark_shown(self, placement_id: str) -> bool:
|
|
207
|
+
if not placement_id:
|
|
208
|
+
return False
|
|
209
|
+
quoted = urllib.parse.quote(placement_id, safe="")
|
|
210
|
+
body = await self._request("POST", f"/ads/{quoted}/shown", {})
|
|
211
|
+
return bool(body and body.get("status") == "published")
|
|
212
|
+
|
|
213
|
+
async def health(self) -> Optional[Dict[str, Any]]:
|
|
214
|
+
return await self._request("GET", "/health", None)
|
|
215
|
+
|
|
216
|
+
async def serve(
|
|
217
|
+
self, *, user_id: Any, send: Callable[[Ad], Awaitable[Any]], locale: Optional[str] = None
|
|
218
|
+
) -> bool:
|
|
219
|
+
ad = await self.next_ad(user_id=user_id, locale=locale)
|
|
220
|
+
if ad is None:
|
|
221
|
+
return False
|
|
222
|
+
await send(ad)
|
|
223
|
+
return await self.mark_shown(ad.placement_id)
|
|
224
|
+
|
|
225
|
+
def _detect_backend(self) -> str:
|
|
226
|
+
try:
|
|
227
|
+
import httpx # noqa: F401
|
|
228
|
+
return "httpx"
|
|
229
|
+
except ImportError:
|
|
230
|
+
pass
|
|
231
|
+
try:
|
|
232
|
+
import aiohttp # noqa: F401
|
|
233
|
+
return "aiohttp"
|
|
234
|
+
except ImportError as exc:
|
|
235
|
+
raise EasyAdError(
|
|
236
|
+
"AsyncEasyAd a besoin de httpx ou aiohttp : pip install httpx",
|
|
237
|
+
code="missing_dependency",
|
|
238
|
+
) from exc
|
|
239
|
+
|
|
240
|
+
async def _request(self, method: str, path: str, payload: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
241
|
+
url = self.base_url + path
|
|
242
|
+
headers = self._headers(payload is not None)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
if self._client_factory == "httpx":
|
|
246
|
+
import httpx
|
|
247
|
+
|
|
248
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
249
|
+
response = await client.request(method, url, json=payload, headers=headers)
|
|
250
|
+
return self._handle(response.status_code, response.text)
|
|
251
|
+
|
|
252
|
+
import aiohttp
|
|
253
|
+
|
|
254
|
+
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
255
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
256
|
+
async with session.request(method, url, json=payload, headers=headers) as response:
|
|
257
|
+
return self._handle(response.status, await response.text())
|
|
258
|
+
except EasyAdError:
|
|
259
|
+
raise
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
return self._failure(exc)
|
easyyad/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easyyad
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Show EasyAd ads in your Telegram bot and earn 50% of what advertisers pay.
|
|
5
|
+
Author-email: EasyAd <support@easyyad.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://easyyad.com
|
|
8
|
+
Project-URL: Documentation, https://easyyad.com/docs
|
|
9
|
+
Keywords: telegram,telegram-bot,bot,ads,advertising,monetization,aiogram,easyad
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Framework :: AsyncIO
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: async
|
|
23
|
+
Requires-Dist: httpx>=0.27; extra == "async"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# easyyad (Python)
|
|
27
|
+
|
|
28
|
+
> Le paquet s'appelle **easyyad**, comme le domaine : `easyad` était déjà pris sur PyPI par une
|
|
29
|
+
> bibliothèque Active Directory sans rapport avec nous.
|
|
30
|
+
|
|
31
|
+
Affichez des publicités EasyAd dans votre bot Telegram et touchez **50 %** de ce que l'annonceur paie.
|
|
32
|
+
|
|
33
|
+
> **English** — Show EasyAd ads in your Telegram bot and earn **50%** of what advertisers pay.
|
|
34
|
+
> `pip install easyyad` (or `easyyad[async]` for aiogram), create `EasyAd(api_key=...)`, then call
|
|
35
|
+
> `next_ad(...)` and `mark_shown(...)` after your bot has answered. Python 3.9+, no required dependency.
|
|
36
|
+
> Full documentation: https://easyyad.com/docs
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install easyyad # client synchrone, sans dépendance
|
|
40
|
+
pip install easyyad[async] # ajoute httpx pour la version asynchrone
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## En trois lignes
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from easyyad import EasyAd
|
|
48
|
+
|
|
49
|
+
easyad = EasyAd(api_key=os.environ["EASYAD_API_KEY"])
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Récupérez votre clé dans votre espace EasyAd, onglet **Bots** → votre bot → **Clé d'intégration**.
|
|
53
|
+
Elle n'est affichée qu'une fois : nous n'en gardons qu'une empreinte, nous ne pouvons pas vous la
|
|
54
|
+
redonner, seulement la remplacer.
|
|
55
|
+
|
|
56
|
+
## Montrer une annonce
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
ad = easyad.next_ad(user_id=message.from_user.id, locale=message.from_user.language_code)
|
|
60
|
+
|
|
61
|
+
if ad:
|
|
62
|
+
bot.send_message(
|
|
63
|
+
message.chat.id, ad.text,
|
|
64
|
+
parse_mode="HTML", reply_markup=ad.reply_markup,
|
|
65
|
+
)
|
|
66
|
+
easyad.mark_shown(ad.placement_id) # sans cette ligne, vous n'êtes pas payé
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`next_ad` renvoie `None` quand il n'y a rien à montrer, quand cet utilisateur a déjà vu son quota du
|
|
70
|
+
jour, ou si EasyAd est momentanément injoignable. Dans les trois cas, votre bot continue comme si
|
|
71
|
+
rien ne s'était passé : **une panne chez nous ne doit jamais casser votre bot.**
|
|
72
|
+
|
|
73
|
+
### Le raccourci `serve()`
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
easyad.serve(
|
|
77
|
+
user_id=message.from_user.id,
|
|
78
|
+
locale=message.from_user.language_code,
|
|
79
|
+
send=lambda ad: bot.send_message(
|
|
80
|
+
message.chat.id, ad.text, parse_mode="HTML", reply_markup=ad.reply_markup,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Demande, envoie, confirme. Si votre fonction `send` lève une exception, rien n'est confirmé — donc
|
|
86
|
+
rien ne vous est compté, et l'annonceur n'est pas facturé.
|
|
87
|
+
|
|
88
|
+
## Bots asynchrones (aiogram, python-telegram-bot)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from easyyad import AsyncEasyAd
|
|
92
|
+
|
|
93
|
+
easyad = AsyncEasyAd(api_key=os.environ["EASYAD_API_KEY"])
|
|
94
|
+
|
|
95
|
+
@dp.message()
|
|
96
|
+
async def handler(message: Message):
|
|
97
|
+
await repondre_normalement(message)
|
|
98
|
+
|
|
99
|
+
ad = await easyad.next_ad(user_id=message.from_user.id, locale=message.from_user.language_code)
|
|
100
|
+
if ad:
|
|
101
|
+
await message.answer(ad.text, parse_mode="HTML", reply_markup=ad.reply_markup)
|
|
102
|
+
await easyad.mark_shown(ad.placement_id)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`AsyncEasyAd` utilise `httpx` s'il est installé, sinon `aiohttp`. Si aucune des deux n'est présente,
|
|
106
|
+
l'erreur arrive à la construction du client — pas au premier appel en production.
|
|
107
|
+
|
|
108
|
+
## Avec une image
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
if ad.image_url:
|
|
112
|
+
bot.send_photo(chat_id, ad.image_url, caption=ad.text, parse_mode="HTML", reply_markup=ad.reply_markup)
|
|
113
|
+
else:
|
|
114
|
+
bot.send_message(chat_id, ad.text, parse_mode="HTML", reply_markup=ad.reply_markup)
|
|
115
|
+
|
|
116
|
+
easyad.mark_shown(ad.placement_id)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Vérifier l'intégration
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
print(easyad.health())
|
|
123
|
+
# {'ok': True, 'bot': {'name': 'Mon bot', 'status': 'approved', 'serving': True}, 'limits': {...}}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Appelez-le au démarrage : vous saurez tout de suite si la clé est bonne et si votre bot est approuvé.
|
|
127
|
+
|
|
128
|
+
## Ce que l'on reçoit de vous
|
|
129
|
+
|
|
130
|
+
`user_id` est **haché avant l'envoi** (SHA-256, tronqué à 32 caractères, salé avec votre clé). Nous ne
|
|
131
|
+
voyons jamais l'identifiant Telegram de vos utilisateurs, et comme le sel est votre clé, deux
|
|
132
|
+
partenaires ne peuvent pas recouper leurs audiences. Cette empreinte ne sert qu'à une chose : ne pas
|
|
133
|
+
montrer trop d'annonces à la même personne.
|
|
134
|
+
|
|
135
|
+
Vous pouvez choisir votre propre sel avec `user_salt=` — gardez-le stable, sinon le plafond de
|
|
136
|
+
fréquence repart de zéro à chaque changement.
|
|
137
|
+
|
|
138
|
+
## Options
|
|
139
|
+
|
|
140
|
+
| Option | Défaut | À quoi ça sert |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `api_key` | — | Votre clé secrète. Obligatoire. |
|
|
143
|
+
| `base_url` | `https://api.easyyad.com/api/v1/sdk/bot` | Pour viser un environnement de test. |
|
|
144
|
+
| `timeout` | `4.0` | Secondes. Au-delà, on renonce : votre bot doit rester rapide. |
|
|
145
|
+
| `raise_on_error` | `False` | `True` pour gérer les exceptions vous-même. |
|
|
146
|
+
| `user_salt` | la clé | Sel du hachage des identifiants. |
|
|
147
|
+
|
|
148
|
+
La journalisation passe par le logger standard `easyyad` :
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import logging
|
|
152
|
+
logging.getLogger("easyyad").setLevel(logging.DEBUG)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Fréquence
|
|
156
|
+
|
|
157
|
+
Deux limites, réglables dans votre espace EasyAd :
|
|
158
|
+
|
|
159
|
+
- **annonces par utilisateur et par jour** (3 par défaut) ;
|
|
160
|
+
- **délai minimum entre deux annonces** pour la même personne (60 minutes par défaut).
|
|
161
|
+
|
|
162
|
+
Elles existent pour protéger votre audience : une conversation privée saturée de publicité fait
|
|
163
|
+
partir vos utilisateurs, et c'est votre revenu qui disparaît avec eux. Vous pouvez les resserrer
|
|
164
|
+
autant que vous voulez.
|
|
165
|
+
|
|
166
|
+
## Rémunération
|
|
167
|
+
|
|
168
|
+
Vous êtes payé sur les annonces **confirmées**. Une annonce demandée mais jamais confirmée est
|
|
169
|
+
abandonnée au bout de 30 minutes et ne compte pour personne. Les gains apparaissent dans votre
|
|
170
|
+
portefeuille après la période de vérification, puis deviennent retirables.
|
|
171
|
+
|
|
172
|
+
Documentation complète : <https://www.easyyad.com/docs/bots>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
easyyad/__init__.py,sha256=F3_42uSeSNflZYvfrNVusWcfVvTjunC9alkyCqhABeY,902
|
|
2
|
+
easyyad/client.py,sha256=s1nSS3A9_-JYh71TPWIWLaNhbVHuyPx2zW_pjQun9mU,10253
|
|
3
|
+
easyyad/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
easyyad-1.0.0.dist-info/licenses/LICENSE,sha256=ORLBh_OfnGJ3FbasZf5ZzOEcAZLmtEeNro93R8Hz3Do,1095
|
|
5
|
+
easyyad-1.0.0.dist-info/METADATA,sha256=oZI2HB2CdJncvj8xdZj95uwhhebrn2NfN_LgwHYl_3c,6335
|
|
6
|
+
easyyad-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
easyyad-1.0.0.dist-info/top_level.txt,sha256=7fELba3xGTK25seb2CcJY-SOG5_jVU-vL_bgwLnmO8Y,8
|
|
8
|
+
easyyad-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 N'GUESSAN JEAN STEPHANE ELIEL (EasyAd)
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
easyyad
|