@adriangalilea/utils 2.22.0 β 2.23.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 -0
- package/dist/bot/updates.d.ts +53 -0
- package/dist/bot/updates.d.ts.map +1 -0
- package/dist/bot/updates.js +94 -0
- package/dist/bot/updates.js.map +1 -0
- package/package.json +6 -1
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/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. |
|
|
520
521
|
| `@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. |
|
|
521
522
|
| `@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. |
|
|
522
523
|
| `@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`. |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** A raw Bot API update. Only `update_id` is load-bearing here; everything else passes through. */
|
|
2
|
+
export interface TelegramUpdate {
|
|
3
|
+
update_id: number;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface QueuePeek {
|
|
7
|
+
/** The registered webhook URL, or null when the token polls (getUpdates works). */
|
|
8
|
+
webhookUrl: string | null;
|
|
9
|
+
/** Updates waiting server-side (`getWebhookInfo.pending_update_count`). */
|
|
10
|
+
pending: number;
|
|
11
|
+
/** The first updates in the queue, verbatim. Empty when a webhook is registered (polling
|
|
12
|
+
* would 409) β the count above still tells the story. */
|
|
13
|
+
head: TelegramUpdate[];
|
|
14
|
+
}
|
|
15
|
+
export interface DrainResult {
|
|
16
|
+
drained: number;
|
|
17
|
+
batches: number;
|
|
18
|
+
lastUpdateId?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface UpdateQueueOptions {
|
|
21
|
+
token: string;
|
|
22
|
+
/** Transport override for tests. Defaults to global fetch. */
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
/** Bot API origin override (local Bot API servers). */
|
|
25
|
+
apiRoot?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Peek/drain/count over one bot token's pending-update queue. See the module doc for the
|
|
28
|
+
* destroy-on-read contract each verb upholds. */
|
|
29
|
+
export declare function updateQueue(options: UpdateQueueOptions): {
|
|
30
|
+
/** Raw `getWebhookInfo`. */
|
|
31
|
+
webhookInfo(): Promise<Record<string, unknown>>;
|
|
32
|
+
/** Updates waiting server-side, without touching any of them. */
|
|
33
|
+
count(): Promise<number>;
|
|
34
|
+
/** Non-destructive look: webhook state, pending count, and the first `limit` updates,
|
|
35
|
+
* verbatim. Confirms NOTHING β repeat as often as you like. */
|
|
36
|
+
peek({ limit }?: {
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<QueuePeek>;
|
|
39
|
+
/**
|
|
40
|
+
* Drain the queue, one-chance-safe. `onBatch` receives each raw batch and is AWAITED
|
|
41
|
+
* before the offset advances β persist there, because the next `getUpdates` call is the
|
|
42
|
+
* confirmation that destroys the batch server-side. An `onBatch` throw aborts the drain
|
|
43
|
+
* with nothing confirmed beyond what `onBatch` already accepted; re-running resumes at
|
|
44
|
+
* the unconfirmed tail. Ends after `quietPolls` consecutive empty long-polls.
|
|
45
|
+
*/
|
|
46
|
+
drain({ onBatch, limit, timeoutS, quietPolls, }: {
|
|
47
|
+
onBatch: (updates: TelegramUpdate[]) => void | Promise<void>;
|
|
48
|
+
limit?: number;
|
|
49
|
+
timeoutS?: number;
|
|
50
|
+
quietPolls?: number;
|
|
51
|
+
}): Promise<DrainResult>;
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=updates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updates.d.ts","sourceRoot":"","sources":["../../src/bot/updates.ts"],"names":[],"mappings":"AAqBA,mGAAmG;AACnG,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACzB,mFAAmF;IACnF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB;8DAC0D;IAC1D,IAAI,EAAE,cAAc,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;kDACkD;AAClD,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB;IA0BrD,4BAA4B;mBACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAI/C,iEAAiE;aAClD,OAAO,CAAC,MAAM,CAAC;IAK9B;oEACgE;qBACtC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAQ,OAAO,CAAC,SAAS,CAAC;IAQtE;;;;;;OAMG;qDAMA;QACF,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,WAAW,CAAC;EAsBzB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pending-update queue of a webhook-less bot token, handled with the respect a
|
|
3
|
+
* destroy-on-read resource deserves. Telegram's `getUpdates` CONFIRMS (discards) every update
|
|
4
|
+
* older than the offset it is called with β reading is destructive, and a queue drained
|
|
5
|
+
* carelessly is gone forever. Two safety properties are the whole point:
|
|
6
|
+
*
|
|
7
|
+
* PEEK is free. `getUpdates` WITHOUT an offset confirms nothing: webhook state, the pending
|
|
8
|
+
* count, and the head of the queue are all inspectable, repeatably, at zero risk.
|
|
9
|
+
*
|
|
10
|
+
* DRAIN is write-before-confirm. Each raw batch is handed to the caller's `onBatch` (persist
|
|
11
|
+
* it there β disk, db, anywhere durable) and AWAITED before the next `getUpdates` call
|
|
12
|
+
* advances the offset, because that next call IS the confirmation. A crash mid-drain loses
|
|
13
|
+
* nothing: re-run, and the unconfirmed tail re-delivers. The drain ends only after the queue
|
|
14
|
+
* stays silent across several consecutive long-polls, never on the first empty response.
|
|
15
|
+
*
|
|
16
|
+
* Framework-free: plain `fetch` against the Bot API, injectable for tests. A 409 means the
|
|
17
|
+
* token has a webhook registered or another poller is competing β surfaced as a typed error,
|
|
18
|
+
* never retried silently.
|
|
19
|
+
*/
|
|
20
|
+
import { SourcedError } from "../offensive.js";
|
|
21
|
+
/** Peek/drain/count over one bot token's pending-update queue. See the module doc for the
|
|
22
|
+
* destroy-on-read contract each verb upholds. */
|
|
23
|
+
export function updateQueue(options) {
|
|
24
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
25
|
+
const root = options.apiRoot ?? "https://api.telegram.org";
|
|
26
|
+
const call = async (method, params) => {
|
|
27
|
+
const res = await doFetch(`${root}/bot${options.token}/${method}`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify(params ?? {}),
|
|
31
|
+
});
|
|
32
|
+
const body = (await res.json());
|
|
33
|
+
if (!body.ok) {
|
|
34
|
+
throw new SourcedError({
|
|
35
|
+
source: "telegram",
|
|
36
|
+
operation: method,
|
|
37
|
+
message: body.error_code === 409
|
|
38
|
+
? `${body.description ?? "409"} (a webhook is registered, or another poller is competing)`
|
|
39
|
+
: (body.description ?? "unknown error"),
|
|
40
|
+
status: body.error_code ?? res.status,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return body.result;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
/** Raw `getWebhookInfo`. */
|
|
47
|
+
webhookInfo() {
|
|
48
|
+
return call("getWebhookInfo");
|
|
49
|
+
},
|
|
50
|
+
/** Updates waiting server-side, without touching any of them. */
|
|
51
|
+
async count() {
|
|
52
|
+
const info = await call("getWebhookInfo");
|
|
53
|
+
return info.pending_update_count ?? 0;
|
|
54
|
+
},
|
|
55
|
+
/** Non-destructive look: webhook state, pending count, and the first `limit` updates,
|
|
56
|
+
* verbatim. Confirms NOTHING β repeat as often as you like. */
|
|
57
|
+
async peek({ limit = 1 } = {}) {
|
|
58
|
+
const info = await call("getWebhookInfo");
|
|
59
|
+
const webhookUrl = info.url ? info.url : null;
|
|
60
|
+
// With a webhook registered getUpdates would 409; the count already tells the story.
|
|
61
|
+
const head = webhookUrl ? [] : await call("getUpdates", { limit, timeout: 0 });
|
|
62
|
+
return { webhookUrl, pending: info.pending_update_count ?? 0, head };
|
|
63
|
+
},
|
|
64
|
+
/**
|
|
65
|
+
* Drain the queue, one-chance-safe. `onBatch` receives each raw batch and is AWAITED
|
|
66
|
+
* before the offset advances β persist there, because the next `getUpdates` call is the
|
|
67
|
+
* confirmation that destroys the batch server-side. An `onBatch` throw aborts the drain
|
|
68
|
+
* with nothing confirmed beyond what `onBatch` already accepted; re-running resumes at
|
|
69
|
+
* the unconfirmed tail. Ends after `quietPolls` consecutive empty long-polls.
|
|
70
|
+
*/
|
|
71
|
+
async drain({ onBatch, limit = 100, timeoutS = 10, quietPolls = 3, }) {
|
|
72
|
+
let offset;
|
|
73
|
+
let drained = 0;
|
|
74
|
+
let batches = 0;
|
|
75
|
+
let lastUpdateId;
|
|
76
|
+
let quiet = 0;
|
|
77
|
+
while (quiet < quietPolls) {
|
|
78
|
+
const batch = await call("getUpdates", { offset, limit, timeout: timeoutS });
|
|
79
|
+
if (batch.length === 0) {
|
|
80
|
+
quiet += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
quiet = 0;
|
|
84
|
+
await onBatch(batch); // persist FIRSTβ¦
|
|
85
|
+
lastUpdateId = batch[batch.length - 1].update_id;
|
|
86
|
+
offset = lastUpdateId + 1; // β¦the NEXT call confirms
|
|
87
|
+
drained += batch.length;
|
|
88
|
+
batches += 1;
|
|
89
|
+
}
|
|
90
|
+
return { drained, batches, lastUpdateId };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=updates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updates.js","sourceRoot":"","sources":["../../src/bot/updates.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adriangalilea/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
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/updates": {
|
|
160
|
+
"types": "./dist/bot/updates.d.ts",
|
|
161
|
+
"default": "./dist/bot/updates.js"
|
|
162
|
+
},
|
|
159
163
|
"./bot/reactions": {
|
|
160
164
|
"types": "./dist/bot/reactions.d.ts",
|
|
161
165
|
"default": "./dist/bot/reactions.js"
|
|
@@ -205,6 +209,7 @@
|
|
|
205
209
|
"test:self-echo": "tsx tests/self-echo-test.ts",
|
|
206
210
|
"test:url": "tsx tests/url-test.ts",
|
|
207
211
|
"test:bot-urls": "tsx tests/bot-urls-test.ts",
|
|
212
|
+
"test:bot-updates": "tsx tests/bot-updates-test.ts",
|
|
208
213
|
"test:reactions": "tsx tests/reactions-test.ts"
|
|
209
214
|
},
|
|
210
215
|
"keywords": [
|