@zhin.js/adapter-telegram 5.0.2 → 5.0.3
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/CHANGELOG.md +41 -0
- package/README.md +61 -144
- package/adapters/telegram.ts +27 -0
- package/agent/tools/create_invite.ts +2 -2
- package/agent/tools/list_admins.ts +2 -2
- package/agent/tools/member_count.ts +2 -2
- package/agent/tools/pin_message.ts +2 -2
- package/agent/tools/react.ts +2 -2
- package/agent/tools/send_poll.ts +2 -2
- package/agent/tools/send_sticker.ts +2 -2
- package/agent/tools/set_description.ts +2 -2
- package/agent/tools/set_permissions.ts +2 -2
- package/agent/tools/unpin_message.ts +2 -2
- package/lib/endpoint.d.ts +65 -0
- package/lib/endpoint.js +332 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/platform-permit.d.ts +18 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
- package/lib/polling.d.ts +9 -0
- package/lib/polling.js +52 -0
- package/lib/protocol.d.ts +250 -0
- package/lib/protocol.js +324 -0
- package/lib/telegram-agent-deps.d.ts +28 -0
- package/lib/telegram-agent-deps.js +30 -0
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +45 -0
- package/package.json +43 -40
- package/plugin.ts +13 -0
- package/schema.json +39 -0
- package/src/endpoint.ts +339 -1103
- package/src/index.ts +40 -186
- package/src/platform-permit.ts +1 -1
- package/src/polling.ts +71 -0
- package/src/protocol.ts +573 -0
- package/src/telegram-agent-deps.ts +52 -11
- package/src/webhook.ts +67 -0
- package/client/Dashboard.tsx +0 -295
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -32
- package/lib/agent/tools/create_invite.js +0 -20
- package/lib/agent/tools/create_invite.js.map +0 -1
- package/lib/agent/tools/list_admins.js +0 -26
- package/lib/agent/tools/list_admins.js.map +0 -1
- package/lib/agent/tools/member_count.js +0 -18
- package/lib/agent/tools/member_count.js.map +0 -1
- package/lib/agent/tools/pin_message.js +0 -21
- package/lib/agent/tools/pin_message.js.map +0 -1
- package/lib/agent/tools/react.js +0 -20
- package/lib/agent/tools/react.js.map +0 -1
- package/lib/agent/tools/send_poll.js +0 -32
- package/lib/agent/tools/send_poll.js.map +0 -1
- package/lib/agent/tools/send_sticker.js +0 -19
- package/lib/agent/tools/send_sticker.js.map +0 -1
- package/lib/agent/tools/set_description.js +0 -19
- package/lib/agent/tools/set_description.js.map +0 -1
- package/lib/agent/tools/set_permissions.js +0 -34
- package/lib/agent/tools/set_permissions.js.map +0 -1
- package/lib/agent/tools/unpin_message.js +0 -21
- package/lib/agent/tools/unpin_message.js.map +0 -1
- package/lib/src/adapter.js +0 -58
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -1046
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/index.js +0 -215
- package/lib/src/index.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/telegram-agent-deps.js +0 -10
- package/lib/src/telegram-agent-deps.js.map +0 -1
- package/lib/src/types.js +0 -2
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -66
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -32
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Bot API protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IncomingMessage } from 'node:http';
|
|
7
|
+
|
|
8
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
9
|
+
export interface TelegramAdapterConfig {
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly token?: string;
|
|
12
|
+
/** Default true. `false` selects webhook mode (requires httpHostToken). */
|
|
13
|
+
readonly polling?: boolean;
|
|
14
|
+
readonly webhook?: {
|
|
15
|
+
readonly domain?: string;
|
|
16
|
+
readonly path?: string;
|
|
17
|
+
readonly secretToken?: string;
|
|
18
|
+
};
|
|
19
|
+
readonly allowedUpdates?: readonly string[];
|
|
20
|
+
readonly apiBaseUrl?: string;
|
|
21
|
+
/** Transitional: legacy root `endpoints[]` with `context: telegram`. */
|
|
22
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedTelegramConfig> & {
|
|
23
|
+
readonly context?: string;
|
|
24
|
+
readonly polling?: boolean;
|
|
25
|
+
readonly webhook?: TelegramAdapterConfig['webhook'];
|
|
26
|
+
readonly allowedUpdates?: readonly string[];
|
|
27
|
+
readonly apiBaseUrl?: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvedTelegramConfig {
|
|
32
|
+
readonly context: 'telegram';
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly token: string;
|
|
35
|
+
readonly mode: 'polling' | 'webhook';
|
|
36
|
+
readonly allowedUpdates: readonly string[];
|
|
37
|
+
readonly apiBaseUrl: string;
|
|
38
|
+
readonly webhook?: {
|
|
39
|
+
readonly domain: string;
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly secretToken?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TelegramUser {
|
|
46
|
+
readonly id: number;
|
|
47
|
+
readonly is_bot?: boolean;
|
|
48
|
+
readonly first_name?: string;
|
|
49
|
+
readonly last_name?: string;
|
|
50
|
+
readonly username?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TelegramChat {
|
|
54
|
+
readonly id: number;
|
|
55
|
+
readonly type: 'private' | 'group' | 'supergroup' | 'channel';
|
|
56
|
+
readonly title?: string;
|
|
57
|
+
readonly username?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TelegramMessageEntity {
|
|
61
|
+
readonly type: string;
|
|
62
|
+
readonly offset: number;
|
|
63
|
+
readonly length: number;
|
|
64
|
+
readonly url?: string;
|
|
65
|
+
readonly user?: TelegramUser;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TelegramPhotoSize {
|
|
69
|
+
readonly file_id: string;
|
|
70
|
+
readonly file_unique_id?: string;
|
|
71
|
+
readonly width?: number;
|
|
72
|
+
readonly height?: number;
|
|
73
|
+
readonly file_size?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TelegramMessage {
|
|
77
|
+
readonly message_id: number;
|
|
78
|
+
readonly date: number;
|
|
79
|
+
readonly chat: TelegramChat;
|
|
80
|
+
readonly from?: TelegramUser;
|
|
81
|
+
readonly text?: string;
|
|
82
|
+
readonly caption?: string;
|
|
83
|
+
readonly entities?: readonly TelegramMessageEntity[];
|
|
84
|
+
readonly reply_to_message?: TelegramMessage;
|
|
85
|
+
readonly photo?: readonly TelegramPhotoSize[];
|
|
86
|
+
readonly video?: {
|
|
87
|
+
readonly file_id: string;
|
|
88
|
+
readonly file_unique_id?: string;
|
|
89
|
+
readonly width?: number;
|
|
90
|
+
readonly height?: number;
|
|
91
|
+
readonly duration?: number;
|
|
92
|
+
readonly file_size?: number;
|
|
93
|
+
};
|
|
94
|
+
readonly audio?: {
|
|
95
|
+
readonly file_id: string;
|
|
96
|
+
readonly file_unique_id?: string;
|
|
97
|
+
readonly duration?: number;
|
|
98
|
+
readonly performer?: string;
|
|
99
|
+
readonly title?: string;
|
|
100
|
+
readonly file_size?: number;
|
|
101
|
+
};
|
|
102
|
+
readonly voice?: {
|
|
103
|
+
readonly file_id: string;
|
|
104
|
+
readonly file_unique_id?: string;
|
|
105
|
+
readonly duration?: number;
|
|
106
|
+
readonly file_size?: number;
|
|
107
|
+
};
|
|
108
|
+
readonly document?: {
|
|
109
|
+
readonly file_id: string;
|
|
110
|
+
readonly file_unique_id?: string;
|
|
111
|
+
readonly file_name?: string;
|
|
112
|
+
readonly mime_type?: string;
|
|
113
|
+
readonly file_size?: number;
|
|
114
|
+
};
|
|
115
|
+
readonly sticker?: {
|
|
116
|
+
readonly file_id: string;
|
|
117
|
+
readonly file_unique_id?: string;
|
|
118
|
+
readonly width?: number;
|
|
119
|
+
readonly height?: number;
|
|
120
|
+
readonly is_animated?: boolean;
|
|
121
|
+
readonly is_video?: boolean;
|
|
122
|
+
readonly emoji?: string;
|
|
123
|
+
};
|
|
124
|
+
readonly location?: {
|
|
125
|
+
readonly longitude: number;
|
|
126
|
+
readonly latitude: number;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface TelegramCallbackQuery {
|
|
131
|
+
readonly id: string;
|
|
132
|
+
readonly from: TelegramUser;
|
|
133
|
+
readonly data?: string;
|
|
134
|
+
readonly message?: TelegramMessage;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface TelegramUpdate {
|
|
138
|
+
readonly update_id: number;
|
|
139
|
+
readonly message?: TelegramMessage;
|
|
140
|
+
readonly edited_message?: TelegramMessage;
|
|
141
|
+
readonly callback_query?: TelegramCallbackQuery;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TelegramChatMember {
|
|
145
|
+
readonly status: string;
|
|
146
|
+
readonly user: TelegramUser;
|
|
147
|
+
readonly can_restrict_members?: boolean;
|
|
148
|
+
readonly can_pin_messages?: boolean;
|
|
149
|
+
readonly can_delete_messages?: boolean;
|
|
150
|
+
readonly can_manage_chat?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface TelegramWireSegment {
|
|
154
|
+
readonly type: string;
|
|
155
|
+
readonly data?: Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface TelegramInlineButton {
|
|
159
|
+
readonly text: string;
|
|
160
|
+
readonly callback_data: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type TelegramOutboundAction =
|
|
164
|
+
| {
|
|
165
|
+
readonly method: 'sendMessage';
|
|
166
|
+
readonly params: {
|
|
167
|
+
readonly chat_id: number | string;
|
|
168
|
+
readonly text: string;
|
|
169
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
170
|
+
readonly reply_markup?: { readonly inline_keyboard: TelegramInlineButton[][] };
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
| {
|
|
174
|
+
readonly method: 'sendPhoto';
|
|
175
|
+
readonly params: {
|
|
176
|
+
readonly chat_id: number | string;
|
|
177
|
+
readonly photo: string;
|
|
178
|
+
readonly caption?: string;
|
|
179
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
| {
|
|
183
|
+
readonly method: 'sendVideo';
|
|
184
|
+
readonly params: {
|
|
185
|
+
readonly chat_id: number | string;
|
|
186
|
+
readonly video: string;
|
|
187
|
+
readonly caption?: string;
|
|
188
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
| {
|
|
192
|
+
readonly method: 'sendAudio';
|
|
193
|
+
readonly params: {
|
|
194
|
+
readonly chat_id: number | string;
|
|
195
|
+
readonly audio: string;
|
|
196
|
+
readonly caption?: string;
|
|
197
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
| {
|
|
201
|
+
readonly method: 'sendVoice';
|
|
202
|
+
readonly params: {
|
|
203
|
+
readonly chat_id: number | string;
|
|
204
|
+
readonly voice: string;
|
|
205
|
+
readonly caption?: string;
|
|
206
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
| {
|
|
210
|
+
readonly method: 'sendDocument';
|
|
211
|
+
readonly params: {
|
|
212
|
+
readonly chat_id: number | string;
|
|
213
|
+
readonly document: string;
|
|
214
|
+
readonly caption?: string;
|
|
215
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
readonly method: 'sendSticker';
|
|
220
|
+
readonly params: {
|
|
221
|
+
readonly chat_id: number | string;
|
|
222
|
+
readonly sticker: string;
|
|
223
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
| {
|
|
227
|
+
readonly method: 'sendLocation';
|
|
228
|
+
readonly params: {
|
|
229
|
+
readonly chat_id: number | string;
|
|
230
|
+
readonly latitude: number;
|
|
231
|
+
readonly longitude: number;
|
|
232
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export function resolveTelegramConfig(config: TelegramAdapterConfig = {}): ResolvedTelegramConfig {
|
|
237
|
+
const entry = config.endpoints?.find((item) => item.context === 'telegram');
|
|
238
|
+
const token = config.token
|
|
239
|
+
?? entry?.token
|
|
240
|
+
?? process.env.TELEGRAM_TOKEN
|
|
241
|
+
?? process.env.TELEGRAM_BOT_TOKEN;
|
|
242
|
+
if (!token) {
|
|
243
|
+
throw new TypeError(
|
|
244
|
+
'Telegram adapter requires token (plugins.<key>.token or endpoints with context: telegram)',
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
248
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
249
|
+
|| process.env.TELEGRAM_BOT_NAME
|
|
250
|
+
|| 'telegram-bot';
|
|
251
|
+
const polling = config.polling ?? entry?.polling;
|
|
252
|
+
const webhookSource = config.webhook ?? entry?.webhook;
|
|
253
|
+
// Match legacy: polling defaults true; webhook only when polling === false.
|
|
254
|
+
const mode: 'polling' | 'webhook' = polling === false ? 'webhook' : 'polling';
|
|
255
|
+
const apiBaseUrl = (
|
|
256
|
+
config.apiBaseUrl
|
|
257
|
+
?? entry?.apiBaseUrl
|
|
258
|
+
?? 'https://api.telegram.org'
|
|
259
|
+
).replace(/\/$/, '');
|
|
260
|
+
const allowedUpdates = config.allowedUpdates
|
|
261
|
+
?? entry?.allowedUpdates
|
|
262
|
+
?? ['message', 'callback_query'];
|
|
263
|
+
const webhook = mode === 'webhook'
|
|
264
|
+
? {
|
|
265
|
+
domain: webhookSource?.domain ?? '',
|
|
266
|
+
path: normalizeWebhookPath(webhookSource?.path ?? '/telegram/webhook'),
|
|
267
|
+
secretToken: webhookSource?.secretToken
|
|
268
|
+
?? process.env.TELEGRAM_WEBHOOK_SECRET
|
|
269
|
+
?? undefined,
|
|
270
|
+
}
|
|
271
|
+
: undefined;
|
|
272
|
+
return {
|
|
273
|
+
context: 'telegram',
|
|
274
|
+
name,
|
|
275
|
+
token,
|
|
276
|
+
mode,
|
|
277
|
+
allowedUpdates: [...allowedUpdates],
|
|
278
|
+
apiBaseUrl,
|
|
279
|
+
webhook,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function normalizeWebhookPath(path: string): string {
|
|
284
|
+
const trimmed = path.trim() || '/telegram/webhook';
|
|
285
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function buildWebhookUrl(webhook: NonNullable<ResolvedTelegramConfig['webhook']>): string {
|
|
289
|
+
const domain = webhook.domain.replace(/\/$/, '');
|
|
290
|
+
if (!domain) {
|
|
291
|
+
throw new TypeError('Telegram webhook mode requires webhook.domain');
|
|
292
|
+
}
|
|
293
|
+
return `${domain}${webhook.path}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function readTextBody(
|
|
297
|
+
request: IncomingMessage,
|
|
298
|
+
options: { readonly limit?: number } = {},
|
|
299
|
+
): Promise<string> {
|
|
300
|
+
const limit = options.limit ?? 1_048_576;
|
|
301
|
+
const chunks: Buffer[] = [];
|
|
302
|
+
let size = 0;
|
|
303
|
+
for await (const chunk of request) {
|
|
304
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
305
|
+
size += buffer.length;
|
|
306
|
+
if (size > limit) {
|
|
307
|
+
request.destroy();
|
|
308
|
+
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
309
|
+
}
|
|
310
|
+
chunks.push(buffer);
|
|
311
|
+
}
|
|
312
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function botApiUrl(config: Pick<ResolvedTelegramConfig, 'apiBaseUrl' | 'token'>, method: string): string {
|
|
316
|
+
return `${config.apiBaseUrl}/bot${config.token}/${method}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function resolveChannel(msg: Pick<TelegramMessage, 'chat'>): {
|
|
320
|
+
readonly channelType: 'private' | 'group';
|
|
321
|
+
readonly channelId: string;
|
|
322
|
+
} {
|
|
323
|
+
return {
|
|
324
|
+
channelType: msg.chat.type === 'private' ? 'private' : 'group',
|
|
325
|
+
channelId: String(msg.chat.id),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function senderDisplayName(user?: TelegramUser): string {
|
|
330
|
+
if (!user) return 'Unknown';
|
|
331
|
+
return user.username || user.first_name || String(user.id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Build inbound text for MessageGateway.receive. */
|
|
335
|
+
export function formatInboundContent(msg: TelegramMessage): string {
|
|
336
|
+
if (msg.text) return msg.text;
|
|
337
|
+
if (msg.caption) return msg.caption;
|
|
338
|
+
if (msg.photo?.length) return '[image]';
|
|
339
|
+
if (msg.video) return '[video]';
|
|
340
|
+
if (msg.audio) return '[audio]';
|
|
341
|
+
if (msg.voice) return '[voice]';
|
|
342
|
+
if (msg.document) {
|
|
343
|
+
return msg.document.file_name ? `[file: ${msg.document.file_name}]` : '[file]';
|
|
344
|
+
}
|
|
345
|
+
if (msg.sticker) {
|
|
346
|
+
return msg.sticker.emoji ? `[sticker: ${msg.sticker.emoji}]` : '[sticker]';
|
|
347
|
+
}
|
|
348
|
+
if (msg.location) {
|
|
349
|
+
return `[location: ${msg.location.latitude},${msg.location.longitude}]`;
|
|
350
|
+
}
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function formatCallbackContent(query: TelegramCallbackQuery): string {
|
|
355
|
+
return query.data ? `[action: ${query.data}]` : '[action]';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Wire-encode an already-rendered outbound payload into Telegram Bot API actions.
|
|
360
|
+
* Segment canonicalization is intentionally not done here.
|
|
361
|
+
*/
|
|
362
|
+
export function formatOutboundActions(
|
|
363
|
+
target: string | number,
|
|
364
|
+
payload: unknown,
|
|
365
|
+
): TelegramOutboundAction[] {
|
|
366
|
+
const chatId = typeof target === 'number' ? target : (/^-?\d+$/.test(target) ? Number(target) : target);
|
|
367
|
+
if (typeof payload === 'string') {
|
|
368
|
+
const text = payload.trim();
|
|
369
|
+
if (!text) throw new Error('No Telegram content to send');
|
|
370
|
+
return [{ method: 'sendMessage', params: { chat_id: chatId, text } }];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const items: Array<string | TelegramWireSegment> = Array.isArray(payload)
|
|
374
|
+
? payload as Array<string | TelegramWireSegment>
|
|
375
|
+
: payload && typeof payload === 'object' && 'type' in (payload as object)
|
|
376
|
+
? [payload as TelegramWireSegment]
|
|
377
|
+
: [];
|
|
378
|
+
|
|
379
|
+
if (items.length === 0) {
|
|
380
|
+
const text = payload == null
|
|
381
|
+
? ''
|
|
382
|
+
: typeof payload === 'object'
|
|
383
|
+
? JSON.stringify(payload)
|
|
384
|
+
: String(payload);
|
|
385
|
+
if (!text.trim()) throw new Error('No Telegram content to send');
|
|
386
|
+
return [{ method: 'sendMessage', params: { chat_id: chatId, text: text.trim() } }];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let textContent = '';
|
|
390
|
+
let replyTo: number | undefined;
|
|
391
|
+
let keyboard: TelegramInlineButton[][] | undefined;
|
|
392
|
+
const actions: TelegramOutboundAction[] = [];
|
|
393
|
+
|
|
394
|
+
const replyParams = (): { reply_parameters?: { message_id: number } } => (
|
|
395
|
+
replyTo != null ? { reply_parameters: { message_id: replyTo } } : {}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const mediaSource = (data: Record<string, unknown>): string | undefined => {
|
|
399
|
+
if (typeof data.file_id === 'string' && data.file_id) return data.file_id;
|
|
400
|
+
if (typeof data.url === 'string' && data.url) return data.url;
|
|
401
|
+
if (typeof data.file === 'string' && data.file) return data.file;
|
|
402
|
+
return undefined;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
for (const item of items) {
|
|
406
|
+
if (typeof item === 'string') {
|
|
407
|
+
textContent += item;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const data = item.data ?? {};
|
|
411
|
+
switch (item.type) {
|
|
412
|
+
case 'text':
|
|
413
|
+
textContent += String(data.text ?? data.content ?? '');
|
|
414
|
+
break;
|
|
415
|
+
case 'at':
|
|
416
|
+
if (data.id) textContent += `@${String(data.name || data.id)}`;
|
|
417
|
+
break;
|
|
418
|
+
case 'reply': {
|
|
419
|
+
const id = Number(data.id ?? data.message_id);
|
|
420
|
+
if (Number.isFinite(id)) replyTo = id;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case 'keyboard': {
|
|
424
|
+
const rows = Array.isArray(data.rows) ? data.rows : [];
|
|
425
|
+
keyboard = rows.map((row) => {
|
|
426
|
+
const buttons = Array.isArray(row) ? row : [];
|
|
427
|
+
return buttons.map((btn) => {
|
|
428
|
+
const record = btn && typeof btn === 'object'
|
|
429
|
+
? btn as { label?: string; text?: string; payload?: string; callback_data?: string }
|
|
430
|
+
: {};
|
|
431
|
+
return {
|
|
432
|
+
text: String(record.label ?? record.text ?? ''),
|
|
433
|
+
callback_data: String(record.payload ?? record.callback_data ?? '').slice(0, 64),
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case 'image': {
|
|
440
|
+
const photo = mediaSource(data);
|
|
441
|
+
if (photo) {
|
|
442
|
+
actions.push({
|
|
443
|
+
method: 'sendPhoto',
|
|
444
|
+
params: {
|
|
445
|
+
chat_id: chatId,
|
|
446
|
+
photo,
|
|
447
|
+
caption: textContent.trim() || undefined,
|
|
448
|
+
...replyParams(),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
textContent = '';
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'video': {
|
|
456
|
+
const video = mediaSource(data);
|
|
457
|
+
if (video) {
|
|
458
|
+
actions.push({
|
|
459
|
+
method: 'sendVideo',
|
|
460
|
+
params: {
|
|
461
|
+
chat_id: chatId,
|
|
462
|
+
video,
|
|
463
|
+
caption: textContent.trim() || undefined,
|
|
464
|
+
...replyParams(),
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
textContent = '';
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case 'audio': {
|
|
472
|
+
const audio = mediaSource(data);
|
|
473
|
+
if (audio) {
|
|
474
|
+
actions.push({
|
|
475
|
+
method: 'sendAudio',
|
|
476
|
+
params: {
|
|
477
|
+
chat_id: chatId,
|
|
478
|
+
audio,
|
|
479
|
+
caption: textContent.trim() || undefined,
|
|
480
|
+
...replyParams(),
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
textContent = '';
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'voice': {
|
|
488
|
+
const voice = mediaSource(data);
|
|
489
|
+
if (voice) {
|
|
490
|
+
actions.push({
|
|
491
|
+
method: 'sendVoice',
|
|
492
|
+
params: {
|
|
493
|
+
chat_id: chatId,
|
|
494
|
+
voice,
|
|
495
|
+
caption: textContent.trim() || undefined,
|
|
496
|
+
...replyParams(),
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
textContent = '';
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
case 'file': {
|
|
504
|
+
const document = mediaSource(data);
|
|
505
|
+
if (document) {
|
|
506
|
+
actions.push({
|
|
507
|
+
method: 'sendDocument',
|
|
508
|
+
params: {
|
|
509
|
+
chat_id: chatId,
|
|
510
|
+
document,
|
|
511
|
+
caption: textContent.trim() || undefined,
|
|
512
|
+
...replyParams(),
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
textContent = '';
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'sticker': {
|
|
520
|
+
const sticker = typeof data.file_id === 'string' ? data.file_id : mediaSource(data);
|
|
521
|
+
if (sticker) {
|
|
522
|
+
actions.push({
|
|
523
|
+
method: 'sendSticker',
|
|
524
|
+
params: { chat_id: chatId, sticker, ...replyParams() },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'location': {
|
|
530
|
+
actions.push({
|
|
531
|
+
method: 'sendLocation',
|
|
532
|
+
params: {
|
|
533
|
+
chat_id: chatId,
|
|
534
|
+
latitude: Number(data.latitude ?? 0),
|
|
535
|
+
longitude: Number(data.longitude ?? 0),
|
|
536
|
+
...replyParams(),
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
default:
|
|
542
|
+
textContent += String(data.text ?? `[${item.type}]`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (actions.length === 0) {
|
|
547
|
+
const text = textContent.trim() || (keyboard ? ' ' : '');
|
|
548
|
+
if (!text && !keyboard) throw new Error('No Telegram content to send');
|
|
549
|
+
return [{
|
|
550
|
+
method: 'sendMessage',
|
|
551
|
+
params: {
|
|
552
|
+
chat_id: chatId,
|
|
553
|
+
text: text || ' ',
|
|
554
|
+
...replyParams(),
|
|
555
|
+
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
556
|
+
},
|
|
557
|
+
}];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (textContent.trim() || keyboard) {
|
|
561
|
+
actions.unshift({
|
|
562
|
+
method: 'sendMessage',
|
|
563
|
+
params: {
|
|
564
|
+
chat_id: chatId,
|
|
565
|
+
text: textContent.trim() || ' ',
|
|
566
|
+
...replyParams(),
|
|
567
|
+
...(keyboard ? { reply_markup: { inline_keyboard: keyboard } } : {}),
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return actions;
|
|
573
|
+
}
|
|
@@ -1,22 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Agent tool deps for telegram.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
5
|
+
|
|
6
|
+
import type { TelegramChatMember } from './protocol.js';
|
|
7
|
+
|
|
8
|
+
export interface TelegramAgentEndpoint {
|
|
9
|
+
pinMessage(chatId: number, messageId: number): Promise<boolean>;
|
|
10
|
+
unpinMessage(chatId: number, messageId?: number): Promise<boolean>;
|
|
11
|
+
setChatDescription(chatId: number, description: string): Promise<boolean>;
|
|
12
|
+
setMessageReaction(chatId: number, messageId: number, reaction: string): Promise<boolean>;
|
|
13
|
+
getChatMemberCount(chatId: number): Promise<number>;
|
|
14
|
+
getChatAdmins(chatId: number): Promise<TelegramChatMember[]>;
|
|
15
|
+
sendStickerMessage(chatId: number, sticker: string): Promise<{ message_id: number }>;
|
|
16
|
+
setChatPermissionsAll(
|
|
17
|
+
chatId: number,
|
|
18
|
+
permissions: Record<string, boolean | undefined>,
|
|
19
|
+
): Promise<boolean>;
|
|
20
|
+
createInviteLink(chatId: number): Promise<string>;
|
|
21
|
+
sendPoll(
|
|
22
|
+
chatId: number,
|
|
23
|
+
question: string,
|
|
24
|
+
options: string[],
|
|
25
|
+
isAnonymous?: boolean,
|
|
26
|
+
allowsMultipleAnswers?: boolean,
|
|
27
|
+
): Promise<{ message_id: number }>;
|
|
28
|
+
}
|
|
7
29
|
|
|
8
30
|
export interface TelegramAgentDeps {
|
|
9
|
-
getEndpoint: (endpointId: string) =>
|
|
10
|
-
getAdapter: () => TelegramAdapter;
|
|
31
|
+
getEndpoint: (endpointId: string) => TelegramAgentEndpoint;
|
|
11
32
|
}
|
|
12
33
|
|
|
13
|
-
|
|
34
|
+
const endpoints = new Map<string, TelegramAgentEndpoint>();
|
|
35
|
+
let override: TelegramAgentDeps | null = null;
|
|
36
|
+
|
|
37
|
+
export function registerTelegramAgentEndpoint(
|
|
38
|
+
endpointId: string,
|
|
39
|
+
endpoint: TelegramAgentEndpoint,
|
|
40
|
+
): () => void {
|
|
41
|
+
endpoints.set(endpointId, endpoint);
|
|
42
|
+
return () => {
|
|
43
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
44
|
+
endpoints.delete(endpointId);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
50
|
+
export function setTelegramAgentDeps(deps: TelegramAgentDeps | null): void {
|
|
51
|
+
override = deps;
|
|
17
52
|
}
|
|
18
53
|
|
|
19
54
|
export function getTelegramAgentDeps(): TelegramAgentDeps {
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
55
|
+
if (override) return override;
|
|
56
|
+
return {
|
|
57
|
+
getEndpoint(endpointId: string): TelegramAgentEndpoint {
|
|
58
|
+
const registered = endpoints.get(endpointId);
|
|
59
|
+
if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
60
|
+
return registered;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
22
63
|
}
|
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram webhook HTTP: secret token → parse → handle update.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { getLogger } from '@zhin.js/logger';
|
|
7
|
+
import { readTextBody, type ResolvedTelegramConfig, type TelegramUpdate } from './protocol.js';
|
|
8
|
+
|
|
9
|
+
const logger = getLogger('telegram');
|
|
10
|
+
|
|
11
|
+
export interface TelegramWebhookHandler {
|
|
12
|
+
readonly config: ResolvedTelegramConfig;
|
|
13
|
+
readonly isOpen: boolean;
|
|
14
|
+
handleUpdate(update: TelegramUpdate): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerTelegramWebhookRoutes(
|
|
18
|
+
http: HttpHost,
|
|
19
|
+
handler: TelegramWebhookHandler,
|
|
20
|
+
): HttpRouteRegistration[] {
|
|
21
|
+
const path = handler.config.webhook!.path;
|
|
22
|
+
return [
|
|
23
|
+
http.route('POST', path, async (request, response) => {
|
|
24
|
+
await handleTelegramWebhookRequest(request, response, handler);
|
|
25
|
+
}, { summary: 'Telegram Bot API webhook', tags: ['telegram'] }),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleTelegramWebhookRequest(
|
|
30
|
+
request: IncomingMessage,
|
|
31
|
+
response: ServerResponse,
|
|
32
|
+
handler: TelegramWebhookHandler,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
const secret = handler.config.webhook?.secretToken;
|
|
36
|
+
if (secret) {
|
|
37
|
+
const header = request.headers['x-telegram-bot-api-secret-token'];
|
|
38
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
39
|
+
if (token !== secret) {
|
|
40
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
41
|
+
response.end(JSON.stringify({ ok: false, description: 'Invalid secret token' }));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rawBody = await readTextBody(request);
|
|
47
|
+
let update: TelegramUpdate;
|
|
48
|
+
try {
|
|
49
|
+
update = JSON.parse(rawBody) as TelegramUpdate;
|
|
50
|
+
} catch {
|
|
51
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
52
|
+
response.end(JSON.stringify({ ok: true }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (handler.isOpen) {
|
|
57
|
+
handler.handleUpdate(update);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
response.end(JSON.stringify({ ok: true }));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('Telegram webhook error:', error);
|
|
64
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
65
|
+
response.end(JSON.stringify({ ok: true }));
|
|
66
|
+
}
|
|
67
|
+
}
|