@adriangalilea/utils 0.16.1 โ†’ 0.16.2

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.
Files changed (86) hide show
  1. package/dist/bot/access-control.d.ts +7 -7
  2. package/dist/bot/access-control.d.ts.map +1 -1
  3. package/dist/bot/access-control.js +144 -121
  4. package/dist/bot/access-control.js.map +1 -1
  5. package/dist/bot/coalesce.d.ts +1 -1
  6. package/dist/bot/coalesce.d.ts.map +1 -1
  7. package/dist/bot/coalesce.js +8 -4
  8. package/dist/bot/coalesce.js.map +1 -1
  9. package/dist/bot/index.d.ts +6 -6
  10. package/dist/bot/index.d.ts.map +1 -1
  11. package/dist/bot/index.js +6 -6
  12. package/dist/bot/index.js.map +1 -1
  13. package/dist/bot/kit.d.ts +2 -2
  14. package/dist/bot/kit.d.ts.map +1 -1
  15. package/dist/bot/kit.js +11 -11
  16. package/dist/bot/kit.js.map +1 -1
  17. package/dist/bot/language.d.ts +6 -6
  18. package/dist/bot/language.d.ts.map +1 -1
  19. package/dist/bot/language.js +51 -33
  20. package/dist/bot/language.js.map +1 -1
  21. package/dist/bot/llm.d.ts +12 -12
  22. package/dist/bot/llm.d.ts.map +1 -1
  23. package/dist/bot/llm.js +46 -42
  24. package/dist/bot/llm.js.map +1 -1
  25. package/dist/bot/menu.d.ts +5 -5
  26. package/dist/bot/menu.d.ts.map +1 -1
  27. package/dist/bot/menu.js +70 -67
  28. package/dist/bot/menu.js.map +1 -1
  29. package/dist/browser.d.ts +6 -6
  30. package/dist/browser.d.ts.map +1 -1
  31. package/dist/browser.js +6 -6
  32. package/dist/browser.js.map +1 -1
  33. package/dist/index.d.ts +14 -14
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +16 -16
  36. package/dist/index.js.map +1 -1
  37. package/dist/offensive.d.ts.map +1 -1
  38. package/dist/offensive.js +17 -11
  39. package/dist/offensive.js.map +1 -1
  40. package/dist/platform/dir.d.ts.map +1 -1
  41. package/dist/platform/dir.js +8 -8
  42. package/dist/platform/dir.js.map +1 -1
  43. package/dist/platform/file.d.ts.map +1 -1
  44. package/dist/platform/file.js +20 -17
  45. package/dist/platform/file.js.map +1 -1
  46. package/dist/platform/kev.d.ts.map +1 -1
  47. package/dist/platform/kev.js +113 -106
  48. package/dist/platform/kev.js.map +1 -1
  49. package/dist/platform/path.d.ts.map +1 -1
  50. package/dist/platform/path.js +5 -5
  51. package/dist/platform/path.js.map +1 -1
  52. package/dist/platform/project.d.ts.map +1 -1
  53. package/dist/platform/project.js +19 -19
  54. package/dist/platform/project.js.map +1 -1
  55. package/dist/platform/unseen.d.ts.map +1 -1
  56. package/dist/platform/unseen.js +8 -6
  57. package/dist/platform/unseen.js.map +1 -1
  58. package/dist/platform/xdg.d.ts.map +1 -1
  59. package/dist/platform/xdg.js +7 -7
  60. package/dist/platform/xdg.js.map +1 -1
  61. package/dist/runtime.d.ts.map +1 -1
  62. package/dist/runtime.js +35 -22
  63. package/dist/runtime.js.map +1 -1
  64. package/dist/say/index.d.ts.map +1 -1
  65. package/dist/say/index.js +2 -2
  66. package/dist/say/index.js.map +1 -1
  67. package/dist/universal/currency/crypto-symbols-data.d.ts.map +1 -1
  68. package/dist/universal/currency/crypto-symbols-data.js +1 -1
  69. package/dist/universal/currency/crypto-symbols-data.js.map +1 -1
  70. package/dist/universal/currency/crypto-symbols.d.ts.map +1 -1
  71. package/dist/universal/currency/crypto-symbols.js +1 -1
  72. package/dist/universal/currency/crypto-symbols.js.map +1 -1
  73. package/dist/universal/currency/download-crypto-list.d.ts.map +1 -1
  74. package/dist/universal/currency/download-crypto-list.js +17 -17
  75. package/dist/universal/currency/download-crypto-list.js.map +1 -1
  76. package/dist/universal/currency/index.d.ts.map +1 -1
  77. package/dist/universal/currency/index.js +117 -80
  78. package/dist/universal/currency/index.js.map +1 -1
  79. package/dist/universal/format.d.ts.map +1 -1
  80. package/dist/universal/format.js +10 -10
  81. package/dist/universal/format.js.map +1 -1
  82. package/dist/universal/log.d.ts +1 -1
  83. package/dist/universal/log.d.ts.map +1 -1
  84. package/dist/universal/log.js +66 -60
  85. package/dist/universal/log.js.map +1 -1
  86. package/package.json +146 -141
@@ -74,12 +74,12 @@
74
74
  *
75
75
  * await gracefulStart(bot)
76
76
  */
77
- import { CallbackData, InlineKeyboard, Plugin, } from 'gramio';
78
- import { say } from '../say/index.js';
79
- const INDEX_KEY = 'ac:index';
77
+ import { CallbackData, InlineKeyboard, Plugin, } from "gramio";
78
+ import { say } from "../say/index.js";
79
+ const INDEX_KEY = "ac:index";
80
80
  const FIRST_MSG_LIMIT = 200;
81
81
  const DEFAULT_THROTTLE_MS = 6 * 60 * 60 * 1000;
82
- const FALLBACK_LANG = 'en';
82
+ const FALLBACK_LANG = "en";
83
83
  /** gramio's `@gramio/session` default `getSessionKey` is `String(senderId)`. */
84
84
  const sessionKey = (userId) => String(userId);
85
85
  // โ”€โ”€โ”€ callback schemas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -88,16 +88,20 @@ const sessionKey = (userId) => String(userId);
88
88
  // `v` (optional) carries the originating list view ('pending' | 'denied'
89
89
  // | 'approved'). When present, the handler refreshes that list after
90
90
  // the action; absent = original notification, edits the message inline.
91
- const acApprove = new CallbackData('acA').number('uid').string('v', { optional: true });
92
- const acDeny = new CallbackData('acD').number('uid').string('v', { optional: true });
93
- const acRevoke = new CallbackData('acR').number('uid');
94
- const acView = new CallbackData('acV').string('v'); // main | approved | pending | denied
95
- const acClose = new CallbackData('acC');
91
+ const acApprove = new CallbackData("acA")
92
+ .number("uid")
93
+ .string("v", { optional: true });
94
+ const acDeny = new CallbackData("acD")
95
+ .number("uid")
96
+ .string("v", { optional: true });
97
+ const acRevoke = new CallbackData("acR").number("uid");
98
+ const acView = new CallbackData("acV").string("v"); // main | approved | pending | denied
99
+ const acClose = new CallbackData("acC");
96
100
  // โ”€โ”€โ”€ small helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
97
101
  const formatUser = (u, fallbackId) => {
98
102
  if (!u)
99
103
  return `id ${fallbackId}`;
100
- const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || `id ${u.id}`;
104
+ const name = [u.firstName, u.lastName].filter(Boolean).join(" ") || `id ${u.id}`;
101
105
  const handle = u.username ? `@${u.username}` : `id ${u.id}`;
102
106
  return `${name} (${handle})`;
103
107
  };
@@ -116,23 +120,23 @@ const fmtAge = (ms) => {
116
120
  const requestNotificationText = (uid, r, repeat, lang) => {
117
121
  const parts = [
118
122
  say(repeat
119
- ? { en: '๐Ÿ” Access re-requested', es: '๐Ÿ” Acceso re-solicitado' }
120
- : { en: '๐Ÿ”” Access requested', es: '๐Ÿ”” Acceso solicitado' }, lang),
121
- '',
123
+ ? { en: "๐Ÿ” Access re-requested", es: "๐Ÿ” Acceso re-solicitado" }
124
+ : { en: "๐Ÿ”” Access requested", es: "๐Ÿ”” Acceso solicitado" }, lang),
125
+ "",
122
126
  `๐Ÿ‘ค ${formatUser(r.user, uid)}`,
123
127
  `๐Ÿ†” ${uid}`,
124
- `โฐ ${say({ en: 'ago', es: 'hace' }, lang)} ${fmtAge(Date.now() - (r.requestedAt ?? Date.now()))}`,
128
+ `โฐ ${say({ en: "ago", es: "hace" }, lang)} ${fmtAge(Date.now() - (r.requestedAt ?? Date.now()))}`,
125
129
  ];
126
130
  if (repeat) {
127
- parts.push(`๐Ÿ” ${say({ en: 'attempts', es: 'intentos' }, lang)}: ${(r.rejectedAttempts ?? 0) + 1}`);
131
+ parts.push(`๐Ÿ” ${say({ en: "attempts", es: "intentos" }, lang)}: ${(r.rejectedAttempts ?? 0) + 1}`);
128
132
  }
129
133
  if (r.firstMessage)
130
- parts.push('', `๐Ÿ’ฌ "${r.firstMessage}"`);
131
- return parts.join('\n');
134
+ parts.push("", `๐Ÿ’ฌ "${r.firstMessage}"`);
135
+ return parts.join("\n");
132
136
  };
133
137
  const requestKeyboard = (uid, lang) => new InlineKeyboard()
134
- .text(say({ en: 'โœ… Approve', es: 'โœ… Aprobar' }, lang), acApprove.pack({ uid }), { style: 'success' })
135
- .text(say({ en: 'โŒ Deny', es: 'โŒ Denegar' }, lang), acDeny.pack({ uid }), { style: 'danger' });
138
+ .text(say({ en: "โœ… Approve", es: "โœ… Aprobar" }, lang), acApprove.pack({ uid }), { style: "success" })
139
+ .text(say({ en: "โŒ Deny", es: "โŒ Denegar" }, lang), acDeny.pack({ uid }), { style: "danger" });
136
140
  // โ”€โ”€โ”€ index helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
137
141
  const loadIndex = async (storage) => {
138
142
  const raw = (await storage.get(INDEX_KEY));
@@ -156,7 +160,7 @@ const indexMove = async (storage, uid, from, to) => {
156
160
  if (i >= 0)
157
161
  list.splice(i, 1);
158
162
  };
159
- if (from === 'any') {
163
+ if (from === "any") {
160
164
  remove(idx.pending);
161
165
  remove(idx.approved);
162
166
  remove(idx.denied);
@@ -168,7 +172,8 @@ const indexMove = async (storage, uid, from, to) => {
168
172
  idx[to].push(uid);
169
173
  await saveIndex(storage, idx);
170
174
  };
171
- const loadFullRecord = async (storage, userId) => (await storage.get(sessionKey(userId))) ?? {};
175
+ const loadFullRecord = async (storage, userId) => (await storage.get(sessionKey(userId))) ??
176
+ {};
172
177
  const saveAccess = async (storage, userId, rec) => {
173
178
  const full = await loadFullRecord(storage, userId);
174
179
  full.access = rec;
@@ -196,8 +201,8 @@ export const accessControl = (opts) => {
196
201
  // ctx.adminId / ctx.isAdmin are typed inside our handlers.
197
202
  // Session-side types flow through `.extend(opts.session)` below
198
203
  // and TypeScript merges them with our generic via the chain.
199
- new Plugin('@adriangalilea/utils/bot/access-control', {
200
- dependencies: ['@adriangalilea/utils/bot/admin'],
204
+ new Plugin("@adriangalilea/utils/bot/access-control", {
205
+ dependencies: ["@adriangalilea/utils/bot/admin"],
201
206
  })
202
207
  // Declare the shared session as a dependency. gramio's runtime
203
208
  // dedups against the bot's top-level extension; types flow.
@@ -205,21 +210,36 @@ export const accessControl = (opts) => {
205
210
  // Compute the gate decision so handlers can read `ctx.access` ergonomically.
206
211
  .derive((ctx) => {
207
212
  // Only message + callback_query carry a senderId we can gate on.
208
- if (!ctx.is('message') && !ctx.is('callback_query')) {
209
- return { access: { allowed: false, reason: 'no-sender' } };
213
+ if (!ctx.is("message") && !ctx.is("callback_query")) {
214
+ return {
215
+ access: {
216
+ allowed: false,
217
+ reason: "no-sender",
218
+ },
219
+ };
210
220
  }
211
221
  const senderId = ctx.from.id;
212
222
  if (senderId === ctx.adminId) {
213
- return { access: { allowed: true, source: 'admin' } };
223
+ return {
224
+ access: { allowed: true, source: "admin" },
225
+ };
214
226
  }
215
227
  if (defaults.has(senderId)) {
216
- return { access: { allowed: true, source: 'default' } };
228
+ return {
229
+ access: { allowed: true, source: "default" },
230
+ };
217
231
  }
218
232
  // ctx.session.access may be undefined for first-ever interaction
219
233
  // (session.initial() returns {} so .access isn't set yet).
220
- const rec = ctx.session.access ?? { status: 'unknown' };
221
- if (rec.status === 'approved') {
222
- return { access: { allowed: true, source: 'store', record: rec } };
234
+ const rec = ctx.session.access ?? { status: "unknown" };
235
+ if (rec.status === "approved") {
236
+ return {
237
+ access: {
238
+ allowed: true,
239
+ source: "store",
240
+ record: rec,
241
+ },
242
+ };
223
243
  }
224
244
  return {
225
245
  access: { allowed: false, reason: rec.status },
@@ -231,8 +251,8 @@ export const accessControl = (opts) => {
231
251
  if (ctx.access.allowed) {
232
252
  // Activity bump (only for store-approved users โ€” admins/defaults
233
253
  // don't have a session record we want to clutter).
234
- if (ctx.access.source === 'store' &&
235
- ctx.is('message') &&
254
+ if (ctx.access.source === "store" &&
255
+ ctx.is("message") &&
236
256
  ctx.session.access) {
237
257
  ctx.session.access = {
238
258
  ...ctx.session.access,
@@ -243,23 +263,23 @@ export const accessControl = (opts) => {
243
263
  return next();
244
264
  }
245
265
  // Acknowledge unauthorized callback queries so the spinner clears.
246
- if (ctx.is('callback_query')) {
266
+ if (ctx.is("callback_query")) {
247
267
  await ctx.answer({
248
- text: say({ en: 'No access.', es: 'Sin acceso.' }, ctxLang(ctx)),
268
+ text: say({ en: "No access.", es: "Sin acceso." }, ctxLang(ctx)),
249
269
  show_alert: false,
250
270
  });
251
271
  return;
252
272
  }
253
273
  // Only message-shaped events have .text/.chat for our notification.
254
- if (!ctx.is('message'))
274
+ if (!ctx.is("message"))
255
275
  return;
256
276
  const userId = ctx.from.id;
257
277
  const existing = ctx.session.access;
258
278
  const now = Date.now();
259
- const isFirstRequest = !existing || existing.status === 'unknown';
279
+ const isFirstRequest = !existing || existing.status === "unknown";
260
280
  const rec = isFirstRequest
261
281
  ? {
262
- status: 'pending',
282
+ status: "pending",
263
283
  user: {
264
284
  id: userId,
265
285
  firstName: ctx.from.firstName,
@@ -274,7 +294,7 @@ export const accessControl = (opts) => {
274
294
  }
275
295
  : { ...existing };
276
296
  if (isFirstRequest) {
277
- await indexAdd(storage, 'pending', userId);
297
+ await indexAdd(storage, "pending", userId);
278
298
  }
279
299
  else {
280
300
  rec.rejectedAttempts = (rec.rejectedAttempts ?? 0) + 1;
@@ -291,17 +311,20 @@ export const accessControl = (opts) => {
291
311
  });
292
312
  }
293
313
  catch (e) {
294
- console.error('[access-control] failed to notify admin (have you /started the bot from your account?)', e);
314
+ console.error("[access-control] failed to notify admin (have you /started the bot from your account?)", e);
295
315
  }
296
- opts.onAccessRequest?.({ user: rec.user, firstMessage: rec.firstMessage });
316
+ opts.onAccessRequest?.({
317
+ user: rec.user,
318
+ firstMessage: rec.firstMessage,
319
+ });
297
320
  }
298
321
  // Persist the updated record to the user's session.
299
322
  ctx.session.access = rec;
300
323
  if (!silentDeny && isFirstRequest) {
301
324
  try {
302
325
  await ctx.send(say({
303
- en: 'This bot is private. Your request has been sent to the admin.',
304
- es: 'Este bot es privado. Tu solicitud se ha enviado al admin.',
326
+ en: "This bot is private. Your request has been sent to the admin.",
327
+ es: "Este bot es privado. Tu solicitud se ha enviado al admin.",
305
328
  }, ctxLang(ctx)));
306
329
  }
307
330
  catch {
@@ -315,24 +338,24 @@ export const accessControl = (opts) => {
315
338
  const aLang = ctxLang(ctx);
316
339
  if (!ctx.isAdmin)
317
340
  return ctx.answer({
318
- text: say({ en: 'Admin only.', es: 'Solo admin.' }, aLang),
341
+ text: say({ en: "Admin only.", es: "Solo admin." }, aLang),
319
342
  show_alert: true,
320
343
  });
321
344
  const uid = ctx.queryData.uid;
322
345
  const rec = await loadAccess(storage, uid);
323
346
  if (!rec)
324
347
  return ctx.answer({
325
- text: say({ en: 'Not found.', es: 'No encontrado.' }, aLang),
348
+ text: say({ en: "Not found.", es: "No encontrado." }, aLang),
326
349
  });
327
- const wasDenied = rec.status === 'denied';
328
- const wasPending = rec.status === 'pending';
329
- rec.status = 'approved';
350
+ const wasDenied = rec.status === "denied";
351
+ const wasPending = rec.status === "pending";
352
+ rec.status = "approved";
330
353
  rec.approvedAt = Date.now();
331
354
  rec.approvedBy = ctx.adminId;
332
355
  rec.deniedAt = undefined;
333
356
  rec.deniedBy = undefined;
334
357
  await saveAccess(storage, uid, rec);
335
- await indexMove(storage, uid, wasPending ? 'pending' : wasDenied ? 'denied' : 'any', 'approved');
358
+ await indexMove(storage, uid, wasPending ? "pending" : wasDenied ? "denied" : "any", "approved");
336
359
  if (rec.chatId !== undefined) {
337
360
  try {
338
361
  const sLang = await langOfUser(storage, uid);
@@ -340,12 +363,12 @@ export const accessControl = (opts) => {
340
363
  chat_id: rec.chatId,
341
364
  text: say(wasDenied
342
365
  ? {
343
- en: 'โœ… The admin reconsidered: you have access.',
344
- es: 'โœ… El admin reconsiderรณ: ya tienes acceso.',
366
+ en: "โœ… The admin reconsidered: you have access.",
367
+ es: "โœ… El admin reconsiderรณ: ya tienes acceso.",
345
368
  }
346
369
  : {
347
- en: 'โœ… Access granted. You can use the bot now.',
348
- es: 'โœ… Acceso concedido. Ya puedes usar el bot.',
370
+ en: "โœ… Access granted. You can use the bot now.",
371
+ es: "โœ… Acceso concedido. Ya puedes usar el bot.",
349
372
  }, sLang),
350
373
  });
351
374
  }
@@ -354,14 +377,14 @@ export const accessControl = (opts) => {
354
377
  }
355
378
  }
356
379
  await ctx.answer({
357
- text: say({ en: 'โœ… Approved', es: 'โœ… Aprobado' }, aLang),
380
+ text: say({ en: "โœ… Approved", es: "โœ… Aprobado" }, aLang),
358
381
  });
359
382
  if (ctx.queryData.v) {
360
383
  await renderView(ctx, storage, defaults, ctx.queryData.v, aLang);
361
384
  }
362
385
  else {
363
386
  try {
364
- await ctx.editText(`${say({ en: 'โœ… Approved', es: 'โœ… Aprobado' }, aLang)} ยท ${formatUser(rec.user, uid)}`);
387
+ await ctx.editText(`${say({ en: "โœ… Approved", es: "โœ… Aprobado" }, aLang)} ยท ${formatUser(rec.user, uid)}`);
365
388
  }
366
389
  catch {
367
390
  // not always editable
@@ -373,27 +396,27 @@ export const accessControl = (opts) => {
373
396
  const aLang = ctxLang(ctx);
374
397
  if (!ctx.isAdmin)
375
398
  return ctx.answer({
376
- text: say({ en: 'Admin only.', es: 'Solo admin.' }, aLang),
399
+ text: say({ en: "Admin only.", es: "Solo admin." }, aLang),
377
400
  show_alert: true,
378
401
  });
379
402
  const uid = ctx.queryData.uid;
380
403
  const rec = await loadAccess(storage, uid);
381
404
  if (!rec)
382
405
  return ctx.answer({
383
- text: say({ en: 'Not found.', es: 'No encontrado.' }, aLang),
406
+ text: say({ en: "Not found.", es: "No encontrado." }, aLang),
384
407
  });
385
- const wasPending = rec.status === 'pending';
386
- rec.status = 'denied';
408
+ const wasPending = rec.status === "pending";
409
+ rec.status = "denied";
387
410
  rec.deniedAt = Date.now();
388
411
  rec.deniedBy = ctx.adminId;
389
412
  await saveAccess(storage, uid, rec);
390
- await indexMove(storage, uid, wasPending ? 'pending' : 'any', 'denied');
413
+ await indexMove(storage, uid, wasPending ? "pending" : "any", "denied");
391
414
  if (rec.chatId !== undefined) {
392
415
  try {
393
416
  const sLang = await langOfUser(storage, uid);
394
417
  await ctx.bot.api.sendMessage({
395
418
  chat_id: rec.chatId,
396
- text: say({ en: 'โŒ Access denied.', es: 'โŒ Acceso denegado.' }, sLang),
419
+ text: say({ en: "โŒ Access denied.", es: "โŒ Acceso denegado." }, sLang),
397
420
  });
398
421
  }
399
422
  catch {
@@ -401,14 +424,14 @@ export const accessControl = (opts) => {
401
424
  }
402
425
  }
403
426
  await ctx.answer({
404
- text: say({ en: 'โŒ Denied', es: 'โŒ Denegado' }, aLang),
427
+ text: say({ en: "โŒ Denied", es: "โŒ Denegado" }, aLang),
405
428
  });
406
429
  if (ctx.queryData.v) {
407
430
  await renderView(ctx, storage, defaults, ctx.queryData.v, aLang);
408
431
  }
409
432
  else {
410
433
  try {
411
- await ctx.editText(`${say({ en: 'โŒ Denied', es: 'โŒ Denegado' }, aLang)} ยท ${formatUser(rec.user, uid)}`);
434
+ await ctx.editText(`${say({ en: "โŒ Denied", es: "โŒ Denegado" }, aLang)} ยท ${formatUser(rec.user, uid)}`);
412
435
  }
413
436
  catch {
414
437
  // ignore
@@ -420,28 +443,28 @@ export const accessControl = (opts) => {
420
443
  const aLang = ctxLang(ctx);
421
444
  if (!ctx.isAdmin)
422
445
  return ctx.answer({
423
- text: say({ en: 'Admin only.', es: 'Solo admin.' }, aLang),
446
+ text: say({ en: "Admin only.", es: "Solo admin." }, aLang),
424
447
  show_alert: true,
425
448
  });
426
449
  const uid = ctx.queryData.uid;
427
450
  const rec = await loadAccess(storage, uid);
428
451
  if (!rec)
429
452
  return ctx.answer({
430
- text: say({ en: 'Not found.', es: 'No encontrado.' }, aLang),
453
+ text: say({ en: "Not found.", es: "No encontrado." }, aLang),
431
454
  });
432
- rec.status = 'denied';
455
+ rec.status = "denied";
433
456
  rec.deniedAt = Date.now();
434
457
  rec.deniedBy = ctx.adminId;
435
458
  await saveAccess(storage, uid, rec);
436
- await indexMove(storage, uid, 'approved', 'denied');
459
+ await indexMove(storage, uid, "approved", "denied");
437
460
  if (rec.chatId !== undefined) {
438
461
  try {
439
462
  const sLang = await langOfUser(storage, uid);
440
463
  await ctx.bot.api.sendMessage({
441
464
  chat_id: rec.chatId,
442
465
  text: say({
443
- en: 'โ†ฉ๏ธ Your bot access has been revoked.',
444
- es: 'โ†ฉ๏ธ Tu acceso al bot ha sido revocado.',
466
+ en: "โ†ฉ๏ธ Your bot access has been revoked.",
467
+ es: "โ†ฉ๏ธ Tu acceso al bot ha sido revocado.",
445
468
  }, sLang),
446
469
  });
447
470
  }
@@ -450,15 +473,15 @@ export const accessControl = (opts) => {
450
473
  }
451
474
  }
452
475
  await ctx.answer({
453
- text: say({ en: 'โ†ฉ๏ธ Revoked', es: 'โ†ฉ๏ธ Revocado' }, aLang),
476
+ text: say({ en: "โ†ฉ๏ธ Revoked", es: "โ†ฉ๏ธ Revocado" }, aLang),
454
477
  });
455
- await renderView(ctx, storage, defaults, 'approved', aLang);
478
+ await renderView(ctx, storage, defaults, "approved", aLang);
456
479
  })
457
480
  .callbackQuery(acView, async (ctx) => {
458
481
  const aLang = ctxLang(ctx);
459
482
  if (!ctx.isAdmin)
460
483
  return ctx.answer({
461
- text: say({ en: 'Admin only.', es: 'Solo admin.' }, aLang),
484
+ text: say({ en: "Admin only.", es: "Solo admin." }, aLang),
462
485
  show_alert: true,
463
486
  });
464
487
  await ctx.answer({});
@@ -468,7 +491,7 @@ export const accessControl = (opts) => {
468
491
  const aLang = ctxLang(ctx);
469
492
  if (!ctx.isAdmin)
470
493
  return ctx.answer({
471
- text: say({ en: 'Admin only.', es: 'Solo admin.' }, aLang),
494
+ text: say({ en: "Admin only.", es: "Solo admin." }, aLang),
472
495
  show_alert: true,
473
496
  });
474
497
  await ctx.answer({});
@@ -479,14 +502,14 @@ export const accessControl = (opts) => {
479
502
  // ignore
480
503
  }
481
504
  })
482
- .command('access', {
505
+ .command("access", {
483
506
  // Admin-only; hidden from Telegram's `/` menu so it doesn't
484
507
  // tempt other users to type it. Admin still invokes via /access.
485
508
  // See https://gramio.dev/triggers/command.html#commandmeta-fields
486
509
  //
487
510
  // Note: gramio's setMyCommands publishes ONE description per
488
511
  // bot, not per language. English form used as the canonical.
489
- description: 'Admin: access control menu',
512
+ description: "Admin: access control menu",
490
513
  hide: true,
491
514
  }, async (ctx) => {
492
515
  if (!ctx.isAdmin)
@@ -498,12 +521,12 @@ export const accessControl = (opts) => {
498
521
  };
499
522
  const renderView = async (ctx, storage, defaults, view, lang) => {
500
523
  const idx = await loadIndex(storage);
501
- const v = view === 'approved'
502
- ? await listView(storage, idx, 'approved', defaults, lang)
503
- : view === 'pending'
504
- ? await listView(storage, idx, 'pending', defaults, lang)
505
- : view === 'denied'
506
- ? await listView(storage, idx, 'denied', defaults, lang)
524
+ const v = view === "approved"
525
+ ? await listView(storage, idx, "approved", defaults, lang)
526
+ : view === "pending"
527
+ ? await listView(storage, idx, "pending", defaults, lang)
528
+ : view === "denied"
529
+ ? await listView(storage, idx, "denied", defaults, lang)
507
530
  : mainView(idx, defaults, lang);
508
531
  try {
509
532
  await ctx.editText(v.text, { reply_markup: v.keyboard });
@@ -513,25 +536,25 @@ const renderView = async (ctx, storage, defaults, view, lang) => {
513
536
  }
514
537
  };
515
538
  const mainView = (idx, defaults, lang) => {
516
- const approved = say({ en: 'Approved', es: 'Aprobados' }, lang);
517
- const pending = say({ en: 'Pending', es: 'Pendientes' }, lang);
518
- const denied = say({ en: 'Denied', es: 'Denegados' }, lang);
539
+ const approved = say({ en: "Approved", es: "Aprobados" }, lang);
540
+ const pending = say({ en: "Pending", es: "Pendientes" }, lang);
541
+ const denied = say({ en: "Denied", es: "Denegados" }, lang);
519
542
  const text = [
520
- say({ en: '๐Ÿ” Access Control', es: '๐Ÿ” Access Control' }, lang),
521
- '',
543
+ say({ en: "๐Ÿ” Access Control", es: "๐Ÿ” Access Control" }, lang),
544
+ "",
522
545
  `โœ… ${approved}: ${idx.approved.length}`,
523
546
  `โณ ${pending}: ${idx.pending.length}`,
524
547
  `โŒ ${denied}: ${idx.denied.length}`,
525
- `๐Ÿ‘‘ ${say({ en: 'Defaults', es: 'Defaults' }, lang)}: ${defaults.size} (hardcoded)`,
526
- ].join('\n');
548
+ `๐Ÿ‘‘ ${say({ en: "Defaults", es: "Defaults" }, lang)}: ${defaults.size} (hardcoded)`,
549
+ ].join("\n");
527
550
  const keyboard = new InlineKeyboard()
528
- .text(`โœ… ${approved} (${idx.approved.length})`, acView.pack({ v: 'approved' }))
529
- .text(`โณ ${pending} (${idx.pending.length})`, acView.pack({ v: 'pending' }))
551
+ .text(`โœ… ${approved} (${idx.approved.length})`, acView.pack({ v: "approved" }))
552
+ .text(`โณ ${pending} (${idx.pending.length})`, acView.pack({ v: "pending" }))
530
553
  .row()
531
- .text(`โŒ ${denied} (${idx.denied.length})`, acView.pack({ v: 'denied' }))
532
- .text(say({ en: '๐Ÿ”„ Refresh', es: '๐Ÿ”„ Refresh' }, lang), acView.pack({ v: 'main' }))
554
+ .text(`โŒ ${denied} (${idx.denied.length})`, acView.pack({ v: "denied" }))
555
+ .text(say({ en: "๐Ÿ”„ Refresh", es: "๐Ÿ”„ Refresh" }, lang), acView.pack({ v: "main" }))
533
556
  .row()
534
- .text(say({ en: 'โœ–๏ธ Close', es: 'โœ–๏ธ Cerrar' }, lang), acClose.pack({}));
557
+ .text(say({ en: "โœ–๏ธ Close", es: "โœ–๏ธ Cerrar" }, lang), acClose.pack({}));
535
558
  return { text, keyboard };
536
559
  };
537
560
  const listView = async (storage, idx, filter, defaults, lang) => {
@@ -539,60 +562,60 @@ const listView = async (storage, idx, filter, defaults, lang) => {
539
562
  // Cap at 20 to keep callback_data + rendering sane.
540
563
  const shownIds = ids.slice(0, 20);
541
564
  const records = await Promise.all(shownIds.map(async (id) => ({ id, rec: await loadAccess(storage, id) })));
542
- const headerEmoji = filter === 'approved' ? 'โœ…' : filter === 'pending' ? 'โณ' : 'โŒ';
543
- const headerLabel = filter === 'approved'
544
- ? say({ en: 'Approved', es: 'Aprobados' }, lang)
545
- : filter === 'pending'
546
- ? say({ en: 'Pending', es: 'Pendientes' }, lang)
547
- : say({ en: 'Denied', es: 'Denegados' }, lang);
548
- const back = say({ en: 'โฌ…๏ธ Back', es: 'โฌ…๏ธ Volver' }, lang);
565
+ const headerEmoji = filter === "approved" ? "โœ…" : filter === "pending" ? "โณ" : "โŒ";
566
+ const headerLabel = filter === "approved"
567
+ ? say({ en: "Approved", es: "Aprobados" }, lang)
568
+ : filter === "pending"
569
+ ? say({ en: "Pending", es: "Pendientes" }, lang)
570
+ : say({ en: "Denied", es: "Denegados" }, lang);
571
+ const back = say({ en: "โฌ…๏ธ Back", es: "โฌ…๏ธ Volver" }, lang);
549
572
  if (ids.length === 0) {
550
573
  const text = `${headerEmoji} ${headerLabel} (0)\n\n` +
551
- say({ en: '(empty)', es: '(vacรญo)' }, lang);
552
- const keyboard = new InlineKeyboard().text(back, acView.pack({ v: 'main' }));
574
+ say({ en: "(empty)", es: "(vacรญo)" }, lang);
575
+ const keyboard = new InlineKeyboard().text(back, acView.pack({ v: "main" }));
553
576
  return { text, keyboard };
554
577
  }
555
- const lines = [`${headerEmoji} ${headerLabel} (${ids.length})`, ''];
578
+ const lines = [`${headerEmoji} ${headerLabel} (${ids.length})`, ""];
556
579
  const keyboard = new InlineKeyboard();
557
580
  for (let i = 0; i < records.length; i++) {
558
581
  const { id, rec } = records[i];
559
582
  if (!rec) {
560
583
  // index referenced a missing record โ€” show as placeholder
561
- lines.push(`${i + 1}. id ${id} ${say({ en: '(data lost)', es: '(datos perdidos)' }, lang)}`);
584
+ lines.push(`${i + 1}. id ${id} ${say({ en: "(data lost)", es: "(datos perdidos)" }, lang)}`);
562
585
  continue;
563
586
  }
564
587
  const ageRef = rec.approvedAt ?? rec.deniedAt ?? rec.requestedAt ?? Date.now();
565
- lines.push(`${i + 1}. ${formatUser(rec.user, id)} ยท ${say({ en: 'ago', es: 'hace' }, lang)} ${fmtAge(Date.now() - ageRef)}` +
566
- (rec.messageCount ? ` ยท ${rec.messageCount} msgs` : ''));
567
- if (filter === 'pending') {
588
+ lines.push(`${i + 1}. ${formatUser(rec.user, id)} ยท ${say({ en: "ago", es: "hace" }, lang)} ${fmtAge(Date.now() - ageRef)}` +
589
+ (rec.messageCount ? ` ยท ${rec.messageCount} msgs` : ""));
590
+ if (filter === "pending") {
568
591
  keyboard
569
- .text(`โœ… ${i + 1}`, acApprove.pack({ uid: id, v: 'pending' }), {
570
- style: 'success',
592
+ .text(`โœ… ${i + 1}`, acApprove.pack({ uid: id, v: "pending" }), {
593
+ style: "success",
571
594
  })
572
- .text(`โŒ ${i + 1}`, acDeny.pack({ uid: id, v: 'pending' }), {
573
- style: 'danger',
595
+ .text(`โŒ ${i + 1}`, acDeny.pack({ uid: id, v: "pending" }), {
596
+ style: "danger",
574
597
  })
575
598
  .row();
576
599
  }
577
- else if (filter === 'approved') {
600
+ else if (filter === "approved") {
578
601
  keyboard
579
- .text(`${say({ en: 'โ†ฉ๏ธ Revoke', es: 'โ†ฉ๏ธ Revocar' }, lang)} #${i + 1}`, acRevoke.pack({ uid: id }), { style: 'danger' })
602
+ .text(`${say({ en: "โ†ฉ๏ธ Revoke", es: "โ†ฉ๏ธ Revocar" }, lang)} #${i + 1}`, acRevoke.pack({ uid: id }), { style: "danger" })
580
603
  .row();
581
604
  }
582
- else if (filter === 'denied') {
605
+ else if (filter === "denied") {
583
606
  keyboard
584
- .text(`${say({ en: 'โœ… Reapprove', es: 'โœ… Reaprobar' }, lang)} #${i + 1}`, acApprove.pack({ uid: id, v: 'denied' }), { style: 'success' })
607
+ .text(`${say({ en: "โœ… Reapprove", es: "โœ… Reaprobar" }, lang)} #${i + 1}`, acApprove.pack({ uid: id, v: "denied" }), { style: "success" })
585
608
  .row();
586
609
  }
587
610
  }
588
611
  if (ids.length > shownIds.length) {
589
- lines.push('', `(+${ids.length - shownIds.length} ${say({ en: 'more, not shown', es: 'mรกs, no mostrados' }, lang)})`);
612
+ lines.push("", `(+${ids.length - shownIds.length} ${say({ en: "more, not shown", es: "mรกs, no mostrados" }, lang)})`);
590
613
  }
591
- if (filter === 'approved' && defaults.size > 0) {
592
- lines.push('', `+ ${defaults.size} hardcoded defaults`);
614
+ if (filter === "approved" && defaults.size > 0) {
615
+ lines.push("", `+ ${defaults.size} hardcoded defaults`);
593
616
  }
594
- keyboard.text(back, acView.pack({ v: 'main' }));
595
- return { text: lines.join('\n'), keyboard };
617
+ keyboard.text(back, acView.pack({ v: "main" }));
618
+ return { text: lines.join("\n"), keyboard };
596
619
  };
597
620
  // โ”€โ”€โ”€ test helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
598
621
  /**
@@ -608,7 +631,7 @@ const listView = async (storage, idx, filter, defaults, lang) => {
608
631
  export const simulateAccessRequest = async (bot, storage, adminId, fakeUser, message) => {
609
632
  const now = Date.now();
610
633
  const rec = {
611
- status: 'pending',
634
+ status: "pending",
612
635
  user: fakeUser,
613
636
  chatId: fakeUser.id,
614
637
  requestedAt: now,
@@ -618,7 +641,7 @@ export const simulateAccessRequest = async (bot, storage, adminId, fakeUser, mes
618
641
  lastNotifiedAt: now,
619
642
  };
620
643
  await saveAccess(storage, fakeUser.id, rec);
621
- await indexAdd(storage, 'pending', fakeUser.id);
644
+ await indexAdd(storage, "pending", fakeUser.id);
622
645
  const adminLang = await langOfUser(storage, adminId);
623
646
  await bot.api.sendMessage({
624
647
  chat_id: adminId,