@wrongstack/telegram 0.309.0 → 0.310.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/dist/approval-flow.d.ts +53 -0
- package/dist/bot-types.d.ts +56 -0
- package/dist/bot.d.ts +18 -159
- package/dist/inbox.d.ts +48 -0
- package/dist/index.js +425 -419
- package/dist/outbox.d.ts +41 -0
- package/dist/poller.d.ts +48 -0
- package/dist/text-format.d.ts +25 -0
- package/package.json +3 -3
|
@@ -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
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
private
|
|
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
|
-
}):
|
|
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
|
-
|
|
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):
|
|
134
|
-
health(signal?: AbortSignal | undefined): Promise<
|
|
135
|
-
|
|
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
|
package/dist/inbox.d.ts
ADDED
|
@@ -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
|