@adriangalilea/utils 7.0.0 → 8.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/README.md +13 -23
- package/dist/bot/access-control.d.ts +89 -31
- package/dist/bot/access-control.d.ts.map +1 -1
- package/dist/bot/access-control.js +10 -28
- package/dist/bot/access-control.js.map +1 -1
- package/dist/bot/callbacks.d.ts +0 -13
- package/dist/bot/callbacks.d.ts.map +1 -1
- package/dist/bot/callbacks.js +0 -16
- package/dist/bot/callbacks.js.map +1 -1
- package/dist/bot/kit.d.ts +4 -9
- package/dist/bot/kit.d.ts.map +1 -1
- package/dist/bot/kit.js +0 -1
- package/dist/bot/kit.js.map +1 -1
- package/dist/bot/lang.d.ts +46 -0
- package/dist/bot/lang.d.ts.map +1 -0
- package/dist/bot/lang.js +29 -0
- package/dist/bot/lang.js.map +1 -0
- package/dist/bot/language.d.ts +0 -2
- package/dist/bot/language.d.ts.map +1 -1
- package/dist/bot/language.js +0 -12
- package/dist/bot/language.js.map +1 -1
- package/dist/bot/llm.d.ts +1 -2
- package/dist/bot/llm.d.ts.map +1 -1
- package/dist/bot/llm.js +1 -2
- package/dist/bot/llm.js.map +1 -1
- package/dist/bot/menu.d.ts +7 -0
- package/dist/bot/menu.d.ts.map +1 -1
- package/dist/bot/menu.js +8 -2
- package/dist/bot/menu.js.map +1 -1
- package/dist/bot/payments/callbacks.d.ts +1 -1
- package/dist/bot/payments/callbacks.d.ts.map +1 -1
- package/dist/bot/payments/callbacks.js +9 -11
- package/dist/bot/payments/callbacks.js.map +1 -1
- package/dist/bot/payments/commands.d.ts +1 -1
- package/dist/bot/payments/commands.d.ts.map +1 -1
- package/dist/bot/payments/commands.js +2 -2
- package/dist/bot/payments/commands.js.map +1 -1
- package/dist/bot/payments/derive.d.ts.map +1 -1
- package/dist/bot/payments/derive.js +7 -6
- package/dist/bot/payments/derive.js.map +1 -1
- package/dist/bot/payments/handlers.d.ts +1 -5
- package/dist/bot/payments/handlers.d.ts.map +1 -1
- package/dist/bot/payments/handlers.js +1 -2
- package/dist/bot/payments/handlers.js.map +1 -1
- package/dist/bot/payments/invoice.d.ts.map +1 -1
- package/dist/bot/payments/invoice.js +1 -2
- package/dist/bot/payments/invoice.js.map +1 -1
- package/dist/bot/payments/menu-item.d.ts.map +1 -1
- package/dist/bot/payments/menu-item.js +2 -2
- package/dist/bot/payments/menu-item.js.map +1 -1
- package/dist/bot/payments/payouts.d.ts +3 -3
- package/dist/bot/payments/payouts.js +3 -3
- package/dist/bot/payments/plugin.d.ts +87 -29
- package/dist/bot/payments/plugin.d.ts.map +1 -1
- package/dist/bot/payments/refund.d.ts +1 -5
- package/dist/bot/payments/refund.d.ts.map +1 -1
- package/dist/bot/payments/refund.js +6 -32
- package/dist/bot/payments/refund.js.map +1 -1
- package/dist/bot/payments/types.d.ts +2 -9
- package/dist/bot/payments/types.d.ts.map +1 -1
- package/dist/bot/payments/types.js +0 -7
- package/dist/bot/payments/types.js.map +1 -1
- package/dist/bot/payments/waiver.d.ts +1 -17
- package/dist/bot/payments/waiver.d.ts.map +1 -1
- package/dist/bot/payments/waiver.js +2 -30
- package/dist/bot/payments/waiver.js.map +1 -1
- package/dist/bot/reactions.d.ts +0 -2
- package/dist/bot/reactions.d.ts.map +1 -1
- package/dist/bot/reactions.js +0 -16
- package/dist/bot/reactions.js.map +1 -1
- package/dist/bot/session.d.ts +1 -21
- package/dist/bot/session.d.ts.map +1 -1
- package/dist/bot/session.js +1 -29
- package/dist/bot/session.js.map +1 -1
- package/dist/bot/storage.d.ts +1 -1
- package/dist/bot/storage.js +1 -1
- package/dist/bot/worker.d.ts.map +1 -1
- package/dist/bot/worker.js +1 -4
- package/dist/bot/worker.js.map +1 -1
- package/dist/cli/text.d.ts.map +1 -1
- package/dist/cli/text.js +2 -3
- package/dist/cli/text.js.map +1 -1
- package/dist/llm/index.js +5 -5
- package/dist/llm/index.js.map +1 -1
- package/dist/metrics/d1.d.ts +4 -1
- package/dist/metrics/d1.d.ts.map +1 -1
- package/dist/metrics/d1.js +3 -0
- package/dist/metrics/d1.js.map +1 -1
- package/dist/metrics/index.d.ts +5 -0
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +8 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/libsql.d.ts +4 -1
- package/dist/metrics/libsql.d.ts.map +1 -1
- package/dist/metrics/libsql.js +3 -0
- package/dist/metrics/libsql.js.map +1 -1
- package/dist/metrics/report.d.ts +13 -0
- package/dist/metrics/report.d.ts.map +1 -1
- package/dist/metrics/report.js.map +1 -1
- package/dist/metrics/sqlite.d.ts +15 -1
- package/dist/metrics/sqlite.d.ts.map +1 -1
- package/dist/metrics/sqlite.js +70 -2
- package/dist/metrics/sqlite.js.map +1 -1
- package/dist/runtime.d.ts +0 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +0 -17
- package/dist/runtime.js.map +1 -1
- package/dist/tg-html/index.d.ts +11 -4
- package/dist/tg-html/index.d.ts.map +1 -1
- package/dist/tg-html/index.js +13 -8
- package/dist/tg-html/index.js.map +1 -1
- package/dist/tg-md/index.d.ts +6 -6
- package/dist/tg-md/index.d.ts.map +1 -1
- package/dist/tg-md/index.js +9 -19
- package/dist/tg-md/index.js.map +1 -1
- package/dist/universal/time.d.ts +11 -2
- package/dist/universal/time.d.ts.map +1 -1
- package/dist/universal/time.js +22 -2
- package/dist/universal/time.js.map +1 -1
- package/package.json +9 -15
- package/dist/bot/index.d.ts +0 -31
- package/dist/bot/index.d.ts.map +0 -1
- package/dist/bot/index.js +0 -31
- package/dist/bot/index.js.map +0 -1
- package/dist/universal/currency/download-crypto-list.d.ts +0 -10
- package/dist/universal/currency/download-crypto-list.d.ts.map +0 -1
- package/dist/universal/currency/download-crypto-list.js +0 -69
- package/dist/universal/currency/download-crypto-list.js.map +0 -1
- package/dist/universal/url/update-rules.d.ts +0 -15
- package/dist/universal/url/update-rules.d.ts.map +0 -1
- package/dist/universal/url/update-rules.js +0 -122
- package/dist/universal/url/update-rules.js.map +0 -1
package/README.md
CHANGED
|
@@ -77,19 +77,6 @@ Enable it only with a deletion and retention policy; no identity is inferred.
|
|
|
77
77
|
`overlaps: [["a", "b"]]` declares unordered audience overlap, not an ordered funnel.
|
|
78
78
|
Daily totals support counts and averages, not percentile latency or event ordering.
|
|
79
79
|
|
|
80
|
-
**7.0 migration:** `defineMetrics` takes `store: metricsStore(driver, project)` instead
|
|
81
|
-
of `write: metricsWriter(...)`; a custom store implements `declare` and `write`, and
|
|
82
|
-
`Measurement` no longer carries `spec`. Existing tables gain a column and a table:
|
|
83
|
-
`ALTER TABLE metric_definition ADD COLUMN per_user INTEGER NOT NULL DEFAULT 0` and the
|
|
84
|
-
`metric_project` table from `METRICS_SCHEMA` (one row per project, its overlaps as
|
|
85
|
-
JSON; a 6.0 `metric_overlap` table is dropped); the first sample after the deploy
|
|
86
|
-
declares the real flags, labels and overlaps. Ingestion credentials need add and
|
|
87
|
-
update on every metric table and never delete: the store reconciles nothing. `readAudience` takes the schema
|
|
88
|
-
(`readSchema(reader, project)`) instead of an overlap list and returns a row per
|
|
89
|
-
declared per-user key. `readReport` replaces hand-rolled window + rows + audience
|
|
90
|
-
composition. Metric keys and dimensions are storage identifiers: introduce a new key
|
|
91
|
-
when changing meaning rather than relabelling history.
|
|
92
|
-
|
|
93
80
|
## Installation
|
|
94
81
|
|
|
95
82
|
```bash
|
|
@@ -600,7 +587,7 @@ await ctx.say.answer({ en: 'OK', es: 'OK' }) // → ctx.answer (callback o
|
|
|
600
587
|
|
|
601
588
|
### Telegram HTML (`tg-html`)
|
|
602
589
|
|
|
603
|
-
Transform arbitrary (LLM-emitted) HTML into Telegram-compatible `parse_mode=HTML` with opinionated, consistent spacing. Telegram's HTML subset has no headings, lists, or block layout — send it `<h1>`/`<ul>` and the API rejects the whole message. `transform()` accepts the HTML a model naturally writes and renders the structure typographically: `h1` → bold+underline + blank line, `h2…h6` → bold + blank line, `ul/li` → `• ` bullets, blocks get blank lines, blockquotes keep their internal line breaks, allowed tags pass through with attributes filtered, unknown tags drop their markup (content kept), and stray `<`/`>` escape instead of vanishing. Zero dependencies, no DOM, Worker-safe.
|
|
590
|
+
Transform arbitrary (LLM-emitted) HTML into Telegram-compatible `parse_mode=HTML` with opinionated, consistent spacing. Telegram's HTML subset has no headings, lists, or block layout — send it `<h1>`/`<ul>` and the API rejects the whole message. `transform()` accepts the HTML a model naturally writes and renders the structure typographically: `h1` → bold+underline + blank line, `h2…h6` → bold + blank line, `ul/li` → `• ` bullets, blocks get blank lines, blockquotes keep their internal line breaks, allowed tags pass through with attributes filtered, unknown tags drop their markup (content kept), and stray `<`/`>` escape instead of vanishing. Zero dependencies, no DOM, Worker-safe.
|
|
604
591
|
|
|
605
592
|
```typescript
|
|
606
593
|
import { transform } from '@adriangalilea/utils/tg-html'
|
|
@@ -626,7 +613,7 @@ Zero dependencies, Worker-safe. Typography (heading air, bullet glyph, title und
|
|
|
626
613
|
|
|
627
614
|
### Telegram bot plugins (GramIO)
|
|
628
615
|
|
|
629
|
-
Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format
|
|
616
|
+
Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format`) are **all optional** — install only what you import.
|
|
630
617
|
|
|
631
618
|
```bash
|
|
632
619
|
pnpm add @adriangalilea/utils gramio @gramio/storage @gramio/session
|
|
@@ -677,14 +664,15 @@ Cloudflare is one adapter, not the architecture: `bot/worker` and `bot/storage-d
|
|
|
677
664
|
|
|
678
665
|
Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for private chats (BotFather → Bot Settings → Threaded Mode) auto-threads out of the box: `@gramio/contexts` ≥0.9 (what gramio ≥0.12 resolves) forwards `message_thread_id` on every `ctx.send` / `ctx.sendDocument` / `ctx.reply` — including from callback handlers, which carry `threadId` too. No fork, no override.
|
|
679
666
|
|
|
680
|
-
**Runs on Cloudflare Workers / bun / anywhere.** Every bot subpath below is import-safe off Node — no filesystem access, no `node:*` modules, no import-time side effects anywhere in its graph — **except `bot/kit`**,
|
|
667
|
+
**Runs on Cloudflare Workers / bun / anywhere.** Every bot subpath below is import-safe off Node — no filesystem access, no `node:*` modules, no import-time side effects anywhere in its graph — **except `bot/kit`**, whose `gracefulStart` handles process signals. `pnpm test:worker-safe` walks every graph and screams on regression.
|
|
681
668
|
|
|
682
669
|
| Subpath | What it does |
|
|
683
670
|
|---|---|
|
|
684
671
|
| `@adriangalilea/utils/bot/ctx` | Structural ctx types (`BotMessageCtx`, `BotCallbackCtx`, …) and the `narrow<T>(ctx)` cast helper. Pure typing — no runtime state. |
|
|
685
672
|
| `@adriangalilea/utils/bot/keys` | The bot-id key namespace, a **persisted contract**: `botId(ctx)`, `botStorageKey(ctx, userId)` → `bot-<id>:<userId>`, `botSubKey(ctx, sub)` → `bot-<id>:<sub>`. Every storage key the library writes derives from here, and those keys live in YOUR Redis/D1 rows — the shape never changes. Pure functions of `ctx.bot.info.id` (no env/fs), which is what keeps the whole bot surface Worker-safe. |
|
|
686
|
-
| `@adriangalilea/utils/bot/kit` | The Node-only corner
|
|
687
|
-
| `@adriangalilea/utils/bot/
|
|
673
|
+
| `@adriangalilea/utils/bot/kit` | The Node-only corner, one function: `gracefulStart(bot, opts?)` — SIGINT/SIGTERM → `bot.stop()` → exit; force-kills if shutdown hangs; calls `bot.syncCommands()` before `bot.start()`. DMs the admin `@<bot> started.` / `@<bot> shutting down.` by default when `KEV.TELEGRAM_ADMIN_ID` is set (graceful only — crashes don't trigger `onStop`); pass `notifyAdmin: false` to disable or `notifyAdmin: 12345` for an explicit chat id. |
|
|
674
|
+
| `@adriangalilea/utils/bot/admin` | `adminContext(admins)` — decorates `ctx.adminId` (the primary admin: the approve/deny and notification target) and `ctx.isAdmin` (any admin) on every event. Ids arrive POSITIONALLY as a number, an array, or a FUNCTION consulted per update, so a db-backed resolver changes who is admin with no restart. Reads no env and touches no OS, which is what keeps it Worker-safe; a Node bot composes env in at the call site (`adminContext(kev.int('TELEGRAM_ADMIN_ID', 123))`). `parseIdList("123, 456 789")` turns an env string into clean ids. `accessControl` and the payments refund flow declare a gramio dependency on this plugin, so gramio throws at `bot.start()` if you forget it. |
|
|
675
|
+
| `@adriangalilea/utils/bot/session` | `botSession(opts)` — **drop-in replacement for `@gramio/session`'s `session()`** that auto-namespaces every key as `bot-<id>:<senderId>` using `ctx.bot.info.id` (populated by `getMe()` at startup). Use this instead of `session()` — full stop. Multiple bots sharing one Redis/D1 stay isolated by construction; every plugin in this package derives the same prefix internally via `botStorageKey(ctx, userId)` / `botSubKey(ctx, sub)` (from `bot/keys`). No regex, no manual prefix argument, no way to forget. Worker-safe. |
|
|
688
676
|
| `@adriangalilea/utils/bot/notify` | Best-effort admin DMs, worker-safe (you pass the ids; no env, no process). `notifyAdmins(bot, adminIds, text, extra?)` — DM each admin, failures logged and swallowed (a notification must never take the bot down). `alertAdminError(bot, adminIds, label, error, throttle?)` — truncated `🚨 label\nName: message`, rate-limited through a caller-owned `alertThrottle(ms?)` so a failure storm sends one alert per window. `gracefulStart`'s start/stop DMs are built on this. |
|
|
689
677
|
| `@adriangalilea/utils/bot/profile` | `syncBotProfile(bot, { name?, description?, about?, photo?, commands?, expects?, adminIds? })` — the bot's Telegram-facing identity as CODE, never BotFather: localized name / description / About / public command list, reconciled idempotently on every boot (get → compare → set per field per language; unchanged values cost one read, so cold-start firing is rate-limit-free). `expects: { inline: true }` declares the BotFather-only capabilities the code assumes — a mismatch (inline mode off on an inline-dependent bot) DMs the admins, since the API can only detect it, not fix it. Never throws; failures log and the bot keeps running. |
|
|
690
678
|
| `@adriangalilea/utils/bot/access-control` | Personal-bot ACL — gates non-admin/non-default users; admin gets DM with `[✅ Aprobar][❌ Denegar]` on first attempt; `/access` opens a persistent menu (revoke / reapprove / list pending). Backed by `@gramio/session` per-user + a small index. **Native alternative**: BotFather → Bot Settings → Access → "Restrict bot usage" — flat allow-list at Telegram. Use this plugin when you want in-bot approval flow instead of a BotFather round-trip; both can coexist. |
|
|
@@ -699,10 +687,10 @@ Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for
|
|
|
699
687
|
| `@adriangalilea/utils/bot/coalesce` | Joins client-split inbound messages back into one. When a user pastes >4096 chars, Telegram clients fragment it into separate `message` updates with no marker. Middleware detects the burst and emits one combined event. |
|
|
700
688
|
| `@adriangalilea/utils/llm` | Multi-provider LLM caller on the Vercel AI SDK — the policy layer the SDK doesn't ship. `createLlm({ providers, health? })` → `.stream(req)` (typed events: `delta` / `reasoning` / `reset` / `tool-call` / `end`) and `.complete(req)`. **Failover:** priority-ordered across OpenAI-compatible, Anthropic-compatible, and OpenRouter endpoints; multiple keys per provider (health tracked per key fingerprint); per-key circuit breaker with exponential cool-down persisted in any KV-shaped `HealthStore`; an attempt that dies after emitting yields `reset` (discard, next candidate regenerates); empty completions count as failures. **Accounting:** tokens summed across billed attempts; `costUsd` is the ACTUAL charge (OpenRouter usage accounting via `providerMetadata`, or a `cost` field on the raw usage frame) — never a price-table estimate. **Knobs:** per-model temperature, maxTokens caps, `disableThinking` mapped to each dialect. **Tools:** AI SDK `tool()` + `toolChoice` pass through (re-exported). Worker-safe; peers: `ai`, `@ai-sdk/openai-compatible`, `@ai-sdk/anthropic`, `@openrouter/ai-sdk-provider`. |
|
|
701
689
|
| `@adriangalilea/utils/bot/llm` | The Telegram side of an LLM chatbot; the model side is `@adriangalilea/utils/llm`. **Output:** `streamChatReply(ctx, events, opts?)` consumes an `AsyncIterable<LlmStreamEvent>` and paints it with Telegram's native message-draft streaming (`sendMessageDraft` full-frame repaints, throttled ~1/s, keepalive under the ~30s draft TTL), then persists the finished markdown via `ctx.send`, entity-split across 4096 by `@gramio/split`. Reasoning models get a thinking phase — streams into the ephemeral draft and evaporates (`reasoning: 'preview'`, default), persists as an expandable blockquote (`'message'`), or never renders at all (`'hidden'`). Upstream `reset` (provider failover) repaints the draft from scratch. Drafts are private-chat-only (+ BotFather forum-topic mode); elsewhere the preview phase is skipped and only the final send happens. Returns `{ content, reasoning, toolCalls, usage, messages }`. The painter underneath is `@adriangalilea/utils/bot/draft` (`createDraftPreview(ctx, { render, throttleMs? })`): dependency-light (no markdown machinery), for producers that push deltas or own their per-frame rendering (plain or rich-message frames) and persist path, with `streamForensics` (render-regression + reset warnings) built in and exported standalone. **History:** `llmHistory({...})` returns `.plugin` (decorates `ctx.llm` with `.add() / .get() / .clear() / .all() / .clearAll()`, per-(user, thread) OpenAI `ChatMessage` shape, persisted in the shared session record so the menu's 🗑 Forget wipes it automatically) AND `.menuItem` (drop-in "🗑 Delete this thread" for `botMenu` — wipes the LLM history AND calls `deleteForumTopic` so the Telegram thread + all its messages disappear from the chat; falls back to history-only clear when no `threadId` is present). |
|
|
702
|
-
| `@adriangalilea/utils/metrics` | Declared counters and timings, bounded dimensions, optional actor IDs, and a schema for generic reporting. Inject a
|
|
703
|
-
| `@adriangalilea/utils/metrics/sqlite` | Atomic SQLite
|
|
690
|
+
| `@adriangalilea/utils/metrics` | Declared counters and timings, bounded dimensions, optional actor IDs, and a schema for generic reporting. Inject a store (`store: metricsStore(driver, project)`); measurement failures are reported without rejecting calls. See Product metrics below. |
|
|
691
|
+
| `@adriangalilea/utils/metrics/sqlite` | Atomic SQLite driver, explicit provisioning schema, and inclusive UTC daily reports; libSQL is its own module (`metrics/libsql`) over the same driver shape. Connections and credentials belong to the application. |
|
|
704
692
|
| `@adriangalilea/utils/bot/menu` | `botMenu({ command, description, items, privacy?, personalData?, adminContact })` — `/settings` command + InlineKeyboard router. Root view always renders a `🛡️ Privacy & data` submenu button that wraps the privacy policy link plus (if `personalData: { storage }`) 🗑 Forget + 📥 Export buttons. Items take `keepRow` (render on the same row as the next item — e.g. a two-per-row language picker) and `rootExtra` (render at the bottom of the root menu, below Privacy & data). `label` / `header` / `style` / `visible` resolvers may be **async** (read your db at render time — never cache render strings in the session); `parseMode: 'HTML'` renders the header formatted (you own escaping); `personalData.onForget(ctx, userId)` wipes YOUR tables inside the same try as the session delete, so Forget either forgets everything or reports failure. `toggleMenuItem({ id, read, write, label: { off, on }, toast? })` — convenience factory for boolean-toggle items with dynamic label + optional toast, storage-agnostic via `read`/`write` closures. |
|
|
705
|
-
| `@adriangalilea/utils/bot/payments` | `botPayments({ session, storage, paysupport, paysupportHint?, legal, waiver, vip?, credits?, perks? })` — Telegram Stars monetization in one drop-in plugin. **Three axes, all optional:** `vip` (positional tier ladder — single rung in v1 is just `vip: [{...}]`, ladder is `vip: [{...}, {...}]`; ids are `vip.1`, `vip.2`, …), `credits` (consumable balance + top-up packs `credits.1`, `credits.2`, …), `perks` (orthogonal one-shot unlocks `perks.<key>`). **Surface:** `ctx.payments.atLeast('vip')` / `atLeast('vip.2')` (typed rank check), `ctx.payments.tier()` / `.tier.level()` / `.tier.label()`, `ctx.payments.credits.{balance, consume, tryConsume}` (throws `InsufficientCredits`), `ctx.payments.has(perkId)`, `await ctx.payments.require('vip', { feature? })` (gate that sends a localized upgrade prompt deep-linked to `/settings → 💎 VIP`), `await ctx.payments.invoice(productKey)` (threads Art. 103(m) TRLGDCU consent inline before `sendInvoice`). **Owns:** waiver consent flow (versioned text → forces re-consent on bump, snapshotted on every charge for audit), `/paysupport` slash command (Telegram ToS §6.5; `paysupportHint` overrides the where-to-manage-charges line when your menu isn't `/settings`), idempotent `successful_payment` fulfillment via `pay:idempotency:{chargeId}` sentinel, lazy subscription expiry (no cron needed), tier upgrade auto-cancel of the lower rung's renewal, and admin-DM refund approval (mirror of `accessControl`'s [✅ Aprobar][❌ Denegar] pattern). **Returns:** `{ plugin, menuItem, payouts, onFulfilled }` — `menuItem` is the drop-in `💎 VIP` entry for `botMenu`; `payouts.{record, list, export, exportForUsers}` is the Fragment payout ledger (you receive TON, log the EUR conversion, export time-windowed CSV/JSON for your gestor); `onFulfilled(productKey \| '*', handler)` / `onRefunded(...)` register fire-and-forget hooks (purchase applied / admin-approved refund — a revenue ledger writes on one, reverses on the other). **Stars-only by design** — Telegram ToS §6.2 forbids third-party payment providers for digital goods. Crypto Pay deferred (MiCA risk); Stripe-outside-Telegram is a future v2 channel. Full compliance memo (Spanish-autónomo seller-of-record analysis, Verifactu vs Crea y Crece, MiCA, Art. 103(m) waiver text, GDPR retention) in `src/bot/payments/CLAUDE.md`. |
|
|
693
|
+
| `@adriangalilea/utils/bot/payments` | `botPayments({ session, storage, paysupport, paysupportHint?, legal, waiver, vip?, credits?, perks? })` — Telegram Stars monetization in one drop-in plugin. **Three axes, all optional:** `vip` (positional tier ladder — single rung in v1 is just `vip: [{...}]`, ladder is `vip: [{...}, {...}]`; ids are `vip.1`, `vip.2`, …), `credits` (consumable balance + top-up packs `credits.1`, `credits.2`, …), `perks` (orthogonal one-shot unlocks `perks.<key>`). **Surface:** `ctx.payments.atLeast('vip')` / `atLeast('vip.2')` (typed rank check), `ctx.payments.tier()` / `.tier.level()` / `.tier.label()`, `ctx.payments.credits.{balance, consume, tryConsume}` (throws `InsufficientCredits`), `ctx.payments.has(perkId)`, `await ctx.payments.require('vip', { feature? })` (gate that sends a localized upgrade prompt deep-linked to `/settings → 💎 VIP`), `await ctx.payments.invoice(productKey)` (threads Art. 103(m) TRLGDCU consent inline before `sendInvoice`). **Owns:** waiver consent flow (versioned text → forces re-consent on bump, snapshotted on every charge for audit), `/paysupport` slash command (Telegram ToS §6.5; `paysupportHint` overrides the where-to-manage-charges line when your menu isn't `/settings`), idempotent `successful_payment` fulfillment via `pay:idempotency:{chargeId}` sentinel, lazy subscription expiry (no cron needed), tier upgrade auto-cancel of the lower rung's renewal, and admin-DM refund approval (mirror of `accessControl`'s [✅ Aprobar][❌ Denegar] pattern). **Returns:** `{ plugin, menuItem, payouts, admin, onFulfilled, onRefunded }` — `menuItem` is the drop-in `💎 VIP` entry for `botMenu`; `admin.{listCharges, getCharge}` backs custom admin commands; `payouts.{record, list, export, exportForUsers}` is the Fragment payout ledger (you receive TON, log the EUR conversion, export time-windowed CSV/JSON for your gestor); `onFulfilled(productKey \| '*', handler)` / `onRefunded(...)` register fire-and-forget hooks (purchase applied / admin-approved refund — a revenue ledger writes on one, reverses on the other). **Stars-only by design** — Telegram ToS §6.2 forbids third-party payment providers for digital goods. Crypto Pay deferred (MiCA risk); Stripe-outside-Telegram is a future v2 channel. Full compliance memo (Spanish-autónomo seller-of-record analysis, Verifactu vs Crea y Crece, MiCA, Art. 103(m) waiver text, GDPR retention) in `src/bot/payments/CLAUDE.md`. |
|
|
706
694
|
| `@adriangalilea/utils/bot/create` | `createBot<S>({ token?, storage?, initial?, admins?, language?, menu?, access?, payments?, handlers?, worker? })` — the composer (see "One bot file, ideation → production" above). Returns `{ build, session, poll, isMain, fetch }`: `poll()` long-polls, `export default app` is a complete Worker, `app.session(ctx)` / the `handlers` callback's `session(ctx)` is the TYPED accessor for your `S` fields. Owns storage+session wiring; resolves storage per environment (D1 binding → `bot/storage-d1`; `BOT_PERSIST` path → sqlite, `redis://` → redis, lazily-imported optional peers; else announced-ephemeral memory) — or pass `storage: (env) => Storage` when the choice is env-dependent (e.g. a D1 binding not named `DB`). Boot NARRATES the composition (`session: memory …`, `features: language(en,es) · menu(/settings)`) so every implicit decision is visible where you're looking. Runnable demo: `pnpm demo:bot`. |
|
|
707
695
|
| `@adriangalilea/utils/bot/worker` | `botWorkerFetch(resolve)` — the Cloudflare Worker cap: secret-checked webhook (ack fast, work + storage flush ride `ctx.waitUntil`, errors DM admins throttled), `POST /setup` (webhook registration with handler-derived `allowed_updates` + a commit-narrating 🚀 deploy DM from the request body), `POST /deploy-started` (the 🛳 "what is shipping" DM — curl it from your deploy script on the still-live version, body `{sha, author, message, etaSeconds?}`), operator-authed `/pause` `/resume` `/webhook-status` (+ `statusExtra`), and a `routes` escape hatch tried before the built-ins. Structural bot type — no workers-types dependency. |
|
|
708
696
|
| `@adriangalilea/utils/bot/flags` | `defineFlags(spec, { read, write? })` — feature flags declared ONCE in code, resolved LIVE from the bot's operator-config record (D1 json row, Redis hash — storage-agnostic, Worker-safe, zero deps). Each flag: `{ kind: 'bool'\|'number'\|'string', label, help?, default }`, where `default` is a scalar or a **tier map** `{ free, vip?, 'vip.N'? }` resolved off `ctx.payments.tier()` — "free users get this limit, premium users get that model" is one declaration, zero branching at the call site (ladder walks down: exact rung → lower rungs → `vip` → `free`). Read sites: `await flags.<key>(ctx)` — stored override wins over the code default, per-ctx reads coalesce into one config fetch. `flags.describe()` is the JSON schema admin panels render generically, so a new flag appears in every panel with zero panel edits; `flags.set(ctx, key, value)` writes a kind-checked live override (`null` clears it — RFC 7386 null-delete, matching SQLite `json_patch`); `flags.overrides(ctx)` lists what's overridden. Corrupt stored values scream (`SourcedError`); bad specs panic at construction. |
|
|
@@ -713,11 +701,13 @@ Standard wiring:
|
|
|
713
701
|
```typescript
|
|
714
702
|
import { Bot } from 'gramio'
|
|
715
703
|
import { redisStorage } from '@gramio/storage-redis'
|
|
716
|
-
import { adminContext
|
|
704
|
+
import { adminContext } from '@adriangalilea/utils/bot/admin'
|
|
705
|
+
import { gracefulStart } from '@adriangalilea/utils/bot/kit'
|
|
717
706
|
import { botSession } from '@adriangalilea/utils/bot/session'
|
|
718
707
|
import { accessControl } from '@adriangalilea/utils/bot/access-control'
|
|
719
708
|
import { createLlm } from '@adriangalilea/utils/llm'
|
|
720
709
|
import { streamChatReply, llmHistory, toModelMessages } from '@adriangalilea/utils/bot/llm'
|
|
710
|
+
import { kev } from '@adriangalilea/utils'
|
|
721
711
|
|
|
722
712
|
// Raw redis is fine — bot-id namespacing happens inside botSession +
|
|
723
713
|
// every plugin via ctx.bot.info.id. Multiple bots sharing this Redis
|
|
@@ -733,7 +723,7 @@ const llm = createLlm({
|
|
|
733
723
|
})
|
|
734
724
|
|
|
735
725
|
const bot = new Bot(process.env.BOT_TOKEN!)
|
|
736
|
-
.extend(adminContext(
|
|
726
|
+
.extend(adminContext(kev.int('TELEGRAM_ADMIN_ID', 190202471))) // or a live resolver
|
|
737
727
|
.extend(userSession)
|
|
738
728
|
.extend(accessControl({ session: userSession, storage, defaults: [] }))
|
|
739
729
|
.extend(chat.plugin)
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
import type { session } from "@gramio/session";
|
|
139
139
|
import type { Storage } from "@gramio/storage";
|
|
140
140
|
import { type AnyBot, type DeriveDefinitions, Plugin } from "gramio";
|
|
141
|
+
import { type LangSession } from "./lang.js";
|
|
141
142
|
export type AccessStatus = "unknown" | "pending" | "approved" | "denied";
|
|
142
143
|
export type AccessUser = {
|
|
143
144
|
id: number;
|
|
@@ -230,8 +231,7 @@ export type AccessInfo = {
|
|
|
230
231
|
*/
|
|
231
232
|
type SessionLike = {
|
|
232
233
|
access?: AccessRecord;
|
|
233
|
-
|
|
234
|
-
};
|
|
234
|
+
} & LangSession;
|
|
235
235
|
/** @internal — kept unexported so it doesn't clash with peers' refs. */
|
|
236
236
|
type AcSessionPluginRef = ReturnType<typeof session<SessionLike, "session">>;
|
|
237
237
|
export type AccessControlOptions = {
|
|
@@ -312,147 +312,205 @@ export declare const accessControl: (opts: AccessControlOptions) => Plugin<Recor
|
|
|
312
312
|
global: AdminDerives & AccessDerives & SessionDerives;
|
|
313
313
|
} & {
|
|
314
314
|
message: {
|
|
315
|
-
session:
|
|
315
|
+
session: {
|
|
316
|
+
access?: AccessRecord;
|
|
317
|
+
} & LangSession & {
|
|
316
318
|
$clear: () => Promise<void>;
|
|
317
319
|
};
|
|
318
320
|
};
|
|
319
321
|
channel_post: {
|
|
320
|
-
session:
|
|
322
|
+
session: {
|
|
323
|
+
access?: AccessRecord;
|
|
324
|
+
} & LangSession & {
|
|
321
325
|
$clear: () => Promise<void>;
|
|
322
326
|
};
|
|
323
327
|
};
|
|
324
328
|
inline_query: {
|
|
325
|
-
session:
|
|
329
|
+
session: {
|
|
330
|
+
access?: AccessRecord;
|
|
331
|
+
} & LangSession & {
|
|
326
332
|
$clear: () => Promise<void>;
|
|
327
333
|
};
|
|
328
334
|
};
|
|
329
335
|
chosen_inline_result: {
|
|
330
|
-
session:
|
|
336
|
+
session: {
|
|
337
|
+
access?: AccessRecord;
|
|
338
|
+
} & LangSession & {
|
|
331
339
|
$clear: () => Promise<void>;
|
|
332
340
|
};
|
|
333
341
|
};
|
|
334
342
|
callback_query: {
|
|
335
|
-
session:
|
|
343
|
+
session: {
|
|
344
|
+
access?: AccessRecord;
|
|
345
|
+
} & LangSession & {
|
|
336
346
|
$clear: () => Promise<void>;
|
|
337
347
|
};
|
|
338
348
|
};
|
|
339
349
|
shipping_query: {
|
|
340
|
-
session:
|
|
350
|
+
session: {
|
|
351
|
+
access?: AccessRecord;
|
|
352
|
+
} & LangSession & {
|
|
341
353
|
$clear: () => Promise<void>;
|
|
342
354
|
};
|
|
343
355
|
};
|
|
344
356
|
pre_checkout_query: {
|
|
345
|
-
session:
|
|
357
|
+
session: {
|
|
358
|
+
access?: AccessRecord;
|
|
359
|
+
} & LangSession & {
|
|
346
360
|
$clear: () => Promise<void>;
|
|
347
361
|
};
|
|
348
362
|
};
|
|
349
363
|
poll_answer: {
|
|
350
|
-
session:
|
|
364
|
+
session: {
|
|
365
|
+
access?: AccessRecord;
|
|
366
|
+
} & LangSession & {
|
|
351
367
|
$clear: () => Promise<void>;
|
|
352
368
|
};
|
|
353
369
|
};
|
|
354
370
|
chat_join_request: {
|
|
355
|
-
session:
|
|
371
|
+
session: {
|
|
372
|
+
access?: AccessRecord;
|
|
373
|
+
} & LangSession & {
|
|
356
374
|
$clear: () => Promise<void>;
|
|
357
375
|
};
|
|
358
376
|
};
|
|
359
377
|
new_chat_members: {
|
|
360
|
-
session:
|
|
378
|
+
session: {
|
|
379
|
+
access?: AccessRecord;
|
|
380
|
+
} & LangSession & {
|
|
361
381
|
$clear: () => Promise<void>;
|
|
362
382
|
};
|
|
363
383
|
};
|
|
364
384
|
new_chat_title: {
|
|
365
|
-
session:
|
|
385
|
+
session: {
|
|
386
|
+
access?: AccessRecord;
|
|
387
|
+
} & LangSession & {
|
|
366
388
|
$clear: () => Promise<void>;
|
|
367
389
|
};
|
|
368
390
|
};
|
|
369
391
|
new_chat_photo: {
|
|
370
|
-
session:
|
|
392
|
+
session: {
|
|
393
|
+
access?: AccessRecord;
|
|
394
|
+
} & LangSession & {
|
|
371
395
|
$clear: () => Promise<void>;
|
|
372
396
|
};
|
|
373
397
|
};
|
|
374
398
|
delete_chat_photo: {
|
|
375
|
-
session:
|
|
399
|
+
session: {
|
|
400
|
+
access?: AccessRecord;
|
|
401
|
+
} & LangSession & {
|
|
376
402
|
$clear: () => Promise<void>;
|
|
377
403
|
};
|
|
378
404
|
};
|
|
379
405
|
group_chat_created: {
|
|
380
|
-
session:
|
|
406
|
+
session: {
|
|
407
|
+
access?: AccessRecord;
|
|
408
|
+
} & LangSession & {
|
|
381
409
|
$clear: () => Promise<void>;
|
|
382
410
|
};
|
|
383
411
|
};
|
|
384
412
|
message_auto_delete_timer_changed: {
|
|
385
|
-
session:
|
|
413
|
+
session: {
|
|
414
|
+
access?: AccessRecord;
|
|
415
|
+
} & LangSession & {
|
|
386
416
|
$clear: () => Promise<void>;
|
|
387
417
|
};
|
|
388
418
|
};
|
|
389
419
|
migrate_to_chat_id: {
|
|
390
|
-
session:
|
|
420
|
+
session: {
|
|
421
|
+
access?: AccessRecord;
|
|
422
|
+
} & LangSession & {
|
|
391
423
|
$clear: () => Promise<void>;
|
|
392
424
|
};
|
|
393
425
|
};
|
|
394
426
|
migrate_from_chat_id: {
|
|
395
|
-
session:
|
|
427
|
+
session: {
|
|
428
|
+
access?: AccessRecord;
|
|
429
|
+
} & LangSession & {
|
|
396
430
|
$clear: () => Promise<void>;
|
|
397
431
|
};
|
|
398
432
|
};
|
|
399
433
|
pinned_message: {
|
|
400
|
-
session:
|
|
434
|
+
session: {
|
|
435
|
+
access?: AccessRecord;
|
|
436
|
+
} & LangSession & {
|
|
401
437
|
$clear: () => Promise<void>;
|
|
402
438
|
};
|
|
403
439
|
};
|
|
404
440
|
invoice: {
|
|
405
|
-
session:
|
|
441
|
+
session: {
|
|
442
|
+
access?: AccessRecord;
|
|
443
|
+
} & LangSession & {
|
|
406
444
|
$clear: () => Promise<void>;
|
|
407
445
|
};
|
|
408
446
|
};
|
|
409
447
|
successful_payment: {
|
|
410
|
-
session:
|
|
448
|
+
session: {
|
|
449
|
+
access?: AccessRecord;
|
|
450
|
+
} & LangSession & {
|
|
411
451
|
$clear: () => Promise<void>;
|
|
412
452
|
};
|
|
413
453
|
};
|
|
414
454
|
chat_shared: {
|
|
415
|
-
session:
|
|
455
|
+
session: {
|
|
456
|
+
access?: AccessRecord;
|
|
457
|
+
} & LangSession & {
|
|
416
458
|
$clear: () => Promise<void>;
|
|
417
459
|
};
|
|
418
460
|
};
|
|
419
461
|
proximity_alert_triggered: {
|
|
420
|
-
session:
|
|
462
|
+
session: {
|
|
463
|
+
access?: AccessRecord;
|
|
464
|
+
} & LangSession & {
|
|
421
465
|
$clear: () => Promise<void>;
|
|
422
466
|
};
|
|
423
467
|
};
|
|
424
468
|
video_chat_scheduled: {
|
|
425
|
-
session:
|
|
469
|
+
session: {
|
|
470
|
+
access?: AccessRecord;
|
|
471
|
+
} & LangSession & {
|
|
426
472
|
$clear: () => Promise<void>;
|
|
427
473
|
};
|
|
428
474
|
};
|
|
429
475
|
video_chat_started: {
|
|
430
|
-
session:
|
|
476
|
+
session: {
|
|
477
|
+
access?: AccessRecord;
|
|
478
|
+
} & LangSession & {
|
|
431
479
|
$clear: () => Promise<void>;
|
|
432
480
|
};
|
|
433
481
|
};
|
|
434
482
|
video_chat_ended: {
|
|
435
|
-
session:
|
|
483
|
+
session: {
|
|
484
|
+
access?: AccessRecord;
|
|
485
|
+
} & LangSession & {
|
|
436
486
|
$clear: () => Promise<void>;
|
|
437
487
|
};
|
|
438
488
|
};
|
|
439
489
|
video_chat_participants_invited: {
|
|
440
|
-
session:
|
|
490
|
+
session: {
|
|
491
|
+
access?: AccessRecord;
|
|
492
|
+
} & LangSession & {
|
|
441
493
|
$clear: () => Promise<void>;
|
|
442
494
|
};
|
|
443
495
|
};
|
|
444
496
|
web_app_data: {
|
|
445
|
-
session:
|
|
497
|
+
session: {
|
|
498
|
+
access?: AccessRecord;
|
|
499
|
+
} & LangSession & {
|
|
446
500
|
$clear: () => Promise<void>;
|
|
447
501
|
};
|
|
448
502
|
};
|
|
449
503
|
location: {
|
|
450
|
-
session:
|
|
504
|
+
session: {
|
|
505
|
+
access?: AccessRecord;
|
|
506
|
+
} & LangSession & {
|
|
451
507
|
$clear: () => Promise<void>;
|
|
452
508
|
};
|
|
453
509
|
};
|
|
454
510
|
passport_data: {
|
|
455
|
-
session:
|
|
511
|
+
session: {
|
|
512
|
+
access?: AccessRecord;
|
|
513
|
+
} & LangSession & {
|
|
456
514
|
$clear: () => Promise<void>;
|
|
457
515
|
};
|
|
458
516
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EACN,KAAK,MAAM,EAEX,KAAK,iBAAiB,EAEtB,MAAM,EACN,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"access-control.d.ts","sourceRoot":"","sources":["../../src/bot/access-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -137,10 +137,11 @@
|
|
|
137
137
|
*/
|
|
138
138
|
import { CallbackData, InlineKeyboard, Plugin, } from "gramio";
|
|
139
139
|
import { say } from "../say/index.js";
|
|
140
|
+
import { coarseSpan } from "../universal/time.js";
|
|
140
141
|
import { botStorageKey, botSubKey } from "./keys.js";
|
|
142
|
+
import { ctxLang, langOfUser, loadFullRecord, } from "./lang.js";
|
|
141
143
|
const FIRST_MSG_LIMIT = 200;
|
|
142
144
|
const DEFAULT_THROTTLE_MS = 6 * 60 * 60 * 1000;
|
|
143
|
-
const FALLBACK_LANG = "en";
|
|
144
145
|
// ─── callback schemas ──────────────────────────────────────────────
|
|
145
146
|
//
|
|
146
147
|
// Short `nameId`s keep callback_data under Telegram's 64-byte cap.
|
|
@@ -172,18 +173,6 @@ const formatUser = (u, fallbackId) => {
|
|
|
172
173
|
const handle = u.username ? `@${u.username}` : `id ${u.id}`;
|
|
173
174
|
return `${name} (${handle})`;
|
|
174
175
|
};
|
|
175
|
-
const fmtAge = (ms) => {
|
|
176
|
-
const s = Math.floor(ms / 1000);
|
|
177
|
-
if (s < 60)
|
|
178
|
-
return `${s}s`;
|
|
179
|
-
const m = Math.floor(s / 60);
|
|
180
|
-
if (m < 60)
|
|
181
|
-
return `${m}min`;
|
|
182
|
-
const h = Math.floor(m / 60);
|
|
183
|
-
if (h < 24)
|
|
184
|
-
return `${h}h`;
|
|
185
|
-
return `${Math.floor(h / 24)}d`;
|
|
186
|
-
};
|
|
187
176
|
const requestNotificationText = (uid, r, repeat, lang) => {
|
|
188
177
|
const parts = [
|
|
189
178
|
say(repeat
|
|
@@ -192,7 +181,7 @@ const requestNotificationText = (uid, r, repeat, lang) => {
|
|
|
192
181
|
"",
|
|
193
182
|
`👤 ${formatUser(r.user, uid)}`,
|
|
194
183
|
`🆔 ${uid}`,
|
|
195
|
-
`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${
|
|
184
|
+
`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${coarseSpan(Date.now() - (r.requestedAt ?? Date.now()))}`,
|
|
196
185
|
];
|
|
197
186
|
if (repeat) {
|
|
198
187
|
parts.push(`🔁 ${say({ en: "attempts", es: "intentos" }, lang)}: ${(r.rejectedAttempts ?? 0) + 1}`);
|
|
@@ -253,29 +242,22 @@ const indexRemove = async (storage, key, uid) => {
|
|
|
253
242
|
}
|
|
254
243
|
await saveIndex(storage, key, idx);
|
|
255
244
|
};
|
|
256
|
-
const
|
|
245
|
+
const loadRecord = (storage, ctx, userId) => loadFullRecord(storage, ctx, userId);
|
|
257
246
|
const saveAccess = async (storage, ctx, userId, rec) => {
|
|
258
|
-
const full = await
|
|
247
|
+
const full = await loadRecord(storage, ctx, userId);
|
|
259
248
|
full.access = rec;
|
|
260
249
|
await storage.set(botStorageKey(ctx, userId), full);
|
|
261
250
|
};
|
|
262
251
|
const loadAccess = async (storage, ctx, userId) => {
|
|
263
|
-
const full = await
|
|
252
|
+
const full = await loadRecord(storage, ctx, userId);
|
|
264
253
|
return full.access;
|
|
265
254
|
};
|
|
266
255
|
/** Forget the access judgment on a user (other plugins' fields survive). */
|
|
267
256
|
const dropAccess = async (storage, ctx, userId) => {
|
|
268
|
-
const full = await
|
|
257
|
+
const full = await loadRecord(storage, ctx, userId);
|
|
269
258
|
delete full.access;
|
|
270
259
|
await storage.set(botStorageKey(ctx, userId), full);
|
|
271
260
|
};
|
|
272
|
-
/** Read recipient's stored language (set by bot/language); fallback to en. */
|
|
273
|
-
const langOfUser = async (storage, ctx, userId) => {
|
|
274
|
-
const full = await loadFullRecord(storage, ctx, userId);
|
|
275
|
-
return full.language ?? FALLBACK_LANG;
|
|
276
|
-
};
|
|
277
|
-
/** Read current ctx's lang. */
|
|
278
|
-
const ctxLang = (ctx) => ctx.session.language ?? FALLBACK_LANG;
|
|
279
261
|
// Resolve the caller's lang for admins; for non-admins send the standard
|
|
280
262
|
// rejection alert and return null so the caller bails. Shares one check
|
|
281
263
|
// across the five admin callback handlers.
|
|
@@ -333,7 +315,7 @@ const groupRequestText = (rec, lang) => {
|
|
|
333
315
|
];
|
|
334
316
|
if (rec.addedBy)
|
|
335
317
|
parts.push(`👤 ${formatUser(rec.addedBy, rec.addedBy.id)}`);
|
|
336
|
-
parts.push(`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${
|
|
318
|
+
parts.push(`⏰ ${say({ en: "ago", es: "hace" }, lang)} ${coarseSpan(Date.now() - (rec.requestedAt ?? Date.now()))}`);
|
|
337
319
|
return parts.join("\n");
|
|
338
320
|
};
|
|
339
321
|
const groupRequestKeyboard = (chatId, lang) => new InlineKeyboard()
|
|
@@ -1142,7 +1124,7 @@ const listView = async (storage, ctx, idx, gIdx, filter, defaults, lang) => {
|
|
|
1142
1124
|
continue;
|
|
1143
1125
|
}
|
|
1144
1126
|
const ageRef = rec.approvedAt ?? rec.deniedAt ?? rec.requestedAt ?? Date.now();
|
|
1145
|
-
lines.push(`${n}. 👤 ${formatUser(rec.user, id)} · ${ago} ${
|
|
1127
|
+
lines.push(`${n}. 👤 ${formatUser(rec.user, id)} · ${ago} ${coarseSpan(Date.now() - ageRef)}` +
|
|
1146
1128
|
(rec.messageCount ? ` · ${rec.messageCount} msgs` : ""));
|
|
1147
1129
|
if (filter === "pending") {
|
|
1148
1130
|
keyboard
|
|
@@ -1169,7 +1151,7 @@ const listView = async (storage, ctx, idx, gIdx, filter, defaults, lang) => {
|
|
|
1169
1151
|
n += 1;
|
|
1170
1152
|
const rec = await loadGroup(storage, ctx, id);
|
|
1171
1153
|
const ageRef = rec?.approvedAt ?? rec?.deniedAt ?? rec?.requestedAt ?? Date.now();
|
|
1172
|
-
lines.push(`${n}. 👥 ${formatGroup(rec, id)} · ${ago} ${
|
|
1154
|
+
lines.push(`${n}. 👥 ${formatGroup(rec, id)} · ${ago} ${coarseSpan(Date.now() - ageRef)}`);
|
|
1173
1155
|
if (filter === "pending") {
|
|
1174
1156
|
keyboard
|
|
1175
1157
|
.text(`✅ ${n}`, acGroupApprove.pack({ gid: id, v: "pending" }), {
|