@zhin.js/adapter-telegram 1.1.18 → 1.1.20

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/src/index.ts CHANGED
@@ -29,7 +29,8 @@ export {
29
29
  type TelegramWireSegment,
30
30
  } from './protocol.js';
31
31
 
32
- export { telegramClient, type TelegramClientEventMap } from './client.js';
32
+ export { type TelegramClientEventMap } from './client.js';
33
+ export { TelegramAdapter } from './adapter.js';
33
34
 
34
35
  export {
35
36
  checkTelegramPlatformPermit,
@@ -37,3 +38,5 @@ export {
37
38
  platformPermit,
38
39
  telegramGroupPermitResolver,
39
40
  } from './platform-permit.js';
41
+
42
+ export type { TelegramPollRequest } from './telegram-client.js';
@@ -0,0 +1,250 @@
1
+ import { botApiUrl, type ResolvedTelegramConfig, type TelegramChatMember } from './protocol.js';
2
+ import { TelegramApiError } from './api-error.js';
3
+
4
+ const DEFAULT_API_TIMEOUT_MS = 30_000;
5
+
6
+ export type TelegramFetch = (
7
+ url: string,
8
+ init?: {
9
+ readonly method?: string;
10
+ readonly headers?: Record<string, string>;
11
+ readonly body?: string | FormData;
12
+ readonly signal?: AbortSignal;
13
+ },
14
+ ) => Promise<{
15
+ readonly ok: boolean;
16
+ readonly status: number;
17
+ text(): Promise<string>;
18
+ json(): Promise<unknown>;
19
+ arrayBuffer?(): Promise<ArrayBuffer>;
20
+ readonly headers?: { get(name: string): string | null };
21
+ }>;
22
+
23
+ export interface TelegramClientApi {
24
+ callApi<T = unknown>(method: string, params?: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
25
+ callApiForm<T = unknown>(method: string, form: FormData, signal?: AbortSignal): Promise<T>;
26
+ pinMessage(chatId: number, messageId: number): Promise<boolean>;
27
+ unpinMessage(chatId: number, messageId?: number): Promise<boolean>;
28
+ setChatDescription(chatId: number, description: string): Promise<boolean>;
29
+ setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean>;
30
+ getChatMemberCount(chatId: number): Promise<number>;
31
+ getChatAdmins(chatId: number): Promise<TelegramChatMember[]>;
32
+ sendStickerMessage(chatId: number, sticker: string): Promise<{ message_id: number }>;
33
+ setChatPermissionsAll(chatId: number, permissions: Record<string, boolean | undefined>): Promise<boolean>;
34
+ createInviteLink(chatId: number): Promise<string>;
35
+ sendPoll(request: TelegramPollRequest): Promise<{ message_id: number }>;
36
+ }
37
+
38
+ export interface TelegramPollRequest {
39
+ readonly chatId: number;
40
+ readonly question: string;
41
+ readonly options: string[];
42
+ readonly isAnonymous?: boolean;
43
+ readonly allowsMultipleAnswers?: boolean;
44
+ }
45
+
46
+ interface TelegramApiOk<T> {
47
+ readonly ok: true;
48
+ readonly result: T;
49
+ }
50
+
51
+ interface TelegramApiErr {
52
+ readonly ok: false;
53
+ readonly description?: string;
54
+ readonly error_code?: number;
55
+ readonly parameters?: { readonly retry_after?: number };
56
+ }
57
+
58
+ interface TelegramHttpRequest {
59
+ readonly method: string;
60
+ readonly init: NonNullable<Parameters<TelegramFetch>[1]>;
61
+ readonly signal?: AbortSignal;
62
+ readonly timeoutMs?: number;
63
+ }
64
+ interface TelegramDeadlineOperation<T> {
65
+ readonly method: string;
66
+ readonly run: (signal: AbortSignal) => Promise<T>;
67
+ readonly signal?: AbortSignal;
68
+ readonly timeoutMs?: number;
69
+ }
70
+
71
+ /** Owns Telegram Bot API transport, cancellation, deadlines and native API operations. */
72
+ export class TelegramClient implements TelegramClientApi {
73
+ readonly #options: Readonly<{ config: ResolvedTelegramConfig; requestTimeoutMs?: number }>;
74
+ protected readonly fetchTransport: TelegramFetch;
75
+ protected requestSignal?: AbortSignal;
76
+ #apiAbort = new AbortController();
77
+ constructor(options: Readonly<{ config: ResolvedTelegramConfig; fetch?: TelegramFetch; requestTimeoutMs?: number }>) {
78
+ if (options.requestTimeoutMs !== undefined && (!Number.isFinite(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)) {
79
+ throw new TypeError('Telegram requestTimeoutMs must be a positive finite number');
80
+ }
81
+ this.#options = options;
82
+ this.fetchTransport = options.fetch ?? globalThis.fetch;
83
+ }
84
+ protected restartRequests(): void { if (this.#apiAbort.signal.aborted) this.#apiAbort = new AbortController(); }
85
+ protected cancelRequests(): void { this.#apiAbort.abort(); }
86
+ async callApi<T = unknown>(
87
+ method: string,
88
+ params: Record<string, unknown> = {},
89
+ signal?: AbortSignal,
90
+ ): Promise<T> {
91
+ return this.#requestApi<T>({ method, init: {
92
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params),
93
+ }, signal, timeoutMs: method === 'getUpdates'
94
+ ? Math.max(this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS, (Number(params.timeout) || 0) * 1_000 + 15_000)
95
+ : undefined });
96
+ }
97
+
98
+ /** multipart/form-data 变体(attach:// 媒体上传;Content-Type 边界由 FormData 自带)。 */
99
+ async callApiForm<T = unknown>(
100
+ method: string,
101
+ form: FormData,
102
+ signal?: AbortSignal,
103
+ ): Promise<T> {
104
+ return this.#requestApi<T>({ method, init: { method: 'POST', body: form }, signal });
105
+ }
106
+
107
+ async #requestApi<T>({ method, init, signal, timeoutMs }: TelegramHttpRequest): Promise<T> {
108
+ return this.withRequestDeadline({ method, run: async requestSignal => {
109
+ const response = await this.fetchTransport(botApiUrl(this.#options.config, method), { ...init, signal: requestSignal });
110
+ return this.#parseApiResponse<T>(method, response);
111
+ }, signal, timeoutMs });
112
+ }
113
+
114
+ protected async withRequestDeadline<T>({ method, run, signal,
115
+ timeoutMs = this.#options.requestTimeoutMs ?? DEFAULT_API_TIMEOUT_MS,
116
+ }: TelegramDeadlineOperation<T>): Promise<T> {
117
+ const abort = new AbortController();
118
+ let issued = false;
119
+ const sources = [...new Set([signal, this.requestSignal, this.#apiAbort.signal].filter((item): item is AbortSignal => !!item))];
120
+ let rejectAbort!: (error: unknown) => void;
121
+ const cancelled = new Promise<never>((_resolve, reject) => { rejectAbort = reject; });
122
+ const onCancel = () => {
123
+ const error = new TelegramApiError('cancelled', `Telegram API ${method} cancelled`, {
124
+ disposition: issued ? 'unknown' : 'not_sent',
125
+ });
126
+ abort.abort(error);
127
+ rejectAbort(error);
128
+ };
129
+ const timer = setTimeout(() => {
130
+ const error = new TelegramApiError('timeout', `Telegram API ${method} timed out`);
131
+ abort.abort(error);
132
+ rejectAbort(error);
133
+ }, timeoutMs);
134
+ for (const source of sources) source.addEventListener('abort', onCancel, { once: true });
135
+ try {
136
+ if (sources.some((source) => source.aborted)) onCancel();
137
+ const request = Promise.resolve().then(async () => {
138
+ abort.signal.throwIfAborted();
139
+ issued = true;
140
+ return run(abort.signal);
141
+ });
142
+ return await Promise.race([request, cancelled]);
143
+ } catch (error) {
144
+ if (abort.signal.aborted) throw abort.signal.reason;
145
+ if (error instanceof TelegramApiError) throw error;
146
+ // Network exceptions can contain credential-bearing URLs. Keep those out of errors/logs.
147
+ const detail = error instanceof Error
148
+ ? error.message.replaceAll(this.#options.config.token, '[redacted]') : 'transport failure';
149
+ throw new TelegramApiError('network', `Telegram API ${method} network request failed: ${detail}`);
150
+ } finally {
151
+ clearTimeout(timer);
152
+ for (const source of sources) source.removeEventListener('abort', onCancel);
153
+ }
154
+ }
155
+
156
+ async #parseApiResponse<T>(
157
+ method: string,
158
+ response: { readonly status: number; text(): Promise<string> },
159
+ ): Promise<T> {
160
+ const text = await response.text();
161
+ let body: TelegramApiOk<T> | TelegramApiErr;
162
+ try {
163
+ body = JSON.parse(text) as TelegramApiOk<T> | TelegramApiErr;
164
+ } catch {
165
+ throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid JSON (${response.status})`, { status: response.status });
166
+ }
167
+ if (!body || typeof body !== 'object' || typeof body.ok !== 'boolean') {
168
+ throw new TelegramApiError('invalid_response', `Telegram API ${method} invalid response envelope`, { status: response.status });
169
+ }
170
+ if (!body.ok || response.status < 200 || response.status >= 300) {
171
+ const status = !body.ok ? body.error_code ?? response.status : response.status;
172
+ const kind = status === 401 || status === 403 ? 'authentication'
173
+ : status === 429 ? 'rate_limit' : response.status >= 400 ? 'http' : 'api';
174
+ const description = !body.ok ? body.description?.replaceAll(this.#options.config.token, '[redacted]') : undefined;
175
+ throw new TelegramApiError(kind,
176
+ `Telegram API ${method} failed (${status}): ${description ?? 'request rejected'}`,
177
+ { status, disposition: !body.ok ? 'rejected' : 'unknown', ...(!body.ok && body.parameters?.retry_after != null
178
+ ? { retryAfterSeconds: body.parameters.retry_after } : {}) },
179
+ );
180
+ }
181
+ if (!('result' in body)) throw new TelegramApiError('invalid_response', `Telegram API ${method} missing result`);
182
+ return body.result;
183
+ }
184
+
185
+ // ── Agent tool surface ──────────────────────────────────────────────
186
+
187
+ async pinMessage(chatId: number, messageId: number): Promise<boolean> {
188
+ await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
189
+ return true;
190
+ }
191
+
192
+ async unpinMessage(chatId: number, messageId?: number): Promise<boolean> {
193
+ if (messageId != null) {
194
+ await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
195
+ } else {
196
+ await this.callApi('unpinAllChatMessages', { chat_id: chatId });
197
+ }
198
+ return true;
199
+ }
200
+
201
+ async setChatDescription(chatId: number, description: string): Promise<boolean> {
202
+ await this.callApi('setChatDescription', { chat_id: chatId, description });
203
+ return true;
204
+ }
205
+
206
+ async setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean> {
207
+ await this.callApi('setMessageReaction', {
208
+ chat_id: chatId,
209
+ message_id: messageId,
210
+ reaction: [{ type: 'emoji', emoji: reaction }],
211
+ });
212
+ return true;
213
+ }
214
+
215
+ async getChatMemberCount(chatId: number): Promise<number> {
216
+ return this.callApi<number>('getChatMemberCount', { chat_id: chatId });
217
+ }
218
+
219
+ async getChatAdmins(chatId: number): Promise<TelegramChatMember[]> {
220
+ return this.callApi<TelegramChatMember[]>('getChatAdministrators', { chat_id: chatId });
221
+ }
222
+
223
+ async sendStickerMessage(chatId: number, sticker: string): Promise<{ message_id: number }> {
224
+ return this.callApi<{ message_id: number }>('sendSticker', { chat_id: chatId, sticker });
225
+ }
226
+
227
+ async setChatPermissionsAll(
228
+ chatId: number,
229
+ permissions: Record<string, boolean | undefined>,
230
+ ): Promise<boolean> {
231
+ await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
232
+ return true;
233
+ }
234
+
235
+ async createInviteLink(chatId: number): Promise<string> {
236
+ const link = await this.callApi<{ invite_link: string }>('createChatInviteLink', { chat_id: chatId });
237
+ return link.invite_link;
238
+ }
239
+
240
+ async sendPoll({ chatId, question, options, isAnonymous = true,
241
+ allowsMultipleAnswers = false }: TelegramPollRequest): Promise<{ message_id: number }> {
242
+ return this.callApi<{ message_id: number }>('sendPoll', {
243
+ chat_id: chatId,
244
+ question,
245
+ options,
246
+ is_anonymous: isAnonymous,
247
+ allows_multiple_answers: allowsMultipleAnswers,
248
+ });
249
+ }
250
+ }
@@ -1 +0,0 @@
1
- export declare const telegramRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -1,6 +0,0 @@
1
- /**
2
- * Telegram 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `telegram endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
- export const telegramRuntimeStateToken = defineEndpointRuntimeStateToken('telegram');
@@ -1,7 +0,0 @@
1
- /**
2
- * Telegram 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `telegram endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
-
7
- export const telegramRuntimeStateToken = defineEndpointRuntimeStateToken('telegram');