@adriangalilea/utils 0.16.0 โ 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.
- package/dist/bot/access-control.d.ts +7 -7
- package/dist/bot/access-control.d.ts.map +1 -1
- package/dist/bot/access-control.js +144 -121
- package/dist/bot/access-control.js.map +1 -1
- package/dist/bot/coalesce.d.ts +1 -1
- package/dist/bot/coalesce.d.ts.map +1 -1
- package/dist/bot/coalesce.js +8 -4
- package/dist/bot/coalesce.js.map +1 -1
- package/dist/bot/index.d.ts +6 -6
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +6 -6
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/kit.d.ts +2 -2
- package/dist/bot/kit.d.ts.map +1 -1
- package/dist/bot/kit.js +11 -11
- package/dist/bot/kit.js.map +1 -1
- package/dist/bot/language.d.ts +6 -6
- package/dist/bot/language.d.ts.map +1 -1
- package/dist/bot/language.js +51 -33
- package/dist/bot/language.js.map +1 -1
- package/dist/bot/llm.d.ts +12 -12
- package/dist/bot/llm.d.ts.map +1 -1
- package/dist/bot/llm.js +55 -43
- package/dist/bot/llm.js.map +1 -1
- package/dist/bot/menu.d.ts +5 -5
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +70 -67
- package/dist/bot/menu.js.map +1 -1
- package/dist/browser.d.ts +6 -6
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +6 -6
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/offensive.d.ts.map +1 -1
- package/dist/offensive.js +17 -11
- package/dist/offensive.js.map +1 -1
- package/dist/platform/dir.d.ts.map +1 -1
- package/dist/platform/dir.js +8 -8
- package/dist/platform/dir.js.map +1 -1
- package/dist/platform/file.d.ts.map +1 -1
- package/dist/platform/file.js +20 -17
- package/dist/platform/file.js.map +1 -1
- package/dist/platform/kev.d.ts.map +1 -1
- package/dist/platform/kev.js +113 -106
- package/dist/platform/kev.js.map +1 -1
- package/dist/platform/path.d.ts.map +1 -1
- package/dist/platform/path.js +5 -5
- package/dist/platform/path.js.map +1 -1
- package/dist/platform/project.d.ts.map +1 -1
- package/dist/platform/project.js +19 -19
- package/dist/platform/project.js.map +1 -1
- package/dist/platform/unseen.d.ts.map +1 -1
- package/dist/platform/unseen.js +8 -6
- package/dist/platform/unseen.js.map +1 -1
- package/dist/platform/xdg.d.ts.map +1 -1
- package/dist/platform/xdg.js +7 -7
- package/dist/platform/xdg.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +35 -22
- package/dist/runtime.js.map +1 -1
- package/dist/say/index.d.ts.map +1 -1
- package/dist/say/index.js +2 -2
- package/dist/say/index.js.map +1 -1
- package/dist/universal/currency/crypto-symbols-data.d.ts.map +1 -1
- package/dist/universal/currency/crypto-symbols-data.js +1 -1
- package/dist/universal/currency/crypto-symbols-data.js.map +1 -1
- package/dist/universal/currency/crypto-symbols.d.ts.map +1 -1
- package/dist/universal/currency/crypto-symbols.js +1 -1
- package/dist/universal/currency/crypto-symbols.js.map +1 -1
- package/dist/universal/currency/download-crypto-list.d.ts.map +1 -1
- package/dist/universal/currency/download-crypto-list.js +17 -17
- package/dist/universal/currency/download-crypto-list.js.map +1 -1
- package/dist/universal/currency/index.d.ts.map +1 -1
- package/dist/universal/currency/index.js +117 -80
- package/dist/universal/currency/index.js.map +1 -1
- package/dist/universal/format.d.ts.map +1 -1
- package/dist/universal/format.js +10 -10
- package/dist/universal/format.js.map +1 -1
- package/dist/universal/log.d.ts +1 -1
- package/dist/universal/log.d.ts.map +1 -1
- package/dist/universal/log.js +66 -60
- package/dist/universal/log.js.map +1 -1
- package/package.json +146 -141
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
*
|
|
75
75
|
* await gracefulStart(bot)
|
|
76
76
|
*/
|
|
77
|
-
import { CallbackData, InlineKeyboard, Plugin, } from
|
|
78
|
-
import { say } from
|
|
79
|
-
const INDEX_KEY =
|
|
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 =
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
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(
|
|
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:
|
|
120
|
-
: { en:
|
|
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:
|
|
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:
|
|
131
|
+
parts.push(`๐ ${say({ en: "attempts", es: "intentos" }, lang)}: ${(r.rejectedAttempts ?? 0) + 1}`);
|
|
128
132
|
}
|
|
129
133
|
if (r.firstMessage)
|
|
130
|
-
parts.push(
|
|
131
|
-
return parts.join(
|
|
134
|
+
parts.push("", `๐ฌ "${r.firstMessage}"`);
|
|
135
|
+
return parts.join("\n");
|
|
132
136
|
};
|
|
133
137
|
const requestKeyboard = (uid, lang) => new InlineKeyboard()
|
|
134
|
-
.text(say({ en:
|
|
135
|
-
.text(say({ en:
|
|
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 ===
|
|
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(
|
|
200
|
-
dependencies: [
|
|
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(
|
|
209
|
-
return {
|
|
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 {
|
|
223
|
+
return {
|
|
224
|
+
access: { allowed: true, source: "admin" },
|
|
225
|
+
};
|
|
214
226
|
}
|
|
215
227
|
if (defaults.has(senderId)) {
|
|
216
|
-
return {
|
|
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:
|
|
221
|
-
if (rec.status ===
|
|
222
|
-
return {
|
|
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 ===
|
|
235
|
-
ctx.is(
|
|
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(
|
|
266
|
+
if (ctx.is("callback_query")) {
|
|
247
267
|
await ctx.answer({
|
|
248
|
-
text: say({ en:
|
|
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(
|
|
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 ===
|
|
279
|
+
const isFirstRequest = !existing || existing.status === "unknown";
|
|
260
280
|
const rec = isFirstRequest
|
|
261
281
|
? {
|
|
262
|
-
status:
|
|
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,
|
|
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(
|
|
314
|
+
console.error("[access-control] failed to notify admin (have you /started the bot from your account?)", e);
|
|
295
315
|
}
|
|
296
|
-
opts.onAccessRequest?.({
|
|
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:
|
|
304
|
-
es:
|
|
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:
|
|
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:
|
|
348
|
+
text: say({ en: "Not found.", es: "No encontrado." }, aLang),
|
|
326
349
|
});
|
|
327
|
-
const wasDenied = rec.status ===
|
|
328
|
-
const wasPending = rec.status ===
|
|
329
|
-
rec.status =
|
|
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 ?
|
|
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:
|
|
344
|
-
es:
|
|
366
|
+
en: "โ
The admin reconsidered: you have access.",
|
|
367
|
+
es: "โ
El admin reconsiderรณ: ya tienes acceso.",
|
|
345
368
|
}
|
|
346
369
|
: {
|
|
347
|
-
en:
|
|
348
|
-
es:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
406
|
+
text: say({ en: "Not found.", es: "No encontrado." }, aLang),
|
|
384
407
|
});
|
|
385
|
-
const wasPending = rec.status ===
|
|
386
|
-
rec.status =
|
|
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 ?
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
453
|
+
text: say({ en: "Not found.", es: "No encontrado." }, aLang),
|
|
431
454
|
});
|
|
432
|
-
rec.status =
|
|
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,
|
|
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:
|
|
444
|
-
es:
|
|
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:
|
|
476
|
+
text: say({ en: "โฉ๏ธ Revoked", es: "โฉ๏ธ Revocado" }, aLang),
|
|
454
477
|
});
|
|
455
|
-
await renderView(ctx, storage, defaults,
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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 ===
|
|
502
|
-
? await listView(storage, idx,
|
|
503
|
-
: view ===
|
|
504
|
-
? await listView(storage, idx,
|
|
505
|
-
: view ===
|
|
506
|
-
? await listView(storage, idx,
|
|
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:
|
|
517
|
-
const pending = say({ en:
|
|
518
|
-
const denied = say({ en:
|
|
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:
|
|
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:
|
|
526
|
-
].join(
|
|
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:
|
|
529
|
-
.text(`โณ ${pending} (${idx.pending.length})`, acView.pack({ v:
|
|
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:
|
|
532
|
-
.text(say({ en:
|
|
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:
|
|
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 ===
|
|
543
|
-
const headerLabel = filter ===
|
|
544
|
-
? say({ en:
|
|
545
|
-
: filter ===
|
|
546
|
-
? say({ en:
|
|
547
|
-
: say({ en:
|
|
548
|
-
const back = say({ en:
|
|
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:
|
|
552
|
-
const keyboard = new InlineKeyboard().text(back, acView.pack({ v:
|
|
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:
|
|
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:
|
|
566
|
-
(rec.messageCount ? ` ยท ${rec.messageCount} msgs` :
|
|
567
|
-
if (filter ===
|
|
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:
|
|
570
|
-
style:
|
|
592
|
+
.text(`โ
${i + 1}`, acApprove.pack({ uid: id, v: "pending" }), {
|
|
593
|
+
style: "success",
|
|
571
594
|
})
|
|
572
|
-
.text(`โ ${i + 1}`, acDeny.pack({ uid: id, v:
|
|
573
|
-
style:
|
|
595
|
+
.text(`โ ${i + 1}`, acDeny.pack({ uid: id, v: "pending" }), {
|
|
596
|
+
style: "danger",
|
|
574
597
|
})
|
|
575
598
|
.row();
|
|
576
599
|
}
|
|
577
|
-
else if (filter ===
|
|
600
|
+
else if (filter === "approved") {
|
|
578
601
|
keyboard
|
|
579
|
-
.text(`${say({ en:
|
|
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 ===
|
|
605
|
+
else if (filter === "denied") {
|
|
583
606
|
keyboard
|
|
584
|
-
.text(`${say({ en:
|
|
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(
|
|
612
|
+
lines.push("", `(+${ids.length - shownIds.length} ${say({ en: "more, not shown", es: "mรกs, no mostrados" }, lang)})`);
|
|
590
613
|
}
|
|
591
|
-
if (filter ===
|
|
592
|
-
lines.push(
|
|
614
|
+
if (filter === "approved" && defaults.size > 0) {
|
|
615
|
+
lines.push("", `+ ${defaults.size} hardcoded defaults`);
|
|
593
616
|
}
|
|
594
|
-
keyboard.text(back, acView.pack({ v:
|
|
595
|
-
return { text: lines.join(
|
|
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:
|
|
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,
|
|
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,
|