@wrongstack/telegram 0.309.1 → 0.310.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.
@@ -0,0 +1,53 @@
1
+ import type { Logger } from '@wrongstack/core/types';
2
+ import type { TelegramApiCallbackQuery, TelegramApiClient } from './api-client.js';
3
+ export interface TelegramApprovalResult {
4
+ approved: boolean;
5
+ fromUser: string;
6
+ fromUserId?: number | undefined;
7
+ }
8
+ export interface TelegramApprovalRequestInput {
9
+ requestId: string;
10
+ sessionId: string;
11
+ expectedChatId: string | number;
12
+ expectedUserIds: readonly (string | number)[];
13
+ allowGroup: boolean;
14
+ expiresAt: number;
15
+ signal?: AbortSignal | undefined;
16
+ }
17
+ type TelegramApprovalRequestState = 'pending' | 'resolved' | 'expired' | 'cancelled';
18
+ interface TelegramApprovalRequest {
19
+ requestId: string;
20
+ sessionId: string;
21
+ expectedChatId: string;
22
+ expectedUserIds: ReadonlySet<string>;
23
+ allowGroup: boolean;
24
+ promptMessageId?: number | undefined;
25
+ pendingCallbacks: TelegramApiCallbackQuery[];
26
+ expiresAt: number;
27
+ state: TelegramApprovalRequestState;
28
+ resolve: (value: TelegramApprovalResult) => void;
29
+ timer: ReturnType<typeof setTimeout>;
30
+ signal?: AbortSignal | undefined;
31
+ abortHandler?: (() => void) | undefined;
32
+ }
33
+ export interface ApprovalFlowDeps {
34
+ log: Logger;
35
+ api: () => TelegramApiClient;
36
+ inboundDenialReason(userId: string | undefined, chatId: string | undefined): 'user' | 'chat' | undefined;
37
+ }
38
+ export declare class ApprovalFlow {
39
+ readonly callbackWaiters: Map<string, TelegramApprovalRequest>;
40
+ private readonly log;
41
+ private readonly api;
42
+ private readonly inboundDenialReason;
43
+ constructor(deps: ApprovalFlowDeps);
44
+ settleApproval(requestId: string, state: Exclude<TelegramApprovalRequestState, 'pending'>, result: TelegramApprovalResult): boolean;
45
+ dispatchCallback(cq: TelegramApiCallbackQuery): Promise<void>;
46
+ private answerCallback;
47
+ awaitApproval(input: TelegramApprovalRequestInput): Promise<TelegramApprovalResult>;
48
+ bindApprovalPrompt(requestId: string, promptMessageId: number): boolean;
49
+ cancelApproval(requestId: string, fromUser?: string): boolean;
50
+ cancelAll(fromUser: string): void;
51
+ }
52
+ export {};
53
+ //# sourceMappingURL=approval-flow.d.ts.map
@@ -0,0 +1,56 @@
1
+ import type { Logger } from '@wrongstack/core/types';
2
+ import type { OffsetStore } from './offset-store.js';
3
+ import type { PollLock } from './poll-lock.js';
4
+ /**
5
+ * Bot-surface type contracts (card 7A-4).
6
+ *
7
+ * Type-only leaf extracted from bot.ts so the composer imports its public
8
+ * shapes without dragging implementation modules into the type graph.
9
+ */
10
+ export interface TelegramBotResponse<T> {
11
+ ok: true;
12
+ result: T;
13
+ }
14
+ /** Incoming message shape emitted as a custom event. */
15
+ export interface TelegramIncomingMessage {
16
+ messageId: number;
17
+ chatId: number;
18
+ chatType: string;
19
+ userId?: number | undefined;
20
+ userName?: string | undefined;
21
+ text: string;
22
+ timestamp: number;
23
+ }
24
+ export interface TelegramBotOptions {
25
+ token: string;
26
+ pollIntervalSec: number;
27
+ allowedUsers: Set<string>;
28
+ allowedChats: Set<string>;
29
+ /** Max messages to buffer for the agent to read. Default: 50. */
30
+ bufferSize: number;
31
+ log: Logger;
32
+ /**
33
+ * Resolved on every outbound send so live `parseMode` config changes
34
+ * (via `api.onConfigChange`) take effect without restarting the plugin.
35
+ * Empty string or `undefined` → plain text. See `TelegramPluginConfig.parseMode`.
36
+ */
37
+ getParseMode?: () => '' | 'HTML' | 'MarkdownV2' | undefined;
38
+ /** Called for each incoming message that passes allowlist checks. */
39
+ onMessage(msg: TelegramIncomingMessage): void;
40
+ /**
41
+ * Optional typed offset store. When provided, the polling offset is persisted
42
+ * atomically on every successful poll and restored on startup, preventing
43
+ * message replay after crashes or restarts.
44
+ */
45
+ offsetStore?: OffsetStore | undefined;
46
+ /**
47
+ * Optional cross-process single-poller lock. Telegram allows one
48
+ * `getUpdates` consumer per token; when another wstack instance holds the
49
+ * lock, this bot stands by (no polling) and takes over once the holder
50
+ * stops or its heartbeat goes stale.
51
+ */
52
+ lock?: PollLock | undefined;
53
+ /** How often a standby instance retries acquiring the lock. Default: 15s. */
54
+ standbyRetryMs?: number | undefined;
55
+ }
56
+ //# sourceMappingURL=bot-types.d.ts.map
package/dist/bot.d.ts CHANGED
@@ -1,182 +1,41 @@
1
- import type { Logger } from '@wrongstack/core/types';
2
- import { type TelegramApiMessage } from './api-client.js';
3
- import type { OffsetStore } from './offset-store.js';
4
- import type { PollLock } from './poll-lock.js';
5
- export interface TelegramBotResponse<T> {
6
- ok: true;
7
- result: T;
8
- }
9
- export interface TelegramIncomingMessage {
10
- messageId: number;
11
- chatId: number;
12
- chatType: string;
13
- userId?: number | undefined;
14
- userName?: string | undefined;
15
- text: string;
16
- timestamp: number;
17
- }
18
- export interface TelegramApprovalResult {
19
- approved: boolean;
20
- fromUser: string;
21
- fromUserId?: number | undefined;
22
- }
23
- export interface TelegramApprovalRequestInput {
24
- requestId: string;
25
- sessionId: string;
26
- expectedChatId: string | number;
27
- expectedUserIds: readonly (string | number)[];
28
- /** Group/supergroup callbacks are rejected unless this was explicitly enabled. */
29
- allowGroup: boolean;
30
- expiresAt: number;
31
- /** Cancels the request when its owning tool execution is aborted. */
32
- signal?: AbortSignal | undefined;
33
- }
34
- export interface TelegramBotOptions {
35
- token: string;
36
- pollIntervalSec: number;
37
- allowedUsers: Set<string>;
38
- allowedChats: Set<string>;
39
- /** Max messages to buffer for the agent to read. Default: 50. */
40
- bufferSize: number;
41
- log: Logger;
42
- /**
43
- * Resolved on every outbound send so live `parseMode` config changes
44
- * (via `api.onConfigChange`) take effect without restarting the plugin.
45
- * Empty string or `undefined` → plain text. See `TelegramPluginConfig.parseMode`.
46
- */
47
- getParseMode?: () => '' | 'HTML' | 'MarkdownV2' | undefined;
48
- /** Called for each incoming message that passes allowlist checks. */
49
- onMessage(msg: TelegramIncomingMessage): void;
50
- /**
51
- * Optional typed offset store. When provided, the polling offset is persisted
52
- * atomically on every successful poll and restored on startup, preventing
53
- * message replay after crashes or restarts.
54
- */
55
- offsetStore?: OffsetStore | undefined;
56
- /**
57
- * Optional cross-process single-poller lock. Telegram allows one
58
- * `getUpdates` consumer per token; when another wstack instance holds the
59
- * lock, this bot stands by (no polling) and takes over once the holder
60
- * stops or its heartbeat goes stale.
61
- */
62
- lock?: PollLock | undefined;
63
- /** How often a standby instance retries acquiring the lock. Default: 15s. */
64
- standbyRetryMs?: number | undefined;
65
- }
1
+ import { ApprovalFlow, type TelegramApprovalRequestInput, type TelegramApprovalResult } from './approval-flow.js';
2
+ import type { TelegramBotOptions } from './bot-types.js';
3
+ import { TelegramInbox } from './inbox.js';
4
+ import { Poller } from './poller.js';
5
+ import { TelegramOutbox } from './outbox.js';
6
+ export type { TelegramBotOptions, TelegramBotResponse, TelegramIncomingMessage } from './bot-types.js';
7
+ export type { TelegramApprovalRequestInput, TelegramApprovalResult } from './approval-flow.js';
8
+ export { escapeHtml, truncateForTelegram } from './text-format.js';
66
9
  export declare class TelegramBot {
67
10
  private readonly api;
68
- private readonly pollIntervalMs;
69
- private readonly allowedUsers;
70
- private readonly allowedChats;
71
11
  private readonly log;
72
- private readonly onMessage;
73
- private readonly controller;
74
- private pollTimer;
75
- private pollActive;
76
- private offset;
77
- /**
78
- * Consecutive HTTP 409 ("another getUpdates in flight") responses. Two
79
- * wstack instances polling the same bot token used to fight at full poll
80
- * speed forever, erroring on every cycle. After CONFLICT_BACKOFF_AFTER
81
- * consecutive conflicts this instance backs off to a slow poll and warns
82
- * once; any successful poll resets to the normal cadence.
83
- */
84
- private conflictStreak;
85
- private static readonly CONFLICT_BACKOFF_AFTER;
86
- private static readonly CONFLICT_POLL_MS;
87
- private _startedAt;
88
- /** Typed offset store for atomic polling-cursor persistence. */
89
- private readonly offsetStore?;
90
- /** Single-poller election across wstack instances sharing this token. */
91
12
  private readonly lock?;
92
- private readonly standbyRetryMs;
93
- private readonly getParseMode?;
94
- private standbyTimer;
95
- private standbyAnnounced;
96
- private readonly bufferMax;
97
- private readonly buffer;
98
- private readonly callbackWaiters;
13
+ readonly approvals: ApprovalFlow;
14
+ readonly poller: Poller;
15
+ readonly inbox: TelegramInbox;
16
+ private readonly outbox;
99
17
  constructor(opts: TelegramBotOptions);
100
- /** Start polling for updates. Idempotent. */
101
18
  start(): void;
102
- /** Stop polling and cancel all in-flight requests. */
103
19
  stop(): void;
104
- /** True when the bot is started but waiting for the poll lock. */
105
20
  get standby(): boolean;
106
- /**
107
- * Acquire the poll lock (when configured) and start the poll loop, or
108
- * stand by and retry until the current holder releases it.
109
- */
110
- private acquireAndPoll;
111
- /** The lock was stolen while we held it — pause polling and stand by. */
112
- private handleLockLost;
113
21
  get startedAt(): number | null;
114
22
  get running(): boolean;
115
- /** Return buffered messages, newest first. Optionally filter by chat. */
116
23
  getMessages(opts?: {
117
24
  chatId?: string | number | undefined;
118
25
  limit?: number | undefined;
119
- }): TelegramIncomingMessage[];
120
- /** Drop messages older than or equal to the given message ID from the buffer (optionally scoped to a specific chat). */
26
+ }): ReturnType<TelegramInbox['getMessages']>;
121
27
  acknowledge(lastMessageId: number, chatId?: string | number | undefined): number;
122
28
  get bufferCount(): number;
123
- sendMessage(chatId: string | number, text: string, signal?: AbortSignal | undefined): Promise<TelegramBotResponse<TelegramApiMessage>>;
124
- /**
125
- * Send a message that has up to one row of inline buttons (Telegram's
126
- * `inline_keyboard`). Used by `telegram_approve` to present a
127
- * yes/no prompt. The keyboard payload is opaque to the bot — callers
128
- * pass already-encoded `callback_data` strings (≤ 64 bytes each).
129
- */
29
+ private processMessage;
30
+ sendMessage(chatId: string | number, text: string, signal?: AbortSignal | undefined): ReturnType<TelegramOutbox['sendMessage']>;
130
31
  sendMessageWithKeyboard(chatId: string | number, text: string, buttons: Array<{
131
32
  text: string;
132
33
  callback_data: string;
133
- }>, signal?: AbortSignal | undefined): Promise<TelegramBotResponse<TelegramApiMessage>>;
134
- health(signal?: AbortSignal | undefined): Promise<{
135
- ok: boolean;
136
- username?: string | undefined;
137
- error?: string | undefined;
138
- }>;
139
- private schedulePoll;
140
- private poll;
141
- /**
142
- * Apply the inbound identity policy to every update type. A non-empty set is
143
- * a mandatory constraint: missing identity fails closed instead of bypassing
144
- * the allowlist. An empty set leaves that identity dimension unrestricted.
145
- */
146
- private inboundDenialReason;
147
- private processMessage;
148
- /**
149
- * Resolve a pending approval request exactly once and record its terminal
150
- * state before removing it from the live registry.
151
- */
152
- private settleApproval;
153
- private dispatchCallback;
154
- /**
155
- * POST /answerCallbackQuery for a callback. Best-effort: failures are
156
- * logged at debug and swallowed — the caller's resolve() must not depend
157
- * on the ack reaching Telegram (the user may get a "loading" spinner if
158
- * it fails, but the agent's approval flow continues normally).
159
- */
160
- private answerCallback;
161
- /**
162
- * Register one approval request before its prompt is sent. The returned
163
- * promise owns the request's only timer and resolves on one terminal event.
164
- */
34
+ }>, signal?: AbortSignal | undefined): ReturnType<TelegramOutbox['sendMessageWithKeyboard']>;
35
+ health(signal?: AbortSignal | undefined): Promise<Awaited<ReturnType<TelegramOutbox['health']>>>;
36
+ get callbackWaiters(): ApprovalFlow['callbackWaiters'];
165
37
  awaitApproval(input: TelegramApprovalRequestInput): Promise<TelegramApprovalResult>;
166
- /**
167
- * Attach the Bot API response's prompt message ID to an existing request.
168
- * Any callback that arrived during the send is replayed against the fully
169
- * bound identity without allocating a second waiter or timer.
170
- */
171
38
  bindApprovalPrompt(requestId: string, promptMessageId: number): boolean;
172
- /** Cancel a request that cannot reach a valid terminal callback. */
173
39
  cancelApproval(requestId: string, fromUser?: string): boolean;
174
- private loadOffset;
175
- private saveOffset;
176
40
  }
177
- export declare function truncateForTelegram(text: string, maxLen?: number): string;
178
- /**
179
- * Escape HTML special chars for Telegram's HTML parse mode.
180
- */
181
- export declare function escapeHtml(text: string): string;
182
41
  //# sourceMappingURL=bot.d.ts.map
@@ -0,0 +1,48 @@
1
+ import type { Logger } from '@wrongstack/core/types';
2
+ import type { TelegramApiMessage } from './api-client.js';
3
+ import type { TelegramIncomingMessage } from './bot-types.js';
4
+ /**
5
+ * Inbound message handling (card 7A-4): the bounded incoming-message buffer,
6
+ * the allowlist identity policy, and update→incoming mapping. Moved verbatim
7
+ * from bot.ts; the bot keeps thin delegates so its public surface and the
8
+ * white-box suites (which reach this owner through `bot.inbox`) keep working.
9
+ */
10
+ export interface TelegramInboxDeps {
11
+ log: Logger;
12
+ /** Max messages to buffer for the agent to read. */
13
+ bufferMax: number;
14
+ allowedUsers: Set<string>;
15
+ allowedChats: Set<string>;
16
+ /** Called for each incoming message that passes allowlist checks. */
17
+ onMessage(msg: TelegramIncomingMessage): void;
18
+ /**
19
+ * Best-effort denial notice sender — wired to the bot's retry-wrapped
20
+ * `sendMessage`. Failures are swallowed by `processMessage` so an
21
+ * unauthorized user cannot spam unhandled rejections into the poll loop.
22
+ */
23
+ sendNotice(chatId: string | number, text: string): Promise<unknown>;
24
+ }
25
+ export declare class TelegramInbox {
26
+ private readonly deps;
27
+ readonly buffer: TelegramIncomingMessage[];
28
+ constructor(deps: TelegramInboxDeps);
29
+ /** Return buffered messages, newest first. Optionally filter by chat. */
30
+ getMessages(opts?: {
31
+ chatId?: string | number | undefined;
32
+ limit?: number | undefined;
33
+ }): TelegramIncomingMessage[];
34
+ /** Drop messages older than or equal to the given message ID from the buffer (optionally scoped to a specific chat). */
35
+ acknowledge(lastMessageId: number, chatId?: string | number | undefined): number;
36
+ get bufferCount(): number;
37
+ /**
38
+ * Apply the inbound identity policy to every update type. A non-empty set is
39
+ * a mandatory constraint: missing identity fails closed instead of bypassing
40
+ * the allowlist. An empty set leaves that identity dimension unrestricted.
41
+ * Public because ApprovalFlow consumes the same gate via the bot's wiring.
42
+ */
43
+ denialReason(userId: string | undefined, chatId: string | undefined): 'user' | 'chat' | undefined;
44
+ processMessage(msg: TelegramApiMessage & {
45
+ text: string;
46
+ }): void;
47
+ }
48
+ //# sourceMappingURL=inbox.d.ts.map