@easyyad/bot-sdk 0.0.0-stage → 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/dist/index.cjs +174 -0
- package/index.d.ts +47 -0
- package/package.json +29 -4
- package/src/index.js +169 -0
package/LICENSE
ADDED
|
@@ -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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @easyyad/bot-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Affichez des publicités EasyAd dans votre bot Telegram et touchez **50 %** de ce que l'annonceur paie.
|
|
4
|
+
|
|
5
|
+
> **English** — Show EasyAd ads in your Telegram bot and earn **50%** of what advertisers pay.
|
|
6
|
+
> `npm install @easyyad/bot-sdk`, create `new EasyAd({ apiKey })`, then call `easyad.serve({ userId, locale, send })`
|
|
7
|
+
> after your bot has answered. Works with ESM (`import`) and CommonJS (`require`), Node 18+. TypeScript types included.
|
|
8
|
+
> Full documentation: https://easyyad.com/docs
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @easyyad/bot-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## En trois lignes
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { EasyAd } from '@easyyad/bot-sdk';
|
|
18
|
+
|
|
19
|
+
const easyad = new EasyAd({ apiKey: process.env.EASYAD_API_KEY });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Récupérez votre clé dans votre espace EasyAd, onglet **Bots** → votre bot → **Clé d'intégration**.
|
|
23
|
+
Elle n'est affichée qu'une fois : nous n'en gardons qu'une empreinte, nous ne pouvons pas vous la
|
|
24
|
+
redonner, seulement la remplacer.
|
|
25
|
+
|
|
26
|
+
## Montrer une annonce
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
bot.on('message', async (ctx) => {
|
|
30
|
+
await repondreNormalement(ctx); // votre bot fait d'abord son travail
|
|
31
|
+
|
|
32
|
+
const ad = await easyad.nextAd({
|
|
33
|
+
userId: ctx.from.id,
|
|
34
|
+
locale: ctx.from.language_code,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (ad) {
|
|
38
|
+
await ctx.reply(ad.text, { parse_mode: 'HTML', reply_markup: ad.replyMarkup });
|
|
39
|
+
await easyad.markShown(ad.placementId); // sans cette ligne, vous n'êtes pas payé
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`nextAd` renvoie `null` quand il n'y a rien à montrer, quand cet utilisateur a déjà vu son quota du
|
|
45
|
+
jour, ou si EasyAd est momentanément injoignable. Dans les trois cas, votre bot continue comme si
|
|
46
|
+
rien ne s'était passé : **une panne chez nous ne doit jamais casser votre bot.**
|
|
47
|
+
|
|
48
|
+
### Le raccourci `serve()`
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
await easyad.serve({
|
|
52
|
+
userId: ctx.from.id,
|
|
53
|
+
locale: ctx.from.language_code,
|
|
54
|
+
send: (ad) => ctx.reply(ad.text, { parse_mode: 'HTML', reply_markup: ad.replyMarkup }),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Demande, envoie, confirme. Si votre `send` échoue, rien n'est confirmé — donc rien ne vous est
|
|
59
|
+
compté, et l'annonceur n'est pas facturé.
|
|
60
|
+
|
|
61
|
+
## Avec une image
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
if (ad.imageUrl) {
|
|
65
|
+
await ctx.replyWithPhoto(ad.imageUrl, { caption: ad.text, parse_mode: 'HTML', reply_markup: ad.replyMarkup });
|
|
66
|
+
} else {
|
|
67
|
+
await ctx.reply(ad.text, { parse_mode: 'HTML', reply_markup: ad.replyMarkup });
|
|
68
|
+
}
|
|
69
|
+
await easyad.markShown(ad.placementId);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Vérifier l'intégration
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
console.log(await easyad.health());
|
|
76
|
+
// { ok: true, bot: { name: 'Mon bot', status: 'approved', serving: true }, limits: { … } }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Appelez-le au démarrage : vous saurez tout de suite si la clé est bonne et si votre bot est approuvé.
|
|
80
|
+
|
|
81
|
+
## Ce que l'on reçoit de vous
|
|
82
|
+
|
|
83
|
+
`userId` est **haché avant l'envoi** (SHA-256, tronqué à 32 caractères, salé avec votre clé). Nous ne
|
|
84
|
+
voyons jamais l'identifiant Telegram de vos utilisateurs, et comme le sel est votre clé, deux
|
|
85
|
+
partenaires ne peuvent pas recouper leurs audiences. Cette empreinte ne sert qu'à une chose : ne pas
|
|
86
|
+
montrer trop d'annonces à la même personne.
|
|
87
|
+
|
|
88
|
+
Vous pouvez choisir votre propre sel avec `userSalt` — gardez-le stable, sinon le plafond de
|
|
89
|
+
fréquence repart de zéro à chaque changement.
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
| Option | Défaut | À quoi ça sert |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `apiKey` | — | Votre clé secrète. Obligatoire. |
|
|
96
|
+
| `baseUrl` | `https://api.easyyad.com/api/v1/sdk/bot` | Pour viser un environnement de test. |
|
|
97
|
+
| `timeoutMs` | `4000` | Au-delà, on renonce : votre bot doit rester rapide. |
|
|
98
|
+
| `throwOnError` | `false` | `true` pour gérer les erreurs vous-même. |
|
|
99
|
+
| `userSalt` | la clé | Sel du hachage des identifiants. |
|
|
100
|
+
| `logger` | — | `(level, message, extra) => void`, branchez votre journalisation. |
|
|
101
|
+
|
|
102
|
+
## Fréquence
|
|
103
|
+
|
|
104
|
+
Deux limites, réglables dans votre espace EasyAd :
|
|
105
|
+
|
|
106
|
+
- **annonces par utilisateur et par jour** (3 par défaut) ;
|
|
107
|
+
- **délai minimum entre deux annonces** pour la même personne (60 minutes par défaut).
|
|
108
|
+
|
|
109
|
+
Elles existent pour protéger votre audience : une conversation privée saturée de publicité fait
|
|
110
|
+
partir vos utilisateurs, et c'est votre revenu qui disparaît avec eux. Vous pouvez les resserrer
|
|
111
|
+
autant que vous voulez.
|
|
112
|
+
|
|
113
|
+
## Rémunération
|
|
114
|
+
|
|
115
|
+
Vous êtes payé sur les annonces **confirmées**. Une annonce demandée mais jamais confirmée est
|
|
116
|
+
abandonnée au bout de 30 minutes et ne compte pour personne. Les gains apparaissent dans votre
|
|
117
|
+
portefeuille après la période de vérification, puis deviennent retirables.
|
|
118
|
+
|
|
119
|
+
Documentation complète : <https://www.easyyad.com/docs/bots>
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* SDK EasyAd pour les bots — JavaScript / TypeScript (Node 18+).
|
|
4
|
+
*
|
|
5
|
+
* Deux appels suffisent : demander une annonce, puis confirmer qu'elle a bien été envoyée.
|
|
6
|
+
* Rien dans ce SDK ne lève d'erreur par défaut : un réseau coupé ou un quota atteint renvoie
|
|
7
|
+
* simplement `null`, pour qu'une panne chez nous ne casse jamais votre bot.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BASE = 'https://api.easyyad.com/api/v1/sdk/bot';
|
|
11
|
+
|
|
12
|
+
class EasyAdError extends Error {
|
|
13
|
+
constructor(message, { status, code, retryAfterSeconds } = {}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'EasyAdError';
|
|
16
|
+
this.status = status ?? 0;
|
|
17
|
+
this.code = code ?? 'unknown';
|
|
18
|
+
this.retryAfterSeconds = retryAfterSeconds ?? null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Identifiant d'utilisateur haché : nous n'avons jamais besoin de l'identifiant réel. */
|
|
23
|
+
async function hashUserId(value, salt) {
|
|
24
|
+
const { createHash } = await import('node:crypto');
|
|
25
|
+
return createHash('sha256').update(`${salt}:${value}`).digest('hex').slice(0, 32);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class EasyAd {
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @param {string} options.apiKey Clé secrète du bot (ea_live_…).
|
|
32
|
+
* @param {string} [options.baseUrl] Pour pointer vers un environnement de test.
|
|
33
|
+
* @param {number} [options.timeoutMs=4000] Au-delà, on renonce : votre bot doit répondre vite.
|
|
34
|
+
* @param {boolean} [options.throwOnError=false] true pour gérer les erreurs vous-même.
|
|
35
|
+
* @param {string} [options.userSalt] Sel du hachage des identifiants (défaut : la clé).
|
|
36
|
+
* @param {(level: string, message: string, extra?: object) => void} [options.logger]
|
|
37
|
+
*/
|
|
38
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE, timeoutMs = 4000, throwOnError = false, userSalt, logger } = {}) {
|
|
39
|
+
if (!apiKey) throw new EasyAdError('Clé EasyAd manquante : passez { apiKey }.', { code: 'missing_api_key' });
|
|
40
|
+
this.apiKey = apiKey;
|
|
41
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
42
|
+
this.timeoutMs = timeoutMs;
|
|
43
|
+
this.throwOnError = throwOnError;
|
|
44
|
+
this.userSalt = userSalt || apiKey;
|
|
45
|
+
this.logger = logger || (() => {});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Demande une annonce pour l'utilisateur en face du bot.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} params
|
|
52
|
+
* @param {string|number} params.userId Identifiant chez vous ; il est haché avant l'envoi.
|
|
53
|
+
* @param {string} [params.locale] « fr », « en-US »… pour un meilleur ciblage.
|
|
54
|
+
* @returns {Promise<null|{placementId: string, text: string, parseMode: string, replyMarkup: object, imageUrl: ?string, label: string, confirmWithinSeconds: number}>}
|
|
55
|
+
* `null` quand il n'y a rien à montrer, que l'utilisateur a atteint son quota, ou en cas de problème réseau.
|
|
56
|
+
*/
|
|
57
|
+
async nextAd({ userId, locale } = {}) {
|
|
58
|
+
if (userId === undefined || userId === null || userId === '') {
|
|
59
|
+
throw new EasyAdError('userId est requis : il sert à limiter la fréquence par utilisateur.', { code: 'missing_user_id' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const userRef = await hashUserId(String(userId), this.userSalt);
|
|
63
|
+
const body = await this.#post('/ads/next', { user_ref: userRef, locale: locale ?? undefined });
|
|
64
|
+
if (!body || !body.ad) return null;
|
|
65
|
+
|
|
66
|
+
const ad = body.ad;
|
|
67
|
+
return {
|
|
68
|
+
placementId: ad.placement_id,
|
|
69
|
+
text: ad.text,
|
|
70
|
+
parseMode: ad.parse_mode || 'HTML',
|
|
71
|
+
replyMarkup: ad.reply_markup,
|
|
72
|
+
imageUrl: ad.image_url ?? null,
|
|
73
|
+
label: ad.label,
|
|
74
|
+
confirmWithinSeconds: ad.confirm_within_seconds ?? 1800,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Confirme l'envoi. **À appeler après** que l'annonce est partie : c'est cette confirmation qui
|
|
80
|
+
* déclenche votre rémunération. Sans elle, l'annonce n'est ni facturée ni payée.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Promise<boolean>} true si la confirmation est enregistrée.
|
|
83
|
+
*/
|
|
84
|
+
async markShown(placementId) {
|
|
85
|
+
if (!placementId) return false;
|
|
86
|
+
const body = await this.#post(`/ads/${encodeURIComponent(placementId)}/shown`, {});
|
|
87
|
+
return Boolean(body && body.status === 'published');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Vérifie la clé et renvoie les réglages du bot. Pratique au démarrage. */
|
|
91
|
+
async health() {
|
|
92
|
+
return this.#request('GET', '/health');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Raccourci le plus courant : demande une annonce, l'envoie avec votre fonction, confirme.
|
|
97
|
+
* `send` reçoit l'annonce et doit l'envoyer ; s'il lève une erreur, rien n'est confirmé.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} params
|
|
100
|
+
* @param {string|number} params.userId
|
|
101
|
+
* @param {string} [params.locale]
|
|
102
|
+
* @param {(ad: object) => Promise<unknown>} params.send
|
|
103
|
+
* @returns {Promise<boolean>} true si une annonce a été envoyée et confirmée.
|
|
104
|
+
*/
|
|
105
|
+
async serve({ userId, locale, send }) {
|
|
106
|
+
const ad = await this.nextAd({ userId, locale });
|
|
107
|
+
if (!ad) return false;
|
|
108
|
+
|
|
109
|
+
await send(ad); // une erreur ici remonte : pas de confirmation, pas de facturation
|
|
110
|
+
return this.markShown(ad.placementId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #post(path, payload) {
|
|
114
|
+
return this.#request('POST', path, payload);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async #request(method, path, payload) {
|
|
118
|
+
const controller = new AbortController();
|
|
119
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(this.baseUrl + path, {
|
|
123
|
+
method,
|
|
124
|
+
headers: {
|
|
125
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
126
|
+
Accept: 'application/json',
|
|
127
|
+
...(payload ? { 'Content-Type': 'application/json' } : {}),
|
|
128
|
+
},
|
|
129
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const text = await response.text();
|
|
134
|
+
let body = null;
|
|
135
|
+
try { body = text ? JSON.parse(text) : null; } catch { /* réponse non JSON */ }
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const error = new EasyAdError(body?.message || `EasyAd a répondu ${response.status}.`, {
|
|
139
|
+
status: response.status,
|
|
140
|
+
code: body?.error || `http_${response.status}`,
|
|
141
|
+
retryAfterSeconds: body?.retry_after_seconds ?? null,
|
|
142
|
+
});
|
|
143
|
+
// Un quota atteint est une réponse normale, pas une panne : on n'alarme personne.
|
|
144
|
+
if (error.code === 'frequency_capped') {
|
|
145
|
+
this.logger('debug', 'EasyAd : quota de fréquence atteint pour cet utilisateur.', { code: error.code });
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (this.throwOnError) throw error;
|
|
149
|
+
this.logger('warn', `EasyAd : ${error.message}`, { code: error.code, status: error.status });
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return body;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
if (e instanceof EasyAdError) throw e;
|
|
156
|
+
const aborted = e?.name === 'AbortError';
|
|
157
|
+
const error = new EasyAdError(
|
|
158
|
+
aborted ? `EasyAd n'a pas répondu en ${this.timeoutMs} ms.` : `EasyAd est injoignable : ${e?.message || e}`,
|
|
159
|
+
{ code: aborted ? 'timeout' : 'network_error' },
|
|
160
|
+
);
|
|
161
|
+
if (this.throwOnError) throw error;
|
|
162
|
+
this.logger('warn', error.message, { code: error.code });
|
|
163
|
+
return null;
|
|
164
|
+
} finally {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
module.exports = EasyAd;
|
|
172
|
+
module.exports.EasyAd = EasyAd;
|
|
173
|
+
module.exports.EasyAdError = EasyAdError;
|
|
174
|
+
module.exports.default = EasyAd;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Types de @easyyad/bot-sdk
|
|
2
|
+
|
|
3
|
+
export interface EasyAdOptions {
|
|
4
|
+
/** Clé secrète du bot (ea_live_…), dans votre espace EasyAd → Bots → Clé d'intégration. */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Pour un environnement de test. Défaut : https://api.easyyad.com/api/v1/sdk/bot */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Délai maximal d'un appel (défaut 4000 ms). */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** true pour recevoir les erreurs au lieu d'un simple `null`. */
|
|
11
|
+
throwOnError?: boolean;
|
|
12
|
+
/** Sel du hachage des identifiants utilisateurs (défaut : la clé). */
|
|
13
|
+
userSalt?: string;
|
|
14
|
+
logger?: (level: string, message: string, extra?: Record<string, unknown>) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EasyAdAd {
|
|
18
|
+
placementId: string;
|
|
19
|
+
text: string;
|
|
20
|
+
parseMode: 'HTML';
|
|
21
|
+
replyMarkup: { inline_keyboard: Array<Array<{ text: string; url: string; style?: string }>> };
|
|
22
|
+
imageUrl: string | null;
|
|
23
|
+
label: string;
|
|
24
|
+
confirmWithinSeconds: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NextAdParams {
|
|
28
|
+
userId: string | number;
|
|
29
|
+
locale?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export declare class EasyAdError extends Error {
|
|
33
|
+
status: number;
|
|
34
|
+
code: string;
|
|
35
|
+
retryAfterSeconds: number | null;
|
|
36
|
+
constructor(message: string, extra?: { status?: number; code?: string; retryAfterSeconds?: number | null });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export declare class EasyAd {
|
|
40
|
+
constructor(options: EasyAdOptions);
|
|
41
|
+
nextAd(params: NextAdParams): Promise<EasyAdAd | null>;
|
|
42
|
+
markShown(placementId: string): Promise<boolean>;
|
|
43
|
+
health(): Promise<Record<string, unknown> | null>;
|
|
44
|
+
serve(params: NextAdParams & { send: (ad: EasyAdAd) => Promise<unknown> }): Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default EasyAd;
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easyyad/bot-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Show EasyAd ads in your Telegram bot and earn 50% of what advertisers pay. / Affichez des publicités EasyAd dans votre bot Telegram et soyez rémunéré.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": ["src", "dist", "index.d.ts", "README.md", "LICENSE"],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"engines": { "node": ">=18" },
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "node scripts/build-cjs.mjs",
|
|
22
|
+
"test": "node scripts/build-cjs.mjs && node --test",
|
|
23
|
+
"prepublishOnly": "npm test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["telegram", "telegram-bot", "bot", "ads", "advertising", "monetization", "grammy", "telegraf", "easyad"],
|
|
26
|
+
"homepage": "https://easyyad.com/docs",
|
|
27
|
+
"bugs": { "email": "support@easyyad.com" },
|
|
28
|
+
"author": "EasyAd <support@easyyad.com> (https://easyyad.com)",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"publishConfig": { "access": "public" }
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK EasyAd pour les bots — JavaScript / TypeScript (Node 18+).
|
|
3
|
+
*
|
|
4
|
+
* Deux appels suffisent : demander une annonce, puis confirmer qu'elle a bien été envoyée.
|
|
5
|
+
* Rien dans ce SDK ne lève d'erreur par défaut : un réseau coupé ou un quota atteint renvoie
|
|
6
|
+
* simplement `null`, pour qu'une panne chez nous ne casse jamais votre bot.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_BASE = 'https://api.easyyad.com/api/v1/sdk/bot';
|
|
10
|
+
|
|
11
|
+
export class EasyAdError extends Error {
|
|
12
|
+
constructor(message, { status, code, retryAfterSeconds } = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'EasyAdError';
|
|
15
|
+
this.status = status ?? 0;
|
|
16
|
+
this.code = code ?? 'unknown';
|
|
17
|
+
this.retryAfterSeconds = retryAfterSeconds ?? null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Identifiant d'utilisateur haché : nous n'avons jamais besoin de l'identifiant réel. */
|
|
22
|
+
async function hashUserId(value, salt) {
|
|
23
|
+
const { createHash } = await import('node:crypto');
|
|
24
|
+
return createHash('sha256').update(`${salt}:${value}`).digest('hex').slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class EasyAd {
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} options
|
|
30
|
+
* @param {string} options.apiKey Clé secrète du bot (ea_live_…).
|
|
31
|
+
* @param {string} [options.baseUrl] Pour pointer vers un environnement de test.
|
|
32
|
+
* @param {number} [options.timeoutMs=4000] Au-delà, on renonce : votre bot doit répondre vite.
|
|
33
|
+
* @param {boolean} [options.throwOnError=false] true pour gérer les erreurs vous-même.
|
|
34
|
+
* @param {string} [options.userSalt] Sel du hachage des identifiants (défaut : la clé).
|
|
35
|
+
* @param {(level: string, message: string, extra?: object) => void} [options.logger]
|
|
36
|
+
*/
|
|
37
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE, timeoutMs = 4000, throwOnError = false, userSalt, logger } = {}) {
|
|
38
|
+
if (!apiKey) throw new EasyAdError('Clé EasyAd manquante : passez { apiKey }.', { code: 'missing_api_key' });
|
|
39
|
+
this.apiKey = apiKey;
|
|
40
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
41
|
+
this.timeoutMs = timeoutMs;
|
|
42
|
+
this.throwOnError = throwOnError;
|
|
43
|
+
this.userSalt = userSalt || apiKey;
|
|
44
|
+
this.logger = logger || (() => {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Demande une annonce pour l'utilisateur en face du bot.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} params
|
|
51
|
+
* @param {string|number} params.userId Identifiant chez vous ; il est haché avant l'envoi.
|
|
52
|
+
* @param {string} [params.locale] « fr », « en-US »… pour un meilleur ciblage.
|
|
53
|
+
* @returns {Promise<null|{placementId: string, text: string, parseMode: string, replyMarkup: object, imageUrl: ?string, label: string, confirmWithinSeconds: number}>}
|
|
54
|
+
* `null` quand il n'y a rien à montrer, que l'utilisateur a atteint son quota, ou en cas de problème réseau.
|
|
55
|
+
*/
|
|
56
|
+
async nextAd({ userId, locale } = {}) {
|
|
57
|
+
if (userId === undefined || userId === null || userId === '') {
|
|
58
|
+
throw new EasyAdError('userId est requis : il sert à limiter la fréquence par utilisateur.', { code: 'missing_user_id' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const userRef = await hashUserId(String(userId), this.userSalt);
|
|
62
|
+
const body = await this.#post('/ads/next', { user_ref: userRef, locale: locale ?? undefined });
|
|
63
|
+
if (!body || !body.ad) return null;
|
|
64
|
+
|
|
65
|
+
const ad = body.ad;
|
|
66
|
+
return {
|
|
67
|
+
placementId: ad.placement_id,
|
|
68
|
+
text: ad.text,
|
|
69
|
+
parseMode: ad.parse_mode || 'HTML',
|
|
70
|
+
replyMarkup: ad.reply_markup,
|
|
71
|
+
imageUrl: ad.image_url ?? null,
|
|
72
|
+
label: ad.label,
|
|
73
|
+
confirmWithinSeconds: ad.confirm_within_seconds ?? 1800,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Confirme l'envoi. **À appeler après** que l'annonce est partie : c'est cette confirmation qui
|
|
79
|
+
* déclenche votre rémunération. Sans elle, l'annonce n'est ni facturée ni payée.
|
|
80
|
+
*
|
|
81
|
+
* @returns {Promise<boolean>} true si la confirmation est enregistrée.
|
|
82
|
+
*/
|
|
83
|
+
async markShown(placementId) {
|
|
84
|
+
if (!placementId) return false;
|
|
85
|
+
const body = await this.#post(`/ads/${encodeURIComponent(placementId)}/shown`, {});
|
|
86
|
+
return Boolean(body && body.status === 'published');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Vérifie la clé et renvoie les réglages du bot. Pratique au démarrage. */
|
|
90
|
+
async health() {
|
|
91
|
+
return this.#request('GET', '/health');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Raccourci le plus courant : demande une annonce, l'envoie avec votre fonction, confirme.
|
|
96
|
+
* `send` reçoit l'annonce et doit l'envoyer ; s'il lève une erreur, rien n'est confirmé.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} params
|
|
99
|
+
* @param {string|number} params.userId
|
|
100
|
+
* @param {string} [params.locale]
|
|
101
|
+
* @param {(ad: object) => Promise<unknown>} params.send
|
|
102
|
+
* @returns {Promise<boolean>} true si une annonce a été envoyée et confirmée.
|
|
103
|
+
*/
|
|
104
|
+
async serve({ userId, locale, send }) {
|
|
105
|
+
const ad = await this.nextAd({ userId, locale });
|
|
106
|
+
if (!ad) return false;
|
|
107
|
+
|
|
108
|
+
await send(ad); // une erreur ici remonte : pas de confirmation, pas de facturation
|
|
109
|
+
return this.markShown(ad.placementId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async #post(path, payload) {
|
|
113
|
+
return this.#request('POST', path, payload);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #request(method, path, payload) {
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(this.baseUrl + path, {
|
|
122
|
+
method,
|
|
123
|
+
headers: {
|
|
124
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
125
|
+
Accept: 'application/json',
|
|
126
|
+
...(payload ? { 'Content-Type': 'application/json' } : {}),
|
|
127
|
+
},
|
|
128
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const text = await response.text();
|
|
133
|
+
let body = null;
|
|
134
|
+
try { body = text ? JSON.parse(text) : null; } catch { /* réponse non JSON */ }
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const error = new EasyAdError(body?.message || `EasyAd a répondu ${response.status}.`, {
|
|
138
|
+
status: response.status,
|
|
139
|
+
code: body?.error || `http_${response.status}`,
|
|
140
|
+
retryAfterSeconds: body?.retry_after_seconds ?? null,
|
|
141
|
+
});
|
|
142
|
+
// Un quota atteint est une réponse normale, pas une panne : on n'alarme personne.
|
|
143
|
+
if (error.code === 'frequency_capped') {
|
|
144
|
+
this.logger('debug', 'EasyAd : quota de fréquence atteint pour cet utilisateur.', { code: error.code });
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (this.throwOnError) throw error;
|
|
148
|
+
this.logger('warn', `EasyAd : ${error.message}`, { code: error.code, status: error.status });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return body;
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e instanceof EasyAdError) throw e;
|
|
155
|
+
const aborted = e?.name === 'AbortError';
|
|
156
|
+
const error = new EasyAdError(
|
|
157
|
+
aborted ? `EasyAd n'a pas répondu en ${this.timeoutMs} ms.` : `EasyAd est injoignable : ${e?.message || e}`,
|
|
158
|
+
{ code: aborted ? 'timeout' : 'network_error' },
|
|
159
|
+
);
|
|
160
|
+
if (this.throwOnError) throw error;
|
|
161
|
+
this.logger('warn', error.message, { code: error.code });
|
|
162
|
+
return null;
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default EasyAd;
|