@adriangalilea/utils 0.5.0 → 0.7.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/README.md +46 -0
- package/dist/bot/access-control.d.ts +339 -0
- package/dist/bot/access-control.d.ts.map +1 -0
- package/dist/bot/access-control.js +516 -0
- package/dist/bot/access-control.js.map +1 -0
- package/dist/bot/coalesce.d.ts +107 -0
- package/dist/bot/coalesce.d.ts.map +1 -0
- package/dist/bot/coalesce.js +155 -0
- package/dist/bot/coalesce.js.map +1 -0
- package/dist/bot/index.d.ts +18 -0
- package/dist/bot/index.d.ts.map +1 -0
- package/dist/bot/index.js +18 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/bot/kit.d.ts +50 -0
- package/dist/bot/kit.d.ts.map +1 -0
- package/dist/bot/kit.js +52 -0
- package/dist/bot/kit.js.map +1 -0
- package/dist/bot/llm-stream.d.ts +84 -0
- package/dist/bot/llm-stream.d.ts.map +1 -0
- package/dist/bot/llm-stream.js +201 -0
- package/dist/bot/llm-stream.js.map +1 -0
- package/package.json +51 -2
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control for personal GramIO bots — a one-stop guard +
|
|
3
|
+
* approve/deny + revocable allow-list with an inline admin menu.
|
|
4
|
+
*
|
|
5
|
+
* stranger DMs your bot
|
|
6
|
+
* │
|
|
7
|
+
* ▼
|
|
8
|
+
* ┌──── plugin gate (this file) ────────────────────┐
|
|
9
|
+
* │ ctx.from.id ∈ admin / defaults / approved? │
|
|
10
|
+
* │ yes → next() │
|
|
11
|
+
* │ no → drop + notify admin (rate-limited) │
|
|
12
|
+
* └─────────────────────────────────────────────────┘
|
|
13
|
+
* │
|
|
14
|
+
* admin gets DM with [✅ Aprobar] [❌ Denegar]
|
|
15
|
+
* │
|
|
16
|
+
* admin taps
|
|
17
|
+
* │
|
|
18
|
+
* stranger's session updated · stranger gets DM
|
|
19
|
+
*
|
|
20
|
+
* **Storage layout.** Per-user state lives in its own key, written
|
|
21
|
+
* through `@gramio/session` so the gate read on the hot path costs
|
|
22
|
+
* nothing extra (session is loaded for the user already). A single
|
|
23
|
+
* tiny index key keeps track of who's pending / approved / denied so
|
|
24
|
+
* the `/access` admin menu can list without scanning the whole DB.
|
|
25
|
+
*
|
|
26
|
+
* storage:
|
|
27
|
+
* access:<userId> → AccessRecord (the user's session)
|
|
28
|
+
* ac:index → { pending, approved, denied }
|
|
29
|
+
*
|
|
30
|
+
* **Cross-user mutations.** When you tap `[✅ Aprobar]` on Pepe's
|
|
31
|
+
* notification, ctx is *yours* (the admin), so `ctx.access` is your
|
|
32
|
+
* own record. To mutate Pepe's record we hit the storage at the same
|
|
33
|
+
* key format we registered the session with (`access:<id>`) and
|
|
34
|
+
* update the index. This isn't a hack — it's our own module
|
|
35
|
+
* coordinating with itself.
|
|
36
|
+
*
|
|
37
|
+
* **Composes with `adminContext`** (kit.ts) — that plugin must be
|
|
38
|
+
* extended first or `bot.start()` throws. Inside this plugin,
|
|
39
|
+
* `ctx.adminId` and `ctx.isAdmin` are typed.
|
|
40
|
+
*
|
|
41
|
+
* Peer deps: `gramio`, `@gramio/storage`, `@gramio/session`.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { Bot } from 'gramio'
|
|
45
|
+
* import { redisStorage } from '@gramio/storage-redis'
|
|
46
|
+
* import { adminContext, gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
47
|
+
* import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
48
|
+
*
|
|
49
|
+
* const storage = redisStorage()
|
|
50
|
+
*
|
|
51
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
52
|
+
* .extend(adminContext({ adminId: 190202471 }))
|
|
53
|
+
* .extend(accessControl({ storage, defaults: [1158734055] }))
|
|
54
|
+
* .command('start', (ctx) => ctx.send(`hola, source=${ctx.access.source}`))
|
|
55
|
+
*
|
|
56
|
+
* await gracefulStart(bot)
|
|
57
|
+
*/
|
|
58
|
+
import { CallbackData, InlineKeyboard, Plugin, } from 'gramio';
|
|
59
|
+
import { session } from '@gramio/session';
|
|
60
|
+
import { inMemoryStorage } from '@gramio/storage';
|
|
61
|
+
const SESSION_KEY_PREFIX = 'access:';
|
|
62
|
+
const INDEX_KEY = 'ac:index';
|
|
63
|
+
const FIRST_MSG_LIMIT = 200;
|
|
64
|
+
const DEFAULT_DENY_MSG = 'Este bot es privado. Tu solicitud se ha enviado al admin.';
|
|
65
|
+
const DEFAULT_THROTTLE_MS = 6 * 60 * 60 * 1000;
|
|
66
|
+
const userSessionKey = (userId) => `${SESSION_KEY_PREFIX}${userId}`;
|
|
67
|
+
// ─── callback schemas ──────────────────────────────────────────────
|
|
68
|
+
//
|
|
69
|
+
// Short `nameId`s keep callback_data under Telegram's 64-byte cap.
|
|
70
|
+
// `v` (optional) carries the originating list view ('pending' | 'denied'
|
|
71
|
+
// | 'approved'). When present, the handler refreshes that list after
|
|
72
|
+
// the action; absent = original notification, edits the message inline.
|
|
73
|
+
const acApprove = new CallbackData('acA').number('uid').string('v', { optional: true });
|
|
74
|
+
const acDeny = new CallbackData('acD').number('uid').string('v', { optional: true });
|
|
75
|
+
const acRevoke = new CallbackData('acR').number('uid');
|
|
76
|
+
const acView = new CallbackData('acV').string('v'); // main | approved | pending | denied
|
|
77
|
+
const acClose = new CallbackData('acC');
|
|
78
|
+
// ─── small helpers ─────────────────────────────────────────────────
|
|
79
|
+
const formatUser = (u, fallbackId) => {
|
|
80
|
+
if (!u)
|
|
81
|
+
return `id ${fallbackId}`;
|
|
82
|
+
const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || `id ${u.id}`;
|
|
83
|
+
const handle = u.username ? `@${u.username}` : `id ${u.id}`;
|
|
84
|
+
return `${name} (${handle})`;
|
|
85
|
+
};
|
|
86
|
+
const fmtAge = (ms) => {
|
|
87
|
+
const s = Math.floor(ms / 1000);
|
|
88
|
+
if (s < 60)
|
|
89
|
+
return `${s}s`;
|
|
90
|
+
const m = Math.floor(s / 60);
|
|
91
|
+
if (m < 60)
|
|
92
|
+
return `${m}min`;
|
|
93
|
+
const h = Math.floor(m / 60);
|
|
94
|
+
if (h < 24)
|
|
95
|
+
return `${h}h`;
|
|
96
|
+
return `${Math.floor(h / 24)}d`;
|
|
97
|
+
};
|
|
98
|
+
const requestNotificationText = (uid, r, repeat) => {
|
|
99
|
+
const parts = [
|
|
100
|
+
repeat ? '🔁 Acceso re-solicitado' : '🔔 Acceso solicitado',
|
|
101
|
+
'',
|
|
102
|
+
`👤 ${formatUser(r.user, uid)}`,
|
|
103
|
+
`🆔 ${uid}`,
|
|
104
|
+
`⏰ hace ${fmtAge(Date.now() - (r.requestedAt ?? Date.now()))}`,
|
|
105
|
+
];
|
|
106
|
+
if (repeat)
|
|
107
|
+
parts.push(`🔁 intentos: ${(r.rejectedAttempts ?? 0) + 1}`);
|
|
108
|
+
if (r.firstMessage)
|
|
109
|
+
parts.push('', `💬 "${r.firstMessage}"`);
|
|
110
|
+
return parts.join('\n');
|
|
111
|
+
};
|
|
112
|
+
const requestKeyboard = (uid) => new InlineKeyboard()
|
|
113
|
+
.text('✅ Aprobar', acApprove.pack({ uid }))
|
|
114
|
+
.text('❌ Denegar', acDeny.pack({ uid }));
|
|
115
|
+
let warnedMemory = false;
|
|
116
|
+
const warnedMemoryStorage = () => {
|
|
117
|
+
if (!warnedMemory) {
|
|
118
|
+
warnedMemory = true;
|
|
119
|
+
console.warn('[access-control] using inMemoryStorage — approvals will not survive restarts. ' +
|
|
120
|
+
'Pass `storage: redisStorage()` (or sqliteStorage / cloudflareStorage) for persistence.');
|
|
121
|
+
}
|
|
122
|
+
return inMemoryStorage();
|
|
123
|
+
};
|
|
124
|
+
// ─── index helpers ─────────────────────────────────────────────────
|
|
125
|
+
const emptyIndex = () => ({ pending: [], approved: [], denied: [] });
|
|
126
|
+
const loadIndex = async (storage) => {
|
|
127
|
+
const raw = (await storage.get(INDEX_KEY));
|
|
128
|
+
return {
|
|
129
|
+
pending: raw?.pending ?? [],
|
|
130
|
+
approved: raw?.approved ?? [],
|
|
131
|
+
denied: raw?.denied ?? [],
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const saveIndex = (storage, idx) => storage.set(INDEX_KEY, idx);
|
|
135
|
+
const indexAdd = async (storage, bucket, uid) => {
|
|
136
|
+
const idx = await loadIndex(storage);
|
|
137
|
+
if (!idx[bucket].includes(uid))
|
|
138
|
+
idx[bucket].push(uid);
|
|
139
|
+
await saveIndex(storage, idx);
|
|
140
|
+
};
|
|
141
|
+
const indexMove = async (storage, uid, from, to) => {
|
|
142
|
+
const idx = await loadIndex(storage);
|
|
143
|
+
const remove = (list) => {
|
|
144
|
+
const i = list.indexOf(uid);
|
|
145
|
+
if (i >= 0)
|
|
146
|
+
list.splice(i, 1);
|
|
147
|
+
};
|
|
148
|
+
if (from === 'any') {
|
|
149
|
+
remove(idx.pending);
|
|
150
|
+
remove(idx.approved);
|
|
151
|
+
remove(idx.denied);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
remove(idx[from]);
|
|
155
|
+
}
|
|
156
|
+
if (!idx[to].includes(uid))
|
|
157
|
+
idx[to].push(uid);
|
|
158
|
+
await saveIndex(storage, idx);
|
|
159
|
+
};
|
|
160
|
+
// ─── per-user record helpers (cross-user storage access) ───────────
|
|
161
|
+
const loadRecord = async (storage, userId) => (await storage.get(userSessionKey(userId)));
|
|
162
|
+
const saveRecord = (storage, userId, rec) => storage.set(userSessionKey(userId), rec);
|
|
163
|
+
// ─── plugin ────────────────────────────────────────────────────────
|
|
164
|
+
export const accessControl = (opts = {}) => {
|
|
165
|
+
const storage = opts.storage ?? warnedMemoryStorage();
|
|
166
|
+
const defaults = new Set(opts.defaults ?? []);
|
|
167
|
+
const denyMessage = opts.denyMessage === false ? null : (opts.denyMessage ?? DEFAULT_DENY_MSG);
|
|
168
|
+
const throttleMs = opts.notifyThrottleMs ?? DEFAULT_THROTTLE_MS;
|
|
169
|
+
return (new Plugin('@adriangalilea/utils/bot/access-control', {
|
|
170
|
+
dependencies: ['@adriangalilea/utils/bot/admin'],
|
|
171
|
+
})
|
|
172
|
+
// Per-user record lives in this session (storage key `access:<senderId>`).
|
|
173
|
+
// The internal name `_accessSession` is plumbing — consumers read
|
|
174
|
+
// `ctx.access` (computed below) instead.
|
|
175
|
+
.extend(session({
|
|
176
|
+
storage,
|
|
177
|
+
key: '_accessSession',
|
|
178
|
+
getSessionKey: (ctx) => userSessionKey(ctx.senderId ?? 0),
|
|
179
|
+
initial: () => ({ status: 'unknown' }),
|
|
180
|
+
}))
|
|
181
|
+
// Compute the gate decision so handlers can read `ctx.access` ergonomically.
|
|
182
|
+
.derive((ctx) => {
|
|
183
|
+
// Only message + callback_query carry a senderId we can gate on.
|
|
184
|
+
if (!ctx.is('message') && !ctx.is('callback_query')) {
|
|
185
|
+
return { access: { allowed: false, reason: 'no-sender' } };
|
|
186
|
+
}
|
|
187
|
+
const senderId = ctx.from.id;
|
|
188
|
+
if (senderId === ctx.adminId) {
|
|
189
|
+
return { access: { allowed: true, source: 'admin' } };
|
|
190
|
+
}
|
|
191
|
+
if (defaults.has(senderId)) {
|
|
192
|
+
return { access: { allowed: true, source: 'default' } };
|
|
193
|
+
}
|
|
194
|
+
const rec = ctx._accessSession;
|
|
195
|
+
if (rec.status === 'approved') {
|
|
196
|
+
return { access: { allowed: true, source: 'store', record: rec } };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
access: { allowed: false, reason: rec.status },
|
|
200
|
+
};
|
|
201
|
+
})
|
|
202
|
+
// Gate. Authorized passes through; unauthorized triggers admin notify
|
|
203
|
+
// and silent stranger reply, then drops.
|
|
204
|
+
.use(async (ctx, next) => {
|
|
205
|
+
if (ctx.access.allowed) {
|
|
206
|
+
// Activity bump (only for store-approved users — admins/defaults
|
|
207
|
+
// don't have a session record we want to clutter).
|
|
208
|
+
if (ctx.access.source === 'store' && ctx.is('message')) {
|
|
209
|
+
ctx._accessSession.lastActivityAt = Date.now();
|
|
210
|
+
ctx._accessSession.messageCount = (ctx._accessSession.messageCount ?? 0) + 1;
|
|
211
|
+
}
|
|
212
|
+
return next();
|
|
213
|
+
}
|
|
214
|
+
// Acknowledge unauthorized callback queries so the spinner clears.
|
|
215
|
+
if (ctx.is('callback_query')) {
|
|
216
|
+
await ctx.answer({ text: 'Sin acceso.', show_alert: false });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Only message-shaped events have .text/.chat for our notification.
|
|
220
|
+
if (!ctx.is('message'))
|
|
221
|
+
return;
|
|
222
|
+
const userId = ctx.from.id;
|
|
223
|
+
const rec = ctx._accessSession;
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const isFirstRequest = rec.status === 'unknown';
|
|
226
|
+
if (isFirstRequest) {
|
|
227
|
+
rec.status = 'pending';
|
|
228
|
+
rec.user = {
|
|
229
|
+
id: userId,
|
|
230
|
+
firstName: ctx.from.firstName,
|
|
231
|
+
lastName: ctx.from.lastName,
|
|
232
|
+
username: ctx.from.username,
|
|
233
|
+
};
|
|
234
|
+
rec.chatId = ctx.chat.id;
|
|
235
|
+
rec.requestedAt = now;
|
|
236
|
+
rec.firstMessage = ctx.text?.slice(0, FIRST_MSG_LIMIT);
|
|
237
|
+
rec.messageCount = 0;
|
|
238
|
+
rec.rejectedAttempts = 0;
|
|
239
|
+
await indexAdd(storage, 'pending', userId);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
rec.rejectedAttempts = (rec.rejectedAttempts ?? 0) + 1;
|
|
243
|
+
}
|
|
244
|
+
const shouldNotify = isFirstRequest || now - (rec.lastNotifiedAt ?? 0) > throttleMs;
|
|
245
|
+
if (shouldNotify) {
|
|
246
|
+
rec.lastNotifiedAt = now;
|
|
247
|
+
try {
|
|
248
|
+
await ctx.bot.api.sendMessage({
|
|
249
|
+
chat_id: ctx.adminId,
|
|
250
|
+
text: requestNotificationText(userId, rec, !isFirstRequest),
|
|
251
|
+
reply_markup: requestKeyboard(userId),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
console.error('[access-control] failed to notify admin (have you /started the bot from your account?)', e);
|
|
256
|
+
}
|
|
257
|
+
opts.onAccessRequest?.({ user: rec.user, firstMessage: rec.firstMessage });
|
|
258
|
+
}
|
|
259
|
+
if (denyMessage && isFirstRequest) {
|
|
260
|
+
try {
|
|
261
|
+
await ctx.send(denyMessage);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// user blocked the bot — irrelevant
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// do NOT call next — drop
|
|
268
|
+
})
|
|
269
|
+
// ─── admin actions ────────────────────────────────────────
|
|
270
|
+
.callbackQuery(acApprove, async (ctx) => {
|
|
271
|
+
if (!ctx.isAdmin)
|
|
272
|
+
return ctx.answer({ text: 'Solo admin.', show_alert: true });
|
|
273
|
+
const uid = ctx.queryData.uid;
|
|
274
|
+
const rec = await loadRecord(storage, uid);
|
|
275
|
+
if (!rec)
|
|
276
|
+
return ctx.answer({ text: 'No encontrado.' });
|
|
277
|
+
const wasDenied = rec.status === 'denied';
|
|
278
|
+
const wasPending = rec.status === 'pending';
|
|
279
|
+
rec.status = 'approved';
|
|
280
|
+
rec.approvedAt = Date.now();
|
|
281
|
+
rec.approvedBy = ctx.adminId;
|
|
282
|
+
rec.deniedAt = undefined;
|
|
283
|
+
rec.deniedBy = undefined;
|
|
284
|
+
await saveRecord(storage, uid, rec);
|
|
285
|
+
await indexMove(storage, uid, wasPending ? 'pending' : wasDenied ? 'denied' : 'any', 'approved');
|
|
286
|
+
if (rec.chatId !== undefined) {
|
|
287
|
+
try {
|
|
288
|
+
await ctx.bot.api.sendMessage({
|
|
289
|
+
chat_id: rec.chatId,
|
|
290
|
+
text: wasDenied
|
|
291
|
+
? '✅ El admin reconsideró: ya tienes acceso.'
|
|
292
|
+
: '✅ Acceso concedido. Ya puedes usar el bot.',
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// user blocked / chat gone
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
await ctx.answer({ text: '✅ Aprobado' });
|
|
300
|
+
if (ctx.queryData.v) {
|
|
301
|
+
await renderView(ctx, storage, defaults, ctx.queryData.v);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
try {
|
|
305
|
+
await ctx.editText(`✅ Aprobado · ${formatUser(rec.user, uid)}`);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// not always editable
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
opts.onApprove?.({ userId: uid, approvedBy: ctx.adminId });
|
|
312
|
+
})
|
|
313
|
+
.callbackQuery(acDeny, async (ctx) => {
|
|
314
|
+
if (!ctx.isAdmin)
|
|
315
|
+
return ctx.answer({ text: 'Solo admin.', show_alert: true });
|
|
316
|
+
const uid = ctx.queryData.uid;
|
|
317
|
+
const rec = await loadRecord(storage, uid);
|
|
318
|
+
if (!rec)
|
|
319
|
+
return ctx.answer({ text: 'No encontrado.' });
|
|
320
|
+
const wasPending = rec.status === 'pending';
|
|
321
|
+
rec.status = 'denied';
|
|
322
|
+
rec.deniedAt = Date.now();
|
|
323
|
+
rec.deniedBy = ctx.adminId;
|
|
324
|
+
await saveRecord(storage, uid, rec);
|
|
325
|
+
await indexMove(storage, uid, wasPending ? 'pending' : 'any', 'denied');
|
|
326
|
+
if (rec.chatId !== undefined) {
|
|
327
|
+
try {
|
|
328
|
+
await ctx.bot.api.sendMessage({
|
|
329
|
+
chat_id: rec.chatId,
|
|
330
|
+
text: '❌ Acceso denegado.',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// ignore
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await ctx.answer({ text: '❌ Denegado' });
|
|
338
|
+
if (ctx.queryData.v) {
|
|
339
|
+
await renderView(ctx, storage, defaults, ctx.queryData.v);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
try {
|
|
343
|
+
await ctx.editText(`❌ Denegado · ${formatUser(rec.user, uid)}`);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// ignore
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
opts.onDeny?.({ userId: uid, deniedBy: ctx.adminId });
|
|
350
|
+
})
|
|
351
|
+
.callbackQuery(acRevoke, async (ctx) => {
|
|
352
|
+
if (!ctx.isAdmin)
|
|
353
|
+
return ctx.answer({ text: 'Solo admin.', show_alert: true });
|
|
354
|
+
const uid = ctx.queryData.uid;
|
|
355
|
+
const rec = await loadRecord(storage, uid);
|
|
356
|
+
if (!rec)
|
|
357
|
+
return ctx.answer({ text: 'No encontrado.' });
|
|
358
|
+
rec.status = 'denied';
|
|
359
|
+
rec.deniedAt = Date.now();
|
|
360
|
+
rec.deniedBy = ctx.adminId;
|
|
361
|
+
await saveRecord(storage, uid, rec);
|
|
362
|
+
await indexMove(storage, uid, 'approved', 'denied');
|
|
363
|
+
if (rec.chatId !== undefined) {
|
|
364
|
+
try {
|
|
365
|
+
await ctx.bot.api.sendMessage({
|
|
366
|
+
chat_id: rec.chatId,
|
|
367
|
+
text: '↩️ Tu acceso al bot ha sido revocado.',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// ignore
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await ctx.answer({ text: '↩️ Revocado' });
|
|
375
|
+
await renderView(ctx, storage, defaults, 'approved');
|
|
376
|
+
})
|
|
377
|
+
.callbackQuery(acView, async (ctx) => {
|
|
378
|
+
if (!ctx.isAdmin)
|
|
379
|
+
return ctx.answer({ text: 'Solo admin.', show_alert: true });
|
|
380
|
+
await ctx.answer({});
|
|
381
|
+
await renderView(ctx, storage, defaults, ctx.queryData.v);
|
|
382
|
+
})
|
|
383
|
+
.callbackQuery(acClose, async (ctx) => {
|
|
384
|
+
if (!ctx.isAdmin)
|
|
385
|
+
return ctx.answer({ text: 'Solo admin.', show_alert: true });
|
|
386
|
+
await ctx.answer({});
|
|
387
|
+
try {
|
|
388
|
+
await ctx.message?.delete();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
.command('access', async (ctx) => {
|
|
395
|
+
if (!ctx.isAdmin)
|
|
396
|
+
return;
|
|
397
|
+
const v = await mainView(storage, defaults);
|
|
398
|
+
await ctx.send(v.text, { reply_markup: v.keyboard });
|
|
399
|
+
}));
|
|
400
|
+
};
|
|
401
|
+
const renderView = async (ctx, storage, defaults, view) => {
|
|
402
|
+
const v = view === 'approved'
|
|
403
|
+
? await listView(storage, 'approved', defaults)
|
|
404
|
+
: view === 'pending'
|
|
405
|
+
? await listView(storage, 'pending', defaults)
|
|
406
|
+
: view === 'denied'
|
|
407
|
+
? await listView(storage, 'denied', defaults)
|
|
408
|
+
: await mainView(storage, defaults);
|
|
409
|
+
try {
|
|
410
|
+
await ctx.editText(v.text, { reply_markup: v.keyboard });
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// editText only works while message is recent enough; ignore
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const mainView = async (storage, defaults) => {
|
|
417
|
+
const idx = await loadIndex(storage);
|
|
418
|
+
const text = [
|
|
419
|
+
'🔐 Access Control',
|
|
420
|
+
'',
|
|
421
|
+
`✅ Aprobados: ${idx.approved.length}`,
|
|
422
|
+
`⏳ Pendientes: ${idx.pending.length}`,
|
|
423
|
+
`❌ Denegados: ${idx.denied.length}`,
|
|
424
|
+
`👑 Defaults: ${defaults.size} (hardcoded)`,
|
|
425
|
+
].join('\n');
|
|
426
|
+
const keyboard = new InlineKeyboard()
|
|
427
|
+
.text(`✅ Aprobados (${idx.approved.length})`, acView.pack({ v: 'approved' }))
|
|
428
|
+
.text(`⏳ Pendientes (${idx.pending.length})`, acView.pack({ v: 'pending' }))
|
|
429
|
+
.row()
|
|
430
|
+
.text(`❌ Denegados (${idx.denied.length})`, acView.pack({ v: 'denied' }))
|
|
431
|
+
.text('🔄 Refresh', acView.pack({ v: 'main' }))
|
|
432
|
+
.row()
|
|
433
|
+
.text('✖️ Cerrar', acClose.pack({}));
|
|
434
|
+
return { text, keyboard };
|
|
435
|
+
};
|
|
436
|
+
const listView = async (storage, filter, defaults) => {
|
|
437
|
+
const idx = await loadIndex(storage);
|
|
438
|
+
const ids = idx[filter];
|
|
439
|
+
// Cap at 20 to keep callback_data + rendering sane.
|
|
440
|
+
const shownIds = ids.slice(0, 20);
|
|
441
|
+
const records = await Promise.all(shownIds.map(async (id) => ({ id, rec: await loadRecord(storage, id) })));
|
|
442
|
+
const headerEmoji = filter === 'approved' ? '✅' : filter === 'pending' ? '⏳' : '❌';
|
|
443
|
+
const headerLabel = filter === 'approved' ? 'Aprobados' : filter === 'pending' ? 'Pendientes' : 'Denegados';
|
|
444
|
+
if (ids.length === 0) {
|
|
445
|
+
const text = `${headerEmoji} ${headerLabel} (0)\n\n(vacío)`;
|
|
446
|
+
const keyboard = new InlineKeyboard().text('⬅️ Volver', acView.pack({ v: 'main' }));
|
|
447
|
+
return { text, keyboard };
|
|
448
|
+
}
|
|
449
|
+
const lines = [`${headerEmoji} ${headerLabel} (${ids.length})`, ''];
|
|
450
|
+
const keyboard = new InlineKeyboard();
|
|
451
|
+
for (let i = 0; i < records.length; i++) {
|
|
452
|
+
const { id, rec } = records[i];
|
|
453
|
+
if (!rec) {
|
|
454
|
+
// index referenced a missing record — show as placeholder
|
|
455
|
+
lines.push(`${i + 1}. id ${id} (datos perdidos)`);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const ageRef = rec.approvedAt ?? rec.deniedAt ?? rec.requestedAt ?? Date.now();
|
|
459
|
+
lines.push(`${i + 1}. ${formatUser(rec.user, id)} · hace ${fmtAge(Date.now() - ageRef)}` +
|
|
460
|
+
(rec.messageCount ? ` · ${rec.messageCount} msgs` : ''));
|
|
461
|
+
if (filter === 'pending') {
|
|
462
|
+
keyboard
|
|
463
|
+
.text(`✅ ${i + 1}`, acApprove.pack({ uid: id, v: 'pending' }))
|
|
464
|
+
.text(`❌ ${i + 1}`, acDeny.pack({ uid: id, v: 'pending' }))
|
|
465
|
+
.row();
|
|
466
|
+
}
|
|
467
|
+
else if (filter === 'approved') {
|
|
468
|
+
keyboard.text(`↩️ Revocar #${i + 1}`, acRevoke.pack({ uid: id })).row();
|
|
469
|
+
}
|
|
470
|
+
else if (filter === 'denied') {
|
|
471
|
+
keyboard
|
|
472
|
+
.text(`✅ Reaprobar #${i + 1}`, acApprove.pack({ uid: id, v: 'denied' }))
|
|
473
|
+
.row();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (ids.length > shownIds.length) {
|
|
477
|
+
lines.push('', `(+${ids.length - shownIds.length} más, no mostrados)`);
|
|
478
|
+
}
|
|
479
|
+
if (filter === 'approved' && defaults.size > 0) {
|
|
480
|
+
lines.push('', `+ ${defaults.size} hardcoded defaults`);
|
|
481
|
+
}
|
|
482
|
+
keyboard.text('⬅️ Volver', acView.pack({ v: 'main' }));
|
|
483
|
+
return { text: lines.join('\n'), keyboard };
|
|
484
|
+
};
|
|
485
|
+
// ─── test helper ───────────────────────────────────────────────────
|
|
486
|
+
/**
|
|
487
|
+
* Inject a synthetic access request — for tests/demos when you can't
|
|
488
|
+
* easily spin up a second Telegram account. Writes a `pending` record
|
|
489
|
+
* to storage at the same key the plugin's session would, updates the
|
|
490
|
+
* index, then DMs the admin with the real
|
|
491
|
+
* `[✅ Aprobar][❌ Denegar]` keyboard. Tapping those buttons exercises
|
|
492
|
+
* the real callback handlers end-to-end.
|
|
493
|
+
*
|
|
494
|
+
* Pass the SAME `storage` instance you passed to `accessControl({ storage })`.
|
|
495
|
+
*/
|
|
496
|
+
export const simulateAccessRequest = async (bot, storage, adminId, fakeUser, message) => {
|
|
497
|
+
const now = Date.now();
|
|
498
|
+
const rec = {
|
|
499
|
+
status: 'pending',
|
|
500
|
+
user: fakeUser,
|
|
501
|
+
chatId: fakeUser.id,
|
|
502
|
+
requestedAt: now,
|
|
503
|
+
firstMessage: message?.slice(0, FIRST_MSG_LIMIT),
|
|
504
|
+
messageCount: 0,
|
|
505
|
+
rejectedAttempts: 0,
|
|
506
|
+
lastNotifiedAt: now,
|
|
507
|
+
};
|
|
508
|
+
await saveRecord(storage, fakeUser.id, rec);
|
|
509
|
+
await indexAdd(storage, 'pending', fakeUser.id);
|
|
510
|
+
await bot.api.sendMessage({
|
|
511
|
+
chat_id: adminId,
|
|
512
|
+
text: requestNotificationText(fakeUser.id, rec, false),
|
|
513
|
+
reply_markup: requestKeyboard(fakeUser.id),
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
//# sourceMappingURL=access-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesce client-split inbound messages.
|
|
3
|
+
*
|
|
4
|
+
* Telegram clients (Desktop / iOS / web) split a single message > 4096
|
|
5
|
+
* chars into multiple `sendMessage` calls before they ever reach the
|
|
6
|
+
* server. The bot receives them as **separate** `message` updates with
|
|
7
|
+
* no marker linking them. This middleware joins them back into one
|
|
8
|
+
* event so your handlers see the full text.
|
|
9
|
+
*
|
|
10
|
+
* user pastes 8000 chars → client splits in 2 → bot gets 2 updates
|
|
11
|
+
* │
|
|
12
|
+
* ▼
|
|
13
|
+
* coalesce middleware
|
|
14
|
+
* │
|
|
15
|
+
* hold + join
|
|
16
|
+
* │
|
|
17
|
+
* ▼
|
|
18
|
+
* handler sees ONE event
|
|
19
|
+
* with full ctx.text
|
|
20
|
+
*
|
|
21
|
+
* ## Detection rule (strict)
|
|
22
|
+
*
|
|
23
|
+
* We coalesce only when ALL hold. Otherwise fragments pass through
|
|
24
|
+
* as separate events — false negatives are preferred over silently
|
|
25
|
+
* merging unrelated messages.
|
|
26
|
+
*
|
|
27
|
+
* 1. Same chat.
|
|
28
|
+
* 2. Same user (override with `acrossUsers: true`).
|
|
29
|
+
* 3. Leading fragment length ≥ `minLeadingLength` (a current
|
|
30
|
+
* guess — see the type definition for the default and the
|
|
31
|
+
* reasoning). Short messages never start a real client split.
|
|
32
|
+
* 4. Each subsequent fragment within `windowMs` of the previous.
|
|
33
|
+
*
|
|
34
|
+
* ## Known caveats
|
|
35
|
+
*
|
|
36
|
+
* - `ctx.entities` is cleared on coalesced messages — per-fragment
|
|
37
|
+
* entity offsets would point at the wrong characters once joined.
|
|
38
|
+
* Plain-text consumers don't care; formatted-input consumers
|
|
39
|
+
* should disable this plugin.
|
|
40
|
+
* - In-memory buffer; doesn't survive bot restart mid-burst.
|
|
41
|
+
*
|
|
42
|
+
* Peer deps: `gramio`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* import { Bot } from 'gramio'
|
|
46
|
+
* import { coalesceLongMessages } from '@adriangalilea/utils/bot/coalesce'
|
|
47
|
+
*
|
|
48
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
49
|
+
* .extend(coalesceLongMessages()) // ← before .on / .command handlers
|
|
50
|
+
* .on('message', (ctx) => {
|
|
51
|
+
* // ctx.text is the full pasted text even if Telegram split it
|
|
52
|
+
* return ctx.send(`got ${ctx.text?.length} chars`)
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* @example Power-user escape hatch
|
|
56
|
+
*
|
|
57
|
+
* import { isCoalescent } from '@adriangalilea/utils/bot/coalesce'
|
|
58
|
+
*
|
|
59
|
+
* if (isCoalescent(prev, curr)) {
|
|
60
|
+
* // do your own thing
|
|
61
|
+
* }
|
|
62
|
+
*/
|
|
63
|
+
import { Plugin } from 'gramio';
|
|
64
|
+
export type CoalesceCriteria = {
|
|
65
|
+
/**
|
|
66
|
+
* Minimum length of the leading fragment to consider a possible
|
|
67
|
+
* client split. Below this → never coalesce, zero latency. Once
|
|
68
|
+
* the buffer is open, follow-up fragments of any size join.
|
|
69
|
+
*/
|
|
70
|
+
minLeadingLength?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Max ms between consecutive fragments to consider them part of
|
|
73
|
+
* one client-split burst.
|
|
74
|
+
*/
|
|
75
|
+
windowMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* If true, fragments from different users (same chat) can coalesce.
|
|
78
|
+
* Useful for "user forwarded a multi-author conversation as one
|
|
79
|
+
* logical block."
|
|
80
|
+
*/
|
|
81
|
+
acrossUsers?: boolean;
|
|
82
|
+
};
|
|
83
|
+
export type CoalesceLongMessagesOptions = CoalesceCriteria & {
|
|
84
|
+
/**
|
|
85
|
+
* Log each fragment + buffer transition to stderr for debugging.
|
|
86
|
+
* Off by default.
|
|
87
|
+
*/
|
|
88
|
+
log?: boolean;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Pure check: are these two fragments part of the same client-split
|
|
92
|
+
* burst? Use this if you want to make your own decision instead of
|
|
93
|
+
* letting the middleware do it.
|
|
94
|
+
*
|
|
95
|
+
* The two fragments are passed as plain objects so this function is
|
|
96
|
+
* decoupled from gramio's context type. Adapt your context as needed.
|
|
97
|
+
*/
|
|
98
|
+
export type CoalesceFragment = {
|
|
99
|
+
text: string;
|
|
100
|
+
chatId: number;
|
|
101
|
+
userId: number;
|
|
102
|
+
/** Timestamp in **milliseconds** (use `Date.now()` or `msg.date * 1000`). */
|
|
103
|
+
dateMs: number;
|
|
104
|
+
};
|
|
105
|
+
export declare const isCoalescent: (prev: CoalesceFragment, curr: CoalesceFragment, opts?: CoalesceCriteria) => boolean;
|
|
106
|
+
export declare const coalesceLongMessages: (opts?: CoalesceLongMessagesOptions) => Plugin<{}, import("gramio").DeriveDefinitions, {}>;
|
|
107
|
+
//# sourceMappingURL=coalesce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/bot/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAW/B,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG,gBAAgB,GAAG;IAC3D;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAA;CACd,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,eAAO,MAAM,YAAY,GACvB,MAAM,gBAAgB,EACtB,MAAM,gBAAgB,EACtB,OAAM,gBAAqB,KAC1B,OAUF,CAAA;AAWD,eAAO,MAAM,oBAAoB,GAAI,OAAM,2BAAgC,uDA8E1E,CAAA"}
|