@adriangalilea/utils 0.7.0 → 0.8.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 +1 -5
- package/dist/bot/access-control.d.ts +91 -58
- package/dist/bot/access-control.d.ts.map +1 -1
- package/dist/bot/access-control.js +105 -75
- package/dist/bot/access-control.js.map +1 -1
- package/dist/bot/index.d.ts +3 -0
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +3 -0
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/kit.d.ts.map +1 -1
- package/dist/bot/kit.js +6 -0
- package/dist/bot/kit.js.map +1 -1
- package/dist/bot/language.d.ts +279 -0
- package/dist/bot/language.d.ts.map +1 -0
- package/dist/bot/language.js +222 -0
- package/dist/bot/language.js.map +1 -0
- package/dist/bot/menu.d.ts +175 -0
- package/dist/bot/menu.d.ts.map +1 -0
- package/dist/bot/menu.js +284 -0
- package/dist/bot/menu.js.map +1 -0
- package/dist/bot/message-history.d.ts +259 -0
- package/dist/bot/message-history.d.ts.map +1 -0
- package/dist/bot/message-history.js +111 -0
- package/dist/bot/message-history.js.map +1 -0
- package/package.json +13 -1
- package/dist/currency/crypto-symbols-data.d.ts +0 -10
- package/dist/currency/crypto-symbols-data.d.ts.map +0 -1
- package/dist/currency/crypto-symbols-data.js +0 -13765
- package/dist/currency/crypto-symbols-data.js.map +0 -1
- package/dist/currency/crypto-symbols.d.ts +0 -20
- package/dist/currency/crypto-symbols.d.ts.map +0 -1
- package/dist/currency/crypto-symbols.js +0 -23
- package/dist/currency/crypto-symbols.js.map +0 -1
- package/dist/currency/download-crypto-list.d.ts +0 -10
- package/dist/currency/download-crypto-list.d.ts.map +0 -1
- package/dist/currency/download-crypto-list.js +0 -69
- package/dist/currency/download-crypto-list.js.map +0 -1
- package/dist/currency/index.d.ts +0 -84
- package/dist/currency/index.d.ts.map +0 -1
- package/dist/currency/index.js +0 -230
- package/dist/currency/index.js.map +0 -1
- package/dist/dir.d.ts +0 -40
- package/dist/dir.d.ts.map +0 -1
- package/dist/dir.js +0 -108
- package/dist/dir.js.map +0 -1
- package/dist/file.d.ts +0 -53
- package/dist/file.d.ts.map +0 -1
- package/dist/file.js +0 -211
- package/dist/file.js.map +0 -1
- package/dist/format.d.ts +0 -40
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -83
- package/dist/format.js.map +0 -1
- package/dist/kev.d.ts +0 -149
- package/dist/kev.d.ts.map +0 -1
- package/dist/kev.js +0 -761
- package/dist/kev.js.map +0 -1
- package/dist/log.d.ts +0 -91
- package/dist/log.d.ts.map +0 -1
- package/dist/log.js +0 -300
- package/dist/log.js.map +0 -1
- package/dist/logger.d.ts +0 -91
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -269
- package/dist/logger.js.map +0 -1
- package/dist/path.d.ts +0 -67
- package/dist/path.d.ts.map +0 -1
- package/dist/path.js +0 -107
- package/dist/path.js.map +0 -1
- package/dist/project.d.ts +0 -35
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -154
- package/dist/project.js.map +0 -1
|
@@ -0,0 +1,279 @@
|
|
|
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 { type DeriveDefinitions, Plugin } from 'gramio';
|
|
60
|
+
import { session } from '@gramio/session';
|
|
61
|
+
import type { MenuItem } from './menu.js';
|
|
62
|
+
/** Branded BCP-47 language tag — obtainable only via the validators below. */
|
|
63
|
+
export type LangCode = string & {
|
|
64
|
+
readonly __langCode: unique symbol;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Validate + canonicalize a BCP-47 tag using the standard
|
|
68
|
+
* `Intl.getCanonicalLocales`. Throws `RangeError` on invalid input.
|
|
69
|
+
* Canonicalizes casing: `'en-us'` → `'en-US'`.
|
|
70
|
+
*/
|
|
71
|
+
export declare const parseLangCode: (s: string) => LangCode;
|
|
72
|
+
/** Non-throwing type guard. Same parser path as `parseLangCode`. */
|
|
73
|
+
export declare const isLangCode: (s: unknown) => s is LangCode;
|
|
74
|
+
export type LanguageScopeStrategy = 'user' | 'chat';
|
|
75
|
+
export type LanguageScope = LanguageScopeStrategy | {
|
|
76
|
+
private?: LanguageScopeStrategy;
|
|
77
|
+
group?: LanguageScopeStrategy;
|
|
78
|
+
supergroup?: LanguageScopeStrategy;
|
|
79
|
+
channel?: LanguageScopeStrategy;
|
|
80
|
+
};
|
|
81
|
+
/** Loose session shape — the plugin only touches the `language` field. */
|
|
82
|
+
type SessionLike = {
|
|
83
|
+
language?: string;
|
|
84
|
+
};
|
|
85
|
+
/** @internal — kept unexported so it doesn't clash with peers' refs. */
|
|
86
|
+
type LangSessionPluginRef = ReturnType<typeof session<SessionLike, 'session'>>;
|
|
87
|
+
export type LanguageOptions<Langs extends readonly string[]> = {
|
|
88
|
+
/**
|
|
89
|
+
* The session plugin to read/write `ctx.session.language` from.
|
|
90
|
+
* Must be extended on the bot before this plugin (gramio's runtime
|
|
91
|
+
* dedup ensures the session derive only runs once per update).
|
|
92
|
+
*/
|
|
93
|
+
session: LangSessionPluginRef;
|
|
94
|
+
/** Tuple of BCP-47 tags. Each validated via `Intl.getCanonicalLocales`. */
|
|
95
|
+
supported: Langs;
|
|
96
|
+
/** Must be a member of `supported`. */
|
|
97
|
+
default: Langs[number];
|
|
98
|
+
/** See module docstring. Per chat-type override possible. */
|
|
99
|
+
scope?: LanguageScope;
|
|
100
|
+
/**
|
|
101
|
+
* Override per-language menu label. Default uses an emoji flag prefix
|
|
102
|
+
* derived from the language code.
|
|
103
|
+
*/
|
|
104
|
+
labels?: Partial<Record<Langs[number], string>>;
|
|
105
|
+
/** Header text for the language sub-menu. Default `'🌐 Language'`. */
|
|
106
|
+
menuLabel?: string;
|
|
107
|
+
};
|
|
108
|
+
export type LanguageFeature<Lang extends string> = {
|
|
109
|
+
plugin: ReturnType<typeof buildLanguagePlugin<Lang>>;
|
|
110
|
+
menuItem: MenuItem;
|
|
111
|
+
};
|
|
112
|
+
type LanguageDerives<Lang extends string> = {
|
|
113
|
+
lang: Lang;
|
|
114
|
+
};
|
|
115
|
+
export declare const language: <const Langs extends readonly string[]>(opts: LanguageOptions<Langs>) => LanguageFeature<Langs[number]>;
|
|
116
|
+
declare const buildLanguagePlugin: <Lang extends string>(args: {
|
|
117
|
+
sessionPlugin: LangSessionPluginRef;
|
|
118
|
+
canonicalSet: ReadonlySet<string>;
|
|
119
|
+
defaultLanguage: Lang;
|
|
120
|
+
matchSupported: (s: string | undefined) => Lang | undefined;
|
|
121
|
+
scopeOpt: LanguageScope | undefined;
|
|
122
|
+
}) => Plugin<{}, DeriveDefinitions & {
|
|
123
|
+
global: LanguageDerives<Lang>;
|
|
124
|
+
} & {
|
|
125
|
+
message: {
|
|
126
|
+
session: SessionLike & {
|
|
127
|
+
$clear: () => Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
channel_post: {
|
|
131
|
+
session: SessionLike & {
|
|
132
|
+
$clear: () => Promise<void>;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
inline_query: {
|
|
136
|
+
session: SessionLike & {
|
|
137
|
+
$clear: () => Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
chosen_inline_result: {
|
|
141
|
+
session: SessionLike & {
|
|
142
|
+
$clear: () => Promise<void>;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
callback_query: {
|
|
146
|
+
session: SessionLike & {
|
|
147
|
+
$clear: () => Promise<void>;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
shipping_query: {
|
|
151
|
+
session: SessionLike & {
|
|
152
|
+
$clear: () => Promise<void>;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
pre_checkout_query: {
|
|
156
|
+
session: SessionLike & {
|
|
157
|
+
$clear: () => Promise<void>;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
poll_answer: {
|
|
161
|
+
session: SessionLike & {
|
|
162
|
+
$clear: () => Promise<void>;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
chat_join_request: {
|
|
166
|
+
session: SessionLike & {
|
|
167
|
+
$clear: () => Promise<void>;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
new_chat_members: {
|
|
171
|
+
session: SessionLike & {
|
|
172
|
+
$clear: () => Promise<void>;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
new_chat_title: {
|
|
176
|
+
session: SessionLike & {
|
|
177
|
+
$clear: () => Promise<void>;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
new_chat_photo: {
|
|
181
|
+
session: SessionLike & {
|
|
182
|
+
$clear: () => Promise<void>;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
delete_chat_photo: {
|
|
186
|
+
session: SessionLike & {
|
|
187
|
+
$clear: () => Promise<void>;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
group_chat_created: {
|
|
191
|
+
session: SessionLike & {
|
|
192
|
+
$clear: () => Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
message_auto_delete_timer_changed: {
|
|
196
|
+
session: SessionLike & {
|
|
197
|
+
$clear: () => Promise<void>;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
migrate_to_chat_id: {
|
|
201
|
+
session: SessionLike & {
|
|
202
|
+
$clear: () => Promise<void>;
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
migrate_from_chat_id: {
|
|
206
|
+
session: SessionLike & {
|
|
207
|
+
$clear: () => Promise<void>;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
pinned_message: {
|
|
211
|
+
session: SessionLike & {
|
|
212
|
+
$clear: () => Promise<void>;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
invoice: {
|
|
216
|
+
session: SessionLike & {
|
|
217
|
+
$clear: () => Promise<void>;
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
successful_payment: {
|
|
221
|
+
session: SessionLike & {
|
|
222
|
+
$clear: () => Promise<void>;
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
chat_shared: {
|
|
226
|
+
session: SessionLike & {
|
|
227
|
+
$clear: () => Promise<void>;
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
proximity_alert_triggered: {
|
|
231
|
+
session: SessionLike & {
|
|
232
|
+
$clear: () => Promise<void>;
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
video_chat_scheduled: {
|
|
236
|
+
session: SessionLike & {
|
|
237
|
+
$clear: () => Promise<void>;
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
video_chat_started: {
|
|
241
|
+
session: SessionLike & {
|
|
242
|
+
$clear: () => Promise<void>;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
video_chat_ended: {
|
|
246
|
+
session: SessionLike & {
|
|
247
|
+
$clear: () => Promise<void>;
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
video_chat_participants_invited: {
|
|
251
|
+
session: SessionLike & {
|
|
252
|
+
$clear: () => Promise<void>;
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
web_app_data: {
|
|
256
|
+
session: SessionLike & {
|
|
257
|
+
$clear: () => Promise<void>;
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
location: {
|
|
261
|
+
session: SessionLike & {
|
|
262
|
+
$clear: () => Promise<void>;
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
passport_data: {
|
|
266
|
+
session: SessionLike & {
|
|
267
|
+
$clear: () => Promise<void>;
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
} & {
|
|
271
|
+
message: {
|
|
272
|
+
lang: Lang;
|
|
273
|
+
};
|
|
274
|
+
callback_query: {
|
|
275
|
+
lang: Lang;
|
|
276
|
+
};
|
|
277
|
+
}, {}>;
|
|
278
|
+
export {};
|
|
279
|
+
//# sourceMappingURL=language.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language.d.ts","sourceRoot":"","sources":["../../src/bot/language.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
/**
|
|
61
|
+
* Validate + canonicalize a BCP-47 tag using the standard
|
|
62
|
+
* `Intl.getCanonicalLocales`. Throws `RangeError` on invalid input.
|
|
63
|
+
* Canonicalizes casing: `'en-us'` → `'en-US'`.
|
|
64
|
+
*/
|
|
65
|
+
export const parseLangCode = (s) => Intl.getCanonicalLocales(s)[0];
|
|
66
|
+
/** Non-throwing type guard. Same parser path as `parseLangCode`. */
|
|
67
|
+
export const isLangCode = (s) => {
|
|
68
|
+
if (typeof s !== 'string')
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
Intl.getCanonicalLocales(s);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
// ─── scope helpers ─────────────────────────────────────────────────
|
|
79
|
+
const DEFAULT_SCOPE = {
|
|
80
|
+
private: 'user',
|
|
81
|
+
group: 'chat',
|
|
82
|
+
supergroup: 'chat',
|
|
83
|
+
channel: 'chat',
|
|
84
|
+
};
|
|
85
|
+
const resolveScope = (scope, chatType) => {
|
|
86
|
+
if (typeof scope === 'string')
|
|
87
|
+
return scope;
|
|
88
|
+
const t = chatType;
|
|
89
|
+
return scope?.[t] ?? DEFAULT_SCOPE[t] ?? 'user';
|
|
90
|
+
};
|
|
91
|
+
// ─── flag emoji (best-effort) ──────────────────────────────────────
|
|
92
|
+
const REGIONLESS_FLAGS = {
|
|
93
|
+
en: '🇬🇧', es: '🇪🇸', fr: '🇫🇷', de: '🇩🇪', it: '🇮🇹',
|
|
94
|
+
pt: '🇵🇹', ru: '🇷🇺', zh: '🇨🇳', ja: '🇯🇵', ko: '🇰🇷',
|
|
95
|
+
ar: '🇸🇦', tr: '🇹🇷', pl: '🇵🇱', nl: '🇳🇱', uk: '🇺🇦',
|
|
96
|
+
};
|
|
97
|
+
const regionToFlag = (region) => String.fromCodePoint(...region.toUpperCase().split('').map((c) => 0x1f1a5 + c.charCodeAt(0)));
|
|
98
|
+
const flagFor = (lang) => {
|
|
99
|
+
const parts = lang.split('-');
|
|
100
|
+
if (parts.length > 1) {
|
|
101
|
+
const region = parts.find((p) => /^[A-Z]{2}$/.test(p));
|
|
102
|
+
if (region)
|
|
103
|
+
return regionToFlag(region);
|
|
104
|
+
}
|
|
105
|
+
return REGIONLESS_FLAGS[parts[0].toLowerCase()] ?? '🌐';
|
|
106
|
+
};
|
|
107
|
+
const autonym = (lang) => {
|
|
108
|
+
try {
|
|
109
|
+
const dn = new Intl.DisplayNames([lang], { type: 'language' });
|
|
110
|
+
return dn.of(lang) ?? lang;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return lang;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const defaultLabel = (lang) => `${flagFor(lang)} ${autonym(lang)}`;
|
|
117
|
+
// ─── callback schema ───────────────────────────────────────────────
|
|
118
|
+
const setLangCb = new CallbackData('lang').string('code');
|
|
119
|
+
// ─── feature factory ───────────────────────────────────────────────
|
|
120
|
+
export const language = (opts) => {
|
|
121
|
+
const scopeOpt = opts.scope;
|
|
122
|
+
const labels = (opts.labels ?? {});
|
|
123
|
+
const menuLabel = opts.menuLabel ?? '🌐 Language';
|
|
124
|
+
// Canonicalize all supported tags at construction.
|
|
125
|
+
const canonical = opts.supported.map((l) => {
|
|
126
|
+
try {
|
|
127
|
+
return Intl.getCanonicalLocales(l)[0];
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`language: "${l}" is not a valid BCP-47 tag (per Intl.getCanonicalLocales)`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const canonicalSet = new Set(canonical);
|
|
134
|
+
let defaultLanguage;
|
|
135
|
+
try {
|
|
136
|
+
defaultLanguage = Intl.getCanonicalLocales(opts.default)[0];
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`language: default "${opts.default}" is not a valid BCP-47 tag`);
|
|
140
|
+
}
|
|
141
|
+
if (!canonicalSet.has(defaultLanguage)) {
|
|
142
|
+
throw new Error(`language: default "${defaultLanguage}" is not in supported[]`);
|
|
143
|
+
}
|
|
144
|
+
const matchSupported = (s) => {
|
|
145
|
+
if (!s)
|
|
146
|
+
return undefined;
|
|
147
|
+
try {
|
|
148
|
+
const c = Intl.getCanonicalLocales(s)[0];
|
|
149
|
+
if (canonicalSet.has(c))
|
|
150
|
+
return c;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// fall through
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
};
|
|
157
|
+
const plugin = buildLanguagePlugin({
|
|
158
|
+
sessionPlugin: opts.session,
|
|
159
|
+
canonicalSet,
|
|
160
|
+
defaultLanguage,
|
|
161
|
+
matchSupported,
|
|
162
|
+
scopeOpt,
|
|
163
|
+
});
|
|
164
|
+
const menuItem = {
|
|
165
|
+
id: 'lang',
|
|
166
|
+
label: menuLabel,
|
|
167
|
+
submenu: canonical.map((l) => ({
|
|
168
|
+
id: l,
|
|
169
|
+
label: (ctx) => {
|
|
170
|
+
const current = ctx.lang;
|
|
171
|
+
const marker = current === l ? '●' : '○';
|
|
172
|
+
const base = labels[l] ?? defaultLabel(l);
|
|
173
|
+
return `${marker} ${base}`;
|
|
174
|
+
},
|
|
175
|
+
action: async (ctx) => {
|
|
176
|
+
// ctx.session is the shared session record. Mutating any
|
|
177
|
+
// field on it goes through @gramio/session's Proxy and
|
|
178
|
+
// auto-persists. We own the `language` field by convention.
|
|
179
|
+
const c = ctx;
|
|
180
|
+
c.session.language = l;
|
|
181
|
+
await c.answer({ text: `✓ ${l}` });
|
|
182
|
+
try {
|
|
183
|
+
await c.delete?.();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// not always deletable
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
return { plugin, menuItem };
|
|
192
|
+
};
|
|
193
|
+
// ─── plugin builder ────────────────────────────────────────────────
|
|
194
|
+
const buildLanguagePlugin = (args) => {
|
|
195
|
+
const { sessionPlugin, canonicalSet, defaultLanguage, matchSupported, scopeOpt } = args;
|
|
196
|
+
return (new Plugin('@adriangalilea/utils/bot/language')
|
|
197
|
+
// Declare the session as a dependency. gramio's runtime dedupes
|
|
198
|
+
// this against the bot's top-level session extension so the
|
|
199
|
+
// session derive runs exactly once per update — but the types
|
|
200
|
+
// (ctx.session: SessionLike) flow into our handlers below.
|
|
201
|
+
.extend(sessionPlugin)
|
|
202
|
+
.derive(['message', 'callback_query'], (ctx) => {
|
|
203
|
+
// 1) stored override
|
|
204
|
+
const stored = ctx.session.language;
|
|
205
|
+
if (stored && canonicalSet.has(stored)) {
|
|
206
|
+
return { lang: stored };
|
|
207
|
+
}
|
|
208
|
+
// 2) Telegram hint — only when in user-scoped resolution
|
|
209
|
+
const chatType = ctx.is('message')
|
|
210
|
+
? ctx.chat.type
|
|
211
|
+
: ctx.message?.chat.type ?? 'private';
|
|
212
|
+
const strategy = resolveScope(scopeOpt, chatType);
|
|
213
|
+
if (strategy === 'user') {
|
|
214
|
+
const hint = matchSupported(ctx.from.languageCode);
|
|
215
|
+
if (hint)
|
|
216
|
+
return { lang: hint };
|
|
217
|
+
}
|
|
218
|
+
// 3) configured default
|
|
219
|
+
return { lang: defaultLanguage };
|
|
220
|
+
}));
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=language.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language.js","sourceRoot":"","sources":["../../src/bot/language.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
type MenuCtx = {
|
|
72
|
+
bot: unknown;
|
|
73
|
+
from?: {
|
|
74
|
+
id: number;
|
|
75
|
+
};
|
|
76
|
+
chat?: {
|
|
77
|
+
id: number;
|
|
78
|
+
type: string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
type Label = string | ((ctx: MenuCtx) => string);
|
|
82
|
+
type Predicate = (ctx: MenuCtx) => boolean;
|
|
83
|
+
type Action = (ctx: MenuCtx) => Promise<void> | void;
|
|
84
|
+
export type MenuItem = {
|
|
85
|
+
id: string;
|
|
86
|
+
label: Label;
|
|
87
|
+
action: Action;
|
|
88
|
+
order?: number;
|
|
89
|
+
visible?: Predicate;
|
|
90
|
+
} | {
|
|
91
|
+
id: string;
|
|
92
|
+
label: Label;
|
|
93
|
+
url: string;
|
|
94
|
+
order?: number;
|
|
95
|
+
visible?: Predicate;
|
|
96
|
+
} | {
|
|
97
|
+
id: string;
|
|
98
|
+
label: Label;
|
|
99
|
+
submenu: MenuItem[];
|
|
100
|
+
order?: number;
|
|
101
|
+
visible?: Predicate;
|
|
102
|
+
};
|
|
103
|
+
export type PersonalDataOptions = {
|
|
104
|
+
/**
|
|
105
|
+
* Storage backend where each user's data lives. Must be the SAME
|
|
106
|
+
* instance you passed to your `session(...)` plugin — that's how
|
|
107
|
+
* /forget and /export reach the right keys.
|
|
108
|
+
*/
|
|
109
|
+
storage: Storage;
|
|
110
|
+
/**
|
|
111
|
+
* How to compute the storage key for a given user id. Defaults to
|
|
112
|
+
* `String(userId)` — matching `@gramio/session`'s default
|
|
113
|
+
* `getSessionKey`. Override if your session uses a custom
|
|
114
|
+
* `getSessionKey`.
|
|
115
|
+
*/
|
|
116
|
+
sessionKey?: (userId: number) => string;
|
|
117
|
+
};
|
|
118
|
+
export type BotMenuOptions = {
|
|
119
|
+
/** Slash command that opens the menu. Default `'settings'`. */
|
|
120
|
+
command?: string;
|
|
121
|
+
/** Description shown in Telegram's command list. */
|
|
122
|
+
description?: string;
|
|
123
|
+
/** Items rendered top-down (sorted by `order`, then registration). */
|
|
124
|
+
items?: MenuItem[];
|
|
125
|
+
/**
|
|
126
|
+
* URL to your privacy policy. Defaults to Telegram's Standard Bot
|
|
127
|
+
* Privacy Policy. Override when you retain content or process data
|
|
128
|
+
* beyond what the standard covers.
|
|
129
|
+
*/
|
|
130
|
+
privacy?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Header text rendered above the keyboard.
|
|
133
|
+
*/
|
|
134
|
+
header?: Label;
|
|
135
|
+
/**
|
|
136
|
+
* Contact the user can reach when something fails (export error,
|
|
137
|
+
* etc.). **Required** — a bot that asks users to trust it with
|
|
138
|
+
* data must always offer a human to talk to when the automated
|
|
139
|
+
* paths fail.
|
|
140
|
+
*/
|
|
141
|
+
adminContact: string;
|
|
142
|
+
/**
|
|
143
|
+
* Enables 🗑 Forget my data and 📥 Export my data buttons. Pass
|
|
144
|
+
* the storage instance backing your `session()`. If omitted, the
|
|
145
|
+
* buttons don't appear (use this for bots with no per-user state
|
|
146
|
+
* beyond what Telegram's standard policy covers).
|
|
147
|
+
*/
|
|
148
|
+
personalData?: PersonalDataOptions;
|
|
149
|
+
};
|
|
150
|
+
type ResolvedPersonalData = {
|
|
151
|
+
storage: Storage;
|
|
152
|
+
sessionKey: (userId: number) => string;
|
|
153
|
+
};
|
|
154
|
+
type ResolvedOpts = {
|
|
155
|
+
command: string;
|
|
156
|
+
description: string;
|
|
157
|
+
privacy: string;
|
|
158
|
+
header: Label;
|
|
159
|
+
adminContact: string;
|
|
160
|
+
personalData: ResolvedPersonalData | null;
|
|
161
|
+
};
|
|
162
|
+
export declare class BotMenu {
|
|
163
|
+
/** @internal */
|
|
164
|
+
readonly _items: MenuItem[];
|
|
165
|
+
/** @internal */
|
|
166
|
+
readonly _opts: ResolvedOpts;
|
|
167
|
+
constructor(opts: BotMenuOptions);
|
|
168
|
+
/** Append a custom item. Mutates the menu. */
|
|
169
|
+
add(item: MenuItem): this;
|
|
170
|
+
/** The gramio plugin: registers the slash command + all callback handlers. */
|
|
171
|
+
get plugin(): Plugin<{}, import("gramio").DeriveDefinitions, {}>;
|
|
172
|
+
}
|
|
173
|
+
export declare const botMenu: (opts: BotMenuOptions) => BotMenu;
|
|
174
|
+
export {};
|
|
175
|
+
//# sourceMappingURL=menu.d.ts.map
|