@adriangalilea/utils 2.23.0 β†’ 2.24.1

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 CHANGED
@@ -517,6 +517,7 @@ Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for
517
517
  | `@adriangalilea/utils/bot/language` (picker surface) | The language-picker vocabulary and surfaces, so bots don't fork label lists: `flagFor` / `autonym` / `languageLabel` ("πŸ‡ͺπŸ‡Έ EspaΓ±ol"), `languagePickerItem({ label, codes, isActive, pick })` (the 2-up primary-highlighted MenuItem factory β€” storage and policy live in your closures), and `addLanguageRows(kb, { codes, pack, active?, activeStyle? })` (the raw-InlineKeyboard twin for onboarding / group-welcome keyboards; `activeStyle: "success"` when primary already marks something else on the keyboard). |
518
518
  | `@adriangalilea/utils/bot/user` | `userLabel(u)` β€” the conditional "[name] [@username] [id]" line every bot re-rolls for admin DMs and logs: `Ada Lovelace (@ada Β· 42)` / `Ada (42)` / `@ada (42)` / `id 42`, missing pieces drop instead of padding. Reads both gramio spellings (`firstName` / `first_name`); plain text by design. |
519
519
  | `@adriangalilea/utils/bot/urls` | Telegram-message URL + entity parsing, on what the platform ALREADY parsed (entities carry exact UTF-16 spans β€” never `startsWith("/")` or a mention regex). `urlsInMessage(msg)` β†’ the url module's `Url[]`: the visible text scanned by `urlsIn` PLUS `text_link` entities β€” hyperlinked words whose URL never appears in the text, invisible to any text scanner (what forwarded newsletters carry); a text_link's `raw` is its anchor words and its span covers them, so span-cutting treats linked words as the link. `isCommandMessage(msg)` (a `bot_command` entity at offset 0), `commandToken(msg)` (the leading command without any `@botname` suffix, `/summary@my_bot` β†’ `/summary`; null for ordinary text), `commandAddressee(msg)` (the bot a command is explicitly addressed to, lowercased, null when bare β€” the group addressing gate: a bare command may belong to any bot in the room, one addressed elsewhere never concerns yours), `cutEntities(text, entities, shouldCut)` (exact span surgery; the predicate sees each visible slice), `messageTextAndEntities(msg)` (text or caption with ITS entity set). Framework-agnostic by shape: gramio's `MessageEntity` class and the raw Bot API object both fit. |
520
+ | `@adriangalilea/utils/bot/announce` | The house announcement (news) template for bot mass messages, first-class: an `Announcement` = banner (`# πŸ“° <bot> News`), ordered sections (canonical `section.info/features/fixes` with πŸ’¬/✨/πŸ›, free-form allowed), italic closer, `Β· signature` (middle dot by design, never an em dash). `renderAnnouncement(a)` β†’ the Telegram-markdown subset; `renderAnnouncementBodies({en, es, …})` β†’ per-language bodies for a polyglot broadcast engine ("en" is the required fallback anchor). Pure, zero deps. |
520
521
  | `@adriangalilea/utils/bot/updates` | The pending-update queue of a webhook-less bot token, treated as the destroy-on-read resource it is (`getUpdates` with an offset CONFIRMS β€” destroys β€” everything below it). `updateQueue({ token, fetch?, apiRoot? })` β†’ `peek({ limit? })` (webhook state + pending count + the queue head, verbatim; sends NO offset so nothing is ever confirmed β€” repeatable, zero risk), `count()`, `webhookInfo()`, and `drain({ onBatch, limit?, timeoutS?, quietPolls? })` β€” the one-chance-safe sweep: each raw batch is handed to `onBatch` (persist it somewhere durable) and AWAITED before the offset advances, because the next call is the confirmation; a crash re-delivers the unconfirmed tail on re-run, and the drain ends only after several consecutive silent long-polls, never on the first empty response. A 409 (webhook registered / competing poller) is a typed `SourcedError`, never a silent retry. Plain `fetch`, injectable for tests; no framework. |
521
522
  | `@adriangalilea/utils/bot/inline-feedback` | Behavioral tripwire for BotFather's inline feedback probability (gates `chosen_inline_result`; defaults 0%, silently resets on bot transfer and inline-mode toggling, invisible to `getMe`): served-but-never-chosen crosses a threshold β†’ throttled admin DM naming the exact BotFather switch. Storage injected as three ops; never throws into the answer path. |
522
523
  | `@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. |
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The house announcement ("news") template for bot mass messages, as a first-class structure:
3
+ * compose an {@link Announcement} once, render it per language, and hand the bodies to a
4
+ * polyglot broadcast engine. The render targets the common Telegram-markdown subset
5
+ * (`# heading` β†’ bold, `- ` bullets, `*…*` italics) most bot pipelines already convert to
6
+ * Telegram HTML β€” no parse_mode assumptions live here.
7
+ *
8
+ * Shape (each section optional, order preserved):
9
+ *
10
+ * # πŸ“° <banner>
11
+ *
12
+ * πŸ’¬ Info
13
+ * - …
14
+ *
15
+ * ✨ New Features
16
+ * - …
17
+ *
18
+ * πŸ› Fixes
19
+ * - …
20
+ *
21
+ * _<closer>_
22
+ *
23
+ * Β· <signature>
24
+ *
25
+ * The signature separator is a middle dot by design β€” never an em dash.
26
+ */
27
+ export interface AnnouncementSection {
28
+ /** Leading emoji for the section title, e.g. "πŸ’¬". */
29
+ emoji: string;
30
+ /** Section title, e.g. "Info". */
31
+ title: string;
32
+ /** Bulleted items, one line each. */
33
+ items: string[];
34
+ }
35
+ export interface Announcement {
36
+ /** Banner line, e.g. "theSummaryBot News". Rendered as `# πŸ“° <banner>`. */
37
+ banner: string;
38
+ sections: AnnouncementSection[];
39
+ /** Italic closing line, e.g. "Questions? Feedback? Feel free to reach out!". */
40
+ closer?: string;
41
+ /** Attribution, e.g. "@adriangalilea". Rendered as `Β· <signature>`. */
42
+ signature?: string;
43
+ }
44
+ /** The three canonical sections, so every announcement wears the same iconography. */
45
+ export declare const section: {
46
+ info: (items: string[], title?: string) => AnnouncementSection;
47
+ features: (items: string[], title?: string) => AnnouncementSection;
48
+ fixes: (items: string[], title?: string) => AnnouncementSection;
49
+ };
50
+ /** Render one language's announcement to the Telegram-markdown subset. */
51
+ export declare function renderAnnouncement(a: Announcement): string;
52
+ /**
53
+ * Per-language bodies for a polyglot broadcast: language code β†’ rendered body. "en" is the
54
+ * required fallback anchor β€” an engine resolves each recipient's language and falls back to it.
55
+ */
56
+ export type AnnouncementBodies = Record<string, string>;
57
+ /** Render a per-language map of announcements into broadcast bodies. Throws without "en". */
58
+ export declare function renderAnnouncementBodies(byLanguage: Record<string, Announcement>): AnnouncementBodies;
59
+ //# sourceMappingURL=announce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../../src/bot/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,mBAAmB;IACnC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC5B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sFAAsF;AACtF,eAAO,MAAM,OAAO;kBACL,MAAM,EAAE,qBAAmB,mBAAmB;sBAC1C,MAAM,EAAE,qBAA2B,mBAAmB;mBACzD,MAAM,EAAE,qBAAoB,mBAAmB;CAC9D,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAU1D;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAExD,6FAA6F;AAC7F,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,kBAAkB,CAKrG"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The house announcement ("news") template for bot mass messages, as a first-class structure:
3
+ * compose an {@link Announcement} once, render it per language, and hand the bodies to a
4
+ * polyglot broadcast engine. The render targets the common Telegram-markdown subset
5
+ * (`# heading` β†’ bold, `- ` bullets, `*…*` italics) most bot pipelines already convert to
6
+ * Telegram HTML β€” no parse_mode assumptions live here.
7
+ *
8
+ * Shape (each section optional, order preserved):
9
+ *
10
+ * # πŸ“° <banner>
11
+ *
12
+ * πŸ’¬ Info
13
+ * - …
14
+ *
15
+ * ✨ New Features
16
+ * - …
17
+ *
18
+ * πŸ› Fixes
19
+ * - …
20
+ *
21
+ * _<closer>_
22
+ *
23
+ * Β· <signature>
24
+ *
25
+ * The signature separator is a middle dot by design β€” never an em dash.
26
+ */
27
+ /** The three canonical sections, so every announcement wears the same iconography. */
28
+ export const section = {
29
+ info: (items, title = "Info") => ({ emoji: "πŸ’¬", title, items }),
30
+ features: (items, title = "New Features") => ({ emoji: "✨", title, items }),
31
+ fixes: (items, title = "Fixes") => ({ emoji: "πŸ›", title, items }),
32
+ };
33
+ /** Render one language's announcement to the Telegram-markdown subset. */
34
+ export function renderAnnouncement(a) {
35
+ const blocks = [`# πŸ“° ${a.banner}`];
36
+ for (const s of a.sections) {
37
+ if (s.items.length === 0)
38
+ continue;
39
+ blocks.push(`${s.emoji} **${s.title}**`);
40
+ blocks.push(s.items.map((i) => `- ${i}`).join("\n"));
41
+ }
42
+ if (a.closer)
43
+ blocks.push(`*${a.closer}*`);
44
+ if (a.signature)
45
+ blocks.push(`Β· ${a.signature}`);
46
+ return blocks.join("\n\n");
47
+ }
48
+ /** Render a per-language map of announcements into broadcast bodies. Throws without "en". */
49
+ export function renderAnnouncementBodies(byLanguage) {
50
+ if (!byLanguage.en)
51
+ throw new Error("announcement bodies need an 'en' fallback");
52
+ const bodies = {};
53
+ for (const [lang, a] of Object.entries(byLanguage))
54
+ bodies[lang] = renderAnnouncement(a);
55
+ return bodies;
56
+ }
57
+ //# sourceMappingURL=announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.js","sourceRoot":"","sources":["../../src/bot/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAqBH,sFAAsF;AACtF,MAAM,CAAC,MAAM,OAAO,GAAG;IACtB,IAAI,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,MAAM,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC/F,QAAQ,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,cAAc,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC1G,KAAK,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,OAAO,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;CACjG,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB,CAAC,CAAe;IACjD,MAAM,MAAM,GAAa,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,CAAC,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,CAAC,SAAS;QAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAQD,6FAA6F;AAC7F,MAAM,UAAU,wBAAwB,CAAC,UAAwC;IAChF,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IACjF,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACzF,OAAO,MAAM,CAAC;AACf,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriangalilea/utils",
3
- "version": "2.23.0",
3
+ "version": "2.24.1",
4
4
  "description": "TypeScript utilities - logger, currency, formatter, GramIO bot plugins, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -156,6 +156,10 @@
156
156
  "types": "./dist/bot/urls.d.ts",
157
157
  "default": "./dist/bot/urls.js"
158
158
  },
159
+ "./bot/announce": {
160
+ "types": "./dist/bot/announce.d.ts",
161
+ "default": "./dist/bot/announce.js"
162
+ },
159
163
  "./bot/updates": {
160
164
  "types": "./dist/bot/updates.d.ts",
161
165
  "default": "./dist/bot/updates.js"
@@ -210,6 +214,7 @@
210
214
  "test:url": "tsx tests/url-test.ts",
211
215
  "test:bot-urls": "tsx tests/bot-urls-test.ts",
212
216
  "test:bot-updates": "tsx tests/bot-updates-test.ts",
217
+ "test:bot-announce": "tsx tests/bot-announce-test.ts",
213
218
  "test:reactions": "tsx tests/reactions-test.ts"
214
219
  },
215
220
  "keywords": [