@adriangalilea/utils 0.7.0 → 0.9.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.
Files changed (81) hide show
  1. package/README.md +29 -5
  2. package/dist/bot/access-control.d.ts +106 -62
  3. package/dist/bot/access-control.d.ts.map +1 -1
  4. package/dist/bot/access-control.js +255 -146
  5. package/dist/bot/access-control.js.map +1 -1
  6. package/dist/bot/index.d.ts +3 -0
  7. package/dist/bot/index.d.ts.map +1 -1
  8. package/dist/bot/index.js +3 -0
  9. package/dist/bot/index.js.map +1 -1
  10. package/dist/bot/kit.d.ts.map +1 -1
  11. package/dist/bot/kit.js +6 -0
  12. package/dist/bot/kit.js.map +1 -1
  13. package/dist/bot/language.d.ts +305 -0
  14. package/dist/bot/language.d.ts.map +1 -0
  15. package/dist/bot/language.js +250 -0
  16. package/dist/bot/language.js.map +1 -0
  17. package/dist/bot/menu.d.ts +189 -0
  18. package/dist/bot/menu.d.ts.map +1 -0
  19. package/dist/bot/menu.js +331 -0
  20. package/dist/bot/menu.js.map +1 -0
  21. package/dist/bot/message-history.d.ts +259 -0
  22. package/dist/bot/message-history.d.ts.map +1 -0
  23. package/dist/bot/message-history.js +111 -0
  24. package/dist/bot/message-history.js.map +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/say/index.d.ts +62 -0
  30. package/dist/say/index.d.ts.map +1 -0
  31. package/dist/say/index.js +59 -0
  32. package/dist/say/index.js.map +1 -0
  33. package/package.json +17 -1
  34. package/dist/currency/crypto-symbols-data.d.ts +0 -10
  35. package/dist/currency/crypto-symbols-data.d.ts.map +0 -1
  36. package/dist/currency/crypto-symbols-data.js +0 -13765
  37. package/dist/currency/crypto-symbols-data.js.map +0 -1
  38. package/dist/currency/crypto-symbols.d.ts +0 -20
  39. package/dist/currency/crypto-symbols.d.ts.map +0 -1
  40. package/dist/currency/crypto-symbols.js +0 -23
  41. package/dist/currency/crypto-symbols.js.map +0 -1
  42. package/dist/currency/download-crypto-list.d.ts +0 -10
  43. package/dist/currency/download-crypto-list.d.ts.map +0 -1
  44. package/dist/currency/download-crypto-list.js +0 -69
  45. package/dist/currency/download-crypto-list.js.map +0 -1
  46. package/dist/currency/index.d.ts +0 -84
  47. package/dist/currency/index.d.ts.map +0 -1
  48. package/dist/currency/index.js +0 -230
  49. package/dist/currency/index.js.map +0 -1
  50. package/dist/dir.d.ts +0 -40
  51. package/dist/dir.d.ts.map +0 -1
  52. package/dist/dir.js +0 -108
  53. package/dist/dir.js.map +0 -1
  54. package/dist/file.d.ts +0 -53
  55. package/dist/file.d.ts.map +0 -1
  56. package/dist/file.js +0 -211
  57. package/dist/file.js.map +0 -1
  58. package/dist/format.d.ts +0 -40
  59. package/dist/format.d.ts.map +0 -1
  60. package/dist/format.js +0 -83
  61. package/dist/format.js.map +0 -1
  62. package/dist/kev.d.ts +0 -149
  63. package/dist/kev.d.ts.map +0 -1
  64. package/dist/kev.js +0 -761
  65. package/dist/kev.js.map +0 -1
  66. package/dist/log.d.ts +0 -91
  67. package/dist/log.d.ts.map +0 -1
  68. package/dist/log.js +0 -300
  69. package/dist/log.js.map +0 -1
  70. package/dist/logger.d.ts +0 -91
  71. package/dist/logger.d.ts.map +0 -1
  72. package/dist/logger.js +0 -269
  73. package/dist/logger.js.map +0 -1
  74. package/dist/path.d.ts +0 -67
  75. package/dist/path.d.ts.map +0 -1
  76. package/dist/path.js +0 -107
  77. package/dist/path.js.map +0 -1
  78. package/dist/project.d.ts +0 -35
  79. package/dist/project.d.ts.map +0 -1
  80. package/dist/project.js +0 -154
  81. package/dist/project.js.map +0 -1
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Per-user language preference for GramIO bots.
3
+ *
4
+ * Follows gramio's canonical "shared infrastructure" pattern (see
5
+ * [Composer docs — Production Architecture](https://gramio.dev/extend/middleware.html#production-architecture)):
6
+ * the bot's session is extended once at the top level by the user,
7
+ * and each feature plugin declares it as a required dependency.
8
+ * gramio's runtime deduplication ensures the session derive runs
9
+ * exactly once per update; TypeScript flows the session's data shape
10
+ * into every plugin that `.extend()`s it.
11
+ *
12
+ * ## What this plugin owns
13
+ *
14
+ * - Validates the supported BCP-47 language tags via `Intl.getCanonicalLocales`
15
+ * - Resolves `ctx.lang` on every event (override → Telegram hint → default)
16
+ * - Persists the user's chosen language as `ctx.session.language`
17
+ * - Provides a `menuItem` for a `botMenu`'s language picker
18
+ *
19
+ * ## What this plugin does NOT own
20
+ *
21
+ * - The session itself. The user creates it (`session(...)`) and
22
+ * extends it at bot level before this plugin.
23
+ * - GDPR machinery. A language preference is trivially covered by
24
+ * [Telegram's Standard Bot Privacy Policy](https://telegram.org/privacy-tpa)
25
+ * under "data necessary to function".
26
+ *
27
+ * ## Resolution priority for `ctx.lang`
28
+ *
29
+ * 1. `ctx.session.language` — stored override
30
+ * 2. `ctx.from.languageCode` — Telegram-detected user lang, only in
31
+ * user-scoped resolution (in groups it would flicker per-speaker)
32
+ * 3. `default` — the fallback passed at construction
33
+ *
34
+ * Peer deps: `gramio`, `@gramio/session`.
35
+ *
36
+ * @example
37
+ * import { Bot } from 'gramio'
38
+ * import { session } from '@gramio/session'
39
+ * import { redisStorage } from '@gramio/storage-redis'
40
+ * import { language } from '@adriangalilea/utils/bot/language'
41
+ *
42
+ * const userSession = session({
43
+ * storage: redisStorage(),
44
+ * key: 'session',
45
+ * initial: () => ({}), // plugins add their fields by convention
46
+ * })
47
+ *
48
+ * const lang = language({
49
+ * session: userSession,
50
+ * supported: ['en','es'] as const,
51
+ * default: 'en',
52
+ * })
53
+ *
54
+ * const bot = new Bot(process.env.BOT_TOKEN!)
55
+ * .extend(userSession) // ← FIRST: ctx.session lands on the real ctx
56
+ * .extend(lang.plugin) // declares userSession as dep; runtime dedup
57
+ * .command('hello', (ctx) => ctx.send({ en: 'Hello', es: 'Hola' }[ctx.lang]))
58
+ */
59
+ import { CallbackData, Plugin } from 'gramio';
60
+ import { say } from '../say/index.js';
61
+ /**
62
+ * Validate + canonicalize a BCP-47 tag using the standard
63
+ * `Intl.getCanonicalLocales`. Throws `RangeError` on invalid input.
64
+ * Canonicalizes casing: `'en-us'` → `'en-US'`.
65
+ */
66
+ export const parseLangCode = (s) => Intl.getCanonicalLocales(s)[0];
67
+ /** Non-throwing type guard. Same parser path as `parseLangCode`. */
68
+ export const isLangCode = (s) => {
69
+ if (typeof s !== 'string')
70
+ return false;
71
+ try {
72
+ Intl.getCanonicalLocales(s);
73
+ return true;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ };
79
+ const buildSayer = (ctx, lang, fallback) => {
80
+ const resolve = (value) => value[lang] ?? value[fallback] ?? say(value, lang);
81
+ const fn = ((value) => resolve(value));
82
+ const c = ctx;
83
+ fn.send = (value, params) => {
84
+ if (typeof c.send !== 'function') {
85
+ throw new TypeError('ctx.say.send: ctx.send is not available on this event');
86
+ }
87
+ return c.send(resolve(value), params);
88
+ };
89
+ fn.edit = (value, params) => {
90
+ if (typeof c.editText !== 'function') {
91
+ throw new TypeError('ctx.say.edit: ctx.editText is only available on callback_query events');
92
+ }
93
+ return c.editText(resolve(value), params);
94
+ };
95
+ fn.answer = (value, params) => {
96
+ if (typeof c.answer !== 'function') {
97
+ throw new TypeError('ctx.say.answer: ctx.answer is only available on callback_query events');
98
+ }
99
+ return c.answer({ text: resolve(value), ...(params ?? {}) });
100
+ };
101
+ return fn;
102
+ };
103
+ // ─── scope helpers ─────────────────────────────────────────────────
104
+ const DEFAULT_SCOPE = {
105
+ private: 'user',
106
+ group: 'chat',
107
+ supergroup: 'chat',
108
+ channel: 'chat',
109
+ };
110
+ const resolveScope = (scope, chatType) => {
111
+ if (typeof scope === 'string')
112
+ return scope;
113
+ const t = chatType;
114
+ return scope?.[t] ?? DEFAULT_SCOPE[t] ?? 'user';
115
+ };
116
+ // ─── flag emoji (best-effort) ──────────────────────────────────────
117
+ const REGIONLESS_FLAGS = {
118
+ en: '🇬🇧', es: '🇪🇸', fr: '🇫🇷', de: '🇩🇪', it: '🇮🇹',
119
+ pt: '🇵🇹', ru: '🇷🇺', zh: '🇨🇳', ja: '🇯🇵', ko: '🇰🇷',
120
+ ar: '🇸🇦', tr: '🇹🇷', pl: '🇵🇱', nl: '🇳🇱', uk: '🇺🇦',
121
+ };
122
+ const regionToFlag = (region) => String.fromCodePoint(...region.toUpperCase().split('').map((c) => 0x1f1a5 + c.charCodeAt(0)));
123
+ const flagFor = (lang) => {
124
+ const parts = lang.split('-');
125
+ if (parts.length > 1) {
126
+ const region = parts.find((p) => /^[A-Z]{2}$/.test(p));
127
+ if (region)
128
+ return regionToFlag(region);
129
+ }
130
+ return REGIONLESS_FLAGS[parts[0].toLowerCase()] ?? '🌐';
131
+ };
132
+ const autonym = (lang) => {
133
+ try {
134
+ const dn = new Intl.DisplayNames([lang], { type: 'language' });
135
+ return dn.of(lang) ?? lang;
136
+ }
137
+ catch {
138
+ return lang;
139
+ }
140
+ };
141
+ const defaultLabel = (lang) => `${flagFor(lang)} ${autonym(lang)}`;
142
+ // ─── callback schema ───────────────────────────────────────────────
143
+ const setLangCb = new CallbackData('lang').string('code');
144
+ // ─── feature factory ───────────────────────────────────────────────
145
+ export const language = (opts) => {
146
+ const scopeOpt = opts.scope;
147
+ const labels = (opts.labels ?? {});
148
+ const menuLabel = opts.menuLabel ?? { en: '🌐 Language', es: '🌐 Idioma' };
149
+ // Canonicalize all supported tags at construction.
150
+ const canonical = opts.supported.map((l) => {
151
+ try {
152
+ return Intl.getCanonicalLocales(l)[0];
153
+ }
154
+ catch {
155
+ throw new Error(`language: "${l}" is not a valid BCP-47 tag (per Intl.getCanonicalLocales)`);
156
+ }
157
+ });
158
+ const canonicalSet = new Set(canonical);
159
+ let defaultLanguage;
160
+ try {
161
+ defaultLanguage = Intl.getCanonicalLocales(opts.default)[0];
162
+ }
163
+ catch {
164
+ throw new Error(`language: default "${opts.default}" is not a valid BCP-47 tag`);
165
+ }
166
+ if (!canonicalSet.has(defaultLanguage)) {
167
+ throw new Error(`language: default "${defaultLanguage}" is not in supported[]`);
168
+ }
169
+ const matchSupported = (s) => {
170
+ if (!s)
171
+ return undefined;
172
+ try {
173
+ const c = Intl.getCanonicalLocales(s)[0];
174
+ if (canonicalSet.has(c))
175
+ return c;
176
+ }
177
+ catch {
178
+ // fall through
179
+ }
180
+ return undefined;
181
+ };
182
+ const plugin = buildLanguagePlugin({
183
+ sessionPlugin: opts.session,
184
+ canonicalSet,
185
+ defaultLanguage,
186
+ matchSupported,
187
+ scopeOpt,
188
+ });
189
+ const menuItem = {
190
+ id: 'lang',
191
+ label: menuLabel,
192
+ submenu: canonical.map((l) => ({
193
+ id: l,
194
+ label: (ctx) => {
195
+ const current = ctx.lang;
196
+ const marker = current === l ? '●' : '○';
197
+ const base = labels[l] ?? defaultLabel(l);
198
+ return `${marker} ${base}`;
199
+ },
200
+ action: async (ctx) => {
201
+ // ctx.session is the shared session record. Mutating any
202
+ // field on it goes through @gramio/session's Proxy and
203
+ // auto-persists. We own the `language` field by convention.
204
+ const c = ctx;
205
+ c.session.language = l;
206
+ await c.answer({ text: `✓ ${l}` });
207
+ try {
208
+ await c.delete?.();
209
+ }
210
+ catch {
211
+ // not always deletable
212
+ }
213
+ },
214
+ })),
215
+ };
216
+ return { plugin, menuItem };
217
+ };
218
+ // ─── plugin builder ────────────────────────────────────────────────
219
+ const buildLanguagePlugin = (args) => {
220
+ const { sessionPlugin, canonicalSet, defaultLanguage, matchSupported, scopeOpt } = args;
221
+ return (new Plugin('@adriangalilea/utils/bot/language')
222
+ // Declare the session as a dependency. gramio's runtime dedupes
223
+ // this against the bot's top-level session extension so the
224
+ // session derive runs exactly once per update — but the types
225
+ // (ctx.session: SessionLike) flow into our handlers below.
226
+ .extend(sessionPlugin)
227
+ .derive(['message', 'callback_query'], (ctx) => {
228
+ const resolveLang = () => {
229
+ // 1) stored override
230
+ const stored = ctx.session.language;
231
+ if (stored && canonicalSet.has(stored))
232
+ return stored;
233
+ // 2) Telegram hint — only when in user-scoped resolution
234
+ const chatType = ctx.is('message')
235
+ ? ctx.chat.type
236
+ : ctx.message?.chat.type ?? 'private';
237
+ const strategy = resolveScope(scopeOpt, chatType);
238
+ if (strategy === 'user') {
239
+ const hint = matchSupported(ctx.from.languageCode);
240
+ if (hint)
241
+ return hint;
242
+ }
243
+ // 3) configured default
244
+ return defaultLanguage;
245
+ };
246
+ const lang = resolveLang();
247
+ return { lang, say: buildSayer(ctx, lang, defaultLanguage) };
248
+ }));
249
+ };
250
+ //# sourceMappingURL=language.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language.js","sourceRoot":"","sources":["../../src/bot/language.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,OAAO,EAAE,YAAY,EAA0B,MAAM,EAAE,MAAM,QAAQ,CAAA;AAGrE,OAAO,EAAE,GAAG,EAAiB,MAAM,iBAAiB,CAAA;AAQpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAS,EAAY,EAAE,CACnD,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAa,CAAA;AAE5C,oEAAoE;AACpE,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAU,EAAiB,EAAE;IACtD,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,CAAC;QACH,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAA;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC,CAAA;AA4ED,MAAM,UAAU,GAAG,CACjB,GAAY,EACZ,IAAO,EACP,QAAW,EACD,EAAE;IACZ,MAAM,OAAO,GAAG,CAAwB,KAAQ,EAAU,EAAE,CAC1D,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,KAAyB,EAAE,IAAI,CAAC,CAAA;IAExE,MAAM,EAAE,GAAG,CAAC,CAAwB,KAAQ,EAAU,EAAE,CACtD,OAAO,CAAC,KAAK,CAAC,CAAa,CAAA;IAO7B,MAAM,CAAC,GAAG,GAAc,CAAA;IAExB,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QAC1B,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACjC,MAAM,IAAI,SAAS,CAAC,uDAAuD,CAAC,CAAA;QAC9E,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;IACvC,CAAC,CAAA;IACD,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QAC1B,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YACrC,MAAM,IAAI,SAAS,CACjB,uEAAuE,CACxE,CAAA;QACH,CAAC;QACD,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;IAC3C,CAAC,CAAA;IACD,EAAE,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QAC5B,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,SAAS,CACjB,uEAAuE,CACxE,CAAA;QACH,CAAC;QACD,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IAC9D,CAAC,CAAA;IAED,OAAO,EAAE,CAAA;AACX,CAAC,CAAA;AAED,sEAAsE;AAEtE,MAAM,aAAa,GAAG;IACpB,OAAO,EAAE,MAA+B;IACxC,KAAK,EAAE,MAA+B;IACtC,UAAU,EAAE,MAA+B;IAC3C,OAAO,EAAE,MAA+B;CAChC,CAAA;AAEV,MAAM,YAAY,GAAG,CACnB,KAAgC,EAChC,QAAgB,EACO,EAAE;IACzB,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,MAAM,CAAC,GAAG,QAAsC,CAAA;IAChD,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,MAAM,CAAA;AACjD,CAAC,CAAA;AAED,sEAAsE;AAEtE,MAAM,gBAAgB,GAA2B;IAC/C,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;IAC1D,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;IAC1D,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAC3D,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,MAAc,EAAU,EAAE,CAC9C,MAAM,CAAC,aAAa,CAClB,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CACxE,CAAA;AAEH,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,IAAI,MAAM;YAAE,OAAO,YAAY,CAAC,MAAM,CAAC,CAAA;IACzC,CAAC;IACD,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAA;AACzD,CAAC,CAAA;AAED,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE;IACvC,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;QAC9D,OAAO,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAA;AAE1E,sEAAsE;AAEtE,MAAM,SAAS,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AAEzD,sEAAsE;AAEtE,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,IAA4B,EACI,EAAE;IAGlC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAA;IAC3B,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAA2B,CAAA;IAC5D,MAAM,SAAS,GACb,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,WAAW,EAAE,CAAA;IAE1D,mDAAmD;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAQ,EAAE;QAC/C,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAS,CAAA;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,cAAc,CAAC,4DAA4D,CAC5E,CAAA;QACH,CAAC;IACH,CAAC,CAAC,CAAA;IACF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAS,SAAS,CAAC,CAAA;IAE/C,IAAI,eAAqB,CAAA;IACzB,IAAI,CAAC;QACH,eAAe,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAS,CAAA;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,sBAAsB,IAAI,CAAC,OAAO,6BAA6B,CAAC,CAAA;IAClF,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,sBAAsB,eAAe,yBAAyB,CAC/D,CAAA;IACH,CAAC;IAED,MAAM,cAAc,GAAG,CAAC,CAAqB,EAAoB,EAAE;QACjE,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAA;QACxB,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACxC,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,OAAO,CAAS,CAAA;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,mBAAmB,CAAO;QACvC,aAAa,EAAE,IAAI,CAAC,OAAO;QAC3B,YAAY;QACZ,eAAe;QACf,cAAc;QACd,QAAQ;KACT,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAa;QACzB,EAAE,EAAE,MAAM;QACV,KAAK,EAAE,SAAS;QAChB,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC7B,EAAE,EAAE,CAAC;YACL,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE;gBACb,MAAM,OAAO,GAAI,GAAoC,CAAC,IAAI,CAAA;gBAC1D,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;gBACxC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAAA;gBACzC,OAAO,GAAG,MAAM,IAAI,IAAI,EAAE,CAAA;YAC5B,CAAC;YACD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACpB,yDAAyD;gBACzD,uDAAuD;gBACvD,4DAA4D;gBAC5D,MAAM,CAAC,GAAG,GAIT,CAAA;gBACD,CAAC,CAAC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;gBACtB,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAA;gBAClC,IAAI,CAAC;oBACH,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAA;gBACpB,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;YACH,CAAC;SACF,CAAC,CAAC;KACJ,CAAA;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC7B,CAAC,CAAA;AAED,sEAAsE;AAEtE,MAAM,mBAAmB,GAAG,CAAsB,IAMjD,EAAE,EAAE;IACH,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAA;IAEvF,OAAO,CACL,IAAI,MAAM,CACR,mCAAmC,CACpC;QACC,gEAAgE;QAChE,4DAA4D;QAC5D,8DAA8D;QAC9D,2DAA2D;SAC1D,MAAM,CAAC,aAAa,CAAC;SACrB,MAAM,CAAC,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE;QAC7C,MAAM,WAAW,GAAG,GAAS,EAAE;YAC7B,qBAAqB;YACrB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAA;YACnC,IAAI,MAAM,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,MAAc,CAAA;YAE7D,yDAAyD;YACzD,MAAM,QAAQ,GACZ,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC;gBACf,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI;gBACf,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS,CAAA;YACzC,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;YACjD,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;gBACxB,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;gBAClD,IAAI,IAAI;oBAAE,OAAO,IAAI,CAAA;YACvB,CAAC;YAED,wBAAwB;YACxB,OAAO,eAAe,CAAA;QACxB,CAAC,CAAA;QAED,MAAM,IAAI,GAAG,WAAW,EAAE,CAAA;QAC1B,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,CAAO,GAAG,EAAE,IAAI,EAAE,eAAe,CAAC,EAAE,CAAA;IACpE,CAAC,CAAC,CACL,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Composable settings menu — registers a single slash command, renders
3
+ * an `InlineKeyboard`, routes callbacks. Features (language, history,
4
+ * etc.) contribute items; the bot builder adds custom items inline.
5
+ *
6
+ * ## Why menu is a separate primitive
7
+ *
8
+ * The user-facing menu is just UI — composing items. Features (per-user
9
+ * state, recording, gates) have their OWN runtime behaviour independent
10
+ * of any menu.
11
+ *
12
+ * ## GDPR rights (Forget / Export)
13
+ *
14
+ * If you pass `personalData: { storage }`, the menu adds two buttons:
15
+ *
16
+ * - 🗑 Forget my data — `storage.delete(sessionKey(userId))`
17
+ * - 📥 Export my data — `storage.get(sessionKey(userId))` → JSON file
18
+ *
19
+ * Because all per-user state across our plugins lives in ONE shared
20
+ * session record (see `bot/language`, `bot/message-history`), wiping
21
+ * or exporting that single key covers everything in one shot. No
22
+ * registry, no cascade, no per-plugin coordination.
23
+ *
24
+ * The `sessionKey` option defaults to `String(userId)` — matching
25
+ * `@gramio/session`'s default `getSessionKey: (ctx) => `${ctx.senderId}`.
26
+ * If you customize the session's `getSessionKey`, pass a matching
27
+ * function here.
28
+ *
29
+ * ## Privacy URL
30
+ *
31
+ * `privacy` defaults to [Telegram's Standard Bot Privacy Policy](https://telegram.org/privacy-tpa).
32
+ * Override when you retain content or process data beyond what the
33
+ * standard covers.
34
+ *
35
+ * Peer deps: `gramio`, `@gramio/storage`.
36
+ *
37
+ * @example Personal LLM bot — language only, no retention
38
+ *
39
+ * import { language } from '@adriangalilea/utils/bot/language'
40
+ * import { botMenu } from '@adriangalilea/utils/bot/menu'
41
+ *
42
+ * const lang = language({ session: userSession, supported: ['en','es'] as const, default: 'en' })
43
+ *
44
+ * const menu = botMenu({
45
+ * command: 'settings',
46
+ * description: 'Open settings',
47
+ * adminContact: '@adriangalilea',
48
+ * items: [lang.menuItem],
49
+ * })
50
+ *
51
+ * bot.extend(userSession).extend(lang.plugin).extend(menu.plugin)
52
+ *
53
+ * @example Bot with retention — adds Forget/Export
54
+ *
55
+ * const menu = botMenu({
56
+ * command: 'settings',
57
+ * description: 'Open settings',
58
+ * adminContact: '@adriangalilea',
59
+ * privacy: 'https://yourbot.com/privacy',
60
+ * personalData: { storage }, // ← enables Forget/Export
61
+ * items: [lang.menuItem],
62
+ * })
63
+ *
64
+ * bot
65
+ * .extend(userSession)
66
+ * .extend(history.plugin)
67
+ * .extend(menu.plugin)
68
+ */
69
+ import { Plugin } from 'gramio';
70
+ import type { Storage } from '@gramio/storage';
71
+ import { type Polyglot } from '../say/index.js';
72
+ type MenuCtx = {
73
+ bot: unknown;
74
+ from?: {
75
+ id: number;
76
+ };
77
+ chat?: {
78
+ id: number;
79
+ type: string;
80
+ };
81
+ session?: {
82
+ language?: string;
83
+ };
84
+ };
85
+ /**
86
+ * Anything a button or header can render as. Authoring a polyglot
87
+ * label is just an inline `{ en, es }` literal — `say()` resolves it
88
+ * to the recipient's language at render time.
89
+ *
90
+ * label: 'Static'
91
+ * label: { en: 'Settings', es: 'Ajustes' }
92
+ * label: (ctx) => `Hi ${ctx.from?.firstName}`
93
+ * label: (ctx) => ({ en: `Hi ${name}`, es: `Hola ${name}` })
94
+ */
95
+ type Label = string | Polyglot<string> | ((ctx: MenuCtx) => string | Polyglot<string>);
96
+ type Predicate = (ctx: MenuCtx) => boolean;
97
+ type Action = (ctx: MenuCtx) => Promise<void> | void;
98
+ export type MenuItem = {
99
+ id: string;
100
+ label: Label;
101
+ action: Action;
102
+ order?: number;
103
+ visible?: Predicate;
104
+ } | {
105
+ id: string;
106
+ label: Label;
107
+ url: string;
108
+ order?: number;
109
+ visible?: Predicate;
110
+ } | {
111
+ id: string;
112
+ label: Label;
113
+ submenu: MenuItem[];
114
+ order?: number;
115
+ visible?: Predicate;
116
+ };
117
+ export type PersonalDataOptions = {
118
+ /**
119
+ * Storage backend where each user's data lives. Must be the SAME
120
+ * instance you passed to your `session(...)` plugin — that's how
121
+ * /forget and /export reach the right keys.
122
+ */
123
+ storage: Storage;
124
+ /**
125
+ * How to compute the storage key for a given user id. Defaults to
126
+ * `String(userId)` — matching `@gramio/session`'s default
127
+ * `getSessionKey`. Override if your session uses a custom
128
+ * `getSessionKey`.
129
+ */
130
+ sessionKey?: (userId: number) => string;
131
+ };
132
+ export type BotMenuOptions = {
133
+ /** Slash command that opens the menu. Default `'settings'`. */
134
+ command?: string;
135
+ /** Description shown in Telegram's command list. */
136
+ description?: string;
137
+ /** Items rendered top-down (sorted by `order`, then registration). */
138
+ items?: MenuItem[];
139
+ /**
140
+ * URL to your privacy policy. Defaults to Telegram's Standard Bot
141
+ * Privacy Policy. Override when you retain content or process data
142
+ * beyond what the standard covers.
143
+ */
144
+ privacy?: string;
145
+ /**
146
+ * Header text rendered above the keyboard.
147
+ */
148
+ header?: Label;
149
+ /**
150
+ * Contact the user can reach when something fails (export error,
151
+ * etc.). **Required** — a bot that asks users to trust it with
152
+ * data must always offer a human to talk to when the automated
153
+ * paths fail.
154
+ */
155
+ adminContact: string;
156
+ /**
157
+ * Enables 🗑 Forget my data and 📥 Export my data buttons. Pass
158
+ * the storage instance backing your `session()`. If omitted, the
159
+ * buttons don't appear (use this for bots with no per-user state
160
+ * beyond what Telegram's standard policy covers).
161
+ */
162
+ personalData?: PersonalDataOptions;
163
+ };
164
+ type ResolvedPersonalData = {
165
+ storage: Storage;
166
+ sessionKey: (userId: number) => string;
167
+ };
168
+ type ResolvedOpts = {
169
+ command: string;
170
+ description: string;
171
+ privacy: string;
172
+ header: Label;
173
+ adminContact: string;
174
+ personalData: ResolvedPersonalData | null;
175
+ };
176
+ export declare class BotMenu {
177
+ /** @internal */
178
+ readonly _items: MenuItem[];
179
+ /** @internal */
180
+ readonly _opts: ResolvedOpts;
181
+ constructor(opts: BotMenuOptions);
182
+ /** Append a custom item. Mutates the menu. */
183
+ add(item: MenuItem): this;
184
+ /** The gramio plugin: registers the slash command + all callback handlers. */
185
+ get plugin(): Plugin<{}, import("gramio").DeriveDefinitions, {}>;
186
+ }
187
+ export declare const botMenu: (opts: BotMenuOptions) => BotMenu;
188
+ export {};
189
+ //# sourceMappingURL=menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../src/bot/menu.ts"],"names":[],"mappings":"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"}