@zhin.js/adapter-telegram 1.1.25 → 1.1.27
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 +185 -0
- package/LICENSE +21 -0
- package/PERMITS.md +17 -13
- package/README.md +9 -5
- package/adapters/telegram/index.js +3 -0
- package/adapters/telegram/index.ts +3 -0
- package/agents/telegram/conversation.js +35 -0
- package/agents/telegram/conversation.ts +38 -0
- package/agents/telegram/messages.js +8 -0
- package/agents/telegram/messages.ts +8 -0
- package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.js +6 -8
- package/agents/telegram/skills/telegram-chat-admin/tools/create_invite/index.ts +7 -9
- package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.js +5 -7
- package/agents/telegram/skills/telegram-chat-admin/tools/list_admins/index.ts +6 -8
- package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.js +5 -7
- package/agents/telegram/skills/telegram-chat-admin/tools/member_count/index.ts +6 -8
- package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.js +5 -6
- package/agents/telegram/skills/telegram-chat-admin/tools/pin_message/index.ts +6 -7
- package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.js +5 -4
- package/agents/telegram/skills/telegram-chat-admin/tools/set_description/index.ts +6 -5
- package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.js +5 -6
- package/agents/telegram/skills/telegram-chat-admin/tools/unpin_message/index.ts +6 -7
- package/agents/telegram/skills/telegram-group-permissions/tools/set_permissions/index.js +5 -6
- package/agents/telegram/skills/telegram-group-permissions/tools/set_permissions/index.ts +6 -7
- package/agents/telegram/skills/telegram-messages/tools/react/index.js +5 -6
- package/agents/telegram/skills/telegram-messages/tools/react/index.ts +6 -7
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.js +8 -7
- package/agents/telegram/skills/telegram-messages/tools/send_poll/index.ts +9 -8
- package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.js +7 -7
- package/agents/telegram/skills/telegram-messages/tools/send_sticker/index.ts +8 -8
- package/lib/console-spec.js +85 -8
- package/lib/endpoint.d.ts +2 -1
- package/lib/endpoint.js +75 -21
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/platform-permit.d.ts +16 -16
- package/lib/platform-permit.js +26 -45
- package/lib/protocol.d.ts +25 -4
- package/lib/protocol.js +31 -16
- package/lib/webhook.d.ts +1 -2
- package/lib/webhook.js +9 -20
- package/package.json +23 -27
- package/plugin.js +0 -6
- package/schema.json +0 -56
- package/src/console-spec.ts +85 -8
- package/src/endpoint.ts +73 -25
- package/src/index.ts +1 -3
- package/src/platform-permit.ts +27 -56
- package/src/protocol.ts +52 -20
- package/src/webhook.ts +11 -23
package/lib/endpoint.js
CHANGED
|
@@ -9,19 +9,22 @@ import { EndpointDeliveryError, } from '@zhin.js/im-contract';
|
|
|
9
9
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
10
10
|
import { runTelegramPollLoop } from './polling.js';
|
|
11
11
|
export { TelegramApiError } from './api-error.js';
|
|
12
|
-
import {
|
|
12
|
+
import { telegramChatMemberStanding } from './platform-permit.js';
|
|
13
13
|
import { buildWebhookUrl, formatCallbackContent, formatCallbackSegments, formatInboundContent, formatInboundSegments, formatOutboundPlan, resolveTelegramChannelType, senderDisplayName, telegramInboundConversation, } from './protocol.js';
|
|
14
14
|
import { registerTelegramWebhookRoutes } from './webhook.js';
|
|
15
15
|
import { createTelegramSelfProfile } from './self-profile.js';
|
|
16
16
|
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
17
17
|
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
18
|
+
/** A failed `getChatMember` is not retried for every message of a busy chat (unknown standing meanwhile). */
|
|
19
|
+
const CHAT_MEMBER_FAILURE_TTL_MS = 10_000;
|
|
18
20
|
const ADMITTED_UPDATE_LIMIT = 2_048;
|
|
19
21
|
const INFLIGHT_UPDATE_LIMIT = 256;
|
|
20
22
|
export { TelegramClient } from './telegram-client.js';
|
|
21
23
|
/**
|
|
22
24
|
* Telegram Bot API 无列表类接口(无 getMyChats/getChatMembers),
|
|
23
25
|
* 仅 getChat/getChatMember 按已知 id 单查,不构成列表能力;
|
|
24
|
-
* 因此本 endpoint
|
|
26
|
+
* 因此本 endpoint 的 EndpointManagement 只有 `inspectSelfStanding`(ADR 0056 §1:getChatMember bot),
|
|
27
|
+
* Console 社交面 RPC 对该平台保持未接线。
|
|
25
28
|
*/
|
|
26
29
|
export class TelegramEndpoint extends TelegramClient {
|
|
27
30
|
[endpointSpec];
|
|
@@ -44,6 +47,24 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
44
47
|
ttlMs: CHAT_MEMBER_CACHE_TTL_MS,
|
|
45
48
|
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
46
49
|
});
|
|
50
|
+
/** Recently failed lookups (negative cache): the standing stays unknown until it expires. */
|
|
51
|
+
#chatMemberFailures = new TtlLookupCache({
|
|
52
|
+
ttlMs: CHAT_MEMBER_FAILURE_TTL_MS,
|
|
53
|
+
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
54
|
+
});
|
|
55
|
+
/** Concurrent messages from one sender share one `getChatMember` call. */
|
|
56
|
+
#chatMemberInflight = new Map();
|
|
57
|
+
#management = Object.freeze({
|
|
58
|
+
// ADR 0056 §1: the Bot's own standing (`bot:*` permits); the framework caches per conversation.
|
|
59
|
+
inspectSelfStanding: async ({ conversation, signal }) => {
|
|
60
|
+
if (conversation.kind === 'private' || !this.#botUserId)
|
|
61
|
+
return undefined;
|
|
62
|
+
const member = await this.callApi('getChatMember', {
|
|
63
|
+
chat_id: Number(conversation.id), user_id: this.#botUserId,
|
|
64
|
+
}, signal);
|
|
65
|
+
return telegramChatMemberStanding(member);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
47
68
|
#control = Object.freeze({
|
|
48
69
|
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
49
70
|
});
|
|
@@ -60,7 +81,7 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
60
81
|
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
61
82
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
62
83
|
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
63
|
-
control: this.#control, content: this.#content,
|
|
84
|
+
control: this.#control, content: this.#content, management: this.#management,
|
|
64
85
|
streaming: options.config.streaming !== false ? createTelegramStreaming(() => this, options.config.streamingIntervalMs) : undefined,
|
|
65
86
|
profile: createTelegramSelfProfile({ client: this,
|
|
66
87
|
current: () => this.#botName === undefined ? {} : { nickname: this.#botName },
|
|
@@ -340,7 +361,7 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
340
361
|
sender: {
|
|
341
362
|
id: String(msg.from?.id ?? ''),
|
|
342
363
|
name: senderDisplayName(msg.from) || undefined,
|
|
343
|
-
...(permit
|
|
364
|
+
...(permit ? { platform: permit } : {}),
|
|
344
365
|
},
|
|
345
366
|
endpointId: this.#options.config.id,
|
|
346
367
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -350,8 +371,6 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
350
371
|
chatType: msg.chat.type,
|
|
351
372
|
userId: msg.from?.id,
|
|
352
373
|
date: msg.date,
|
|
353
|
-
...(permit?.role ? { senderRole: permit.role } : {}),
|
|
354
|
-
...(permit?.permissions.length ? { senderPermissions: [...permit.permissions] } : {}),
|
|
355
374
|
}),
|
|
356
375
|
});
|
|
357
376
|
}
|
|
@@ -379,7 +398,10 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
379
398
|
}
|
|
380
399
|
return false;
|
|
381
400
|
}
|
|
382
|
-
/**
|
|
401
|
+
/**
|
|
402
|
+
* 群消息 sender role 解析:getChatMember + 60s 缓存;并发查询合并为一次调用,失败负缓存 10s,
|
|
403
|
+
* `chat_member` / `my_chat_member` 更新时以新状态覆盖缓存。
|
|
404
|
+
*/
|
|
383
405
|
async #resolveGroupSenderPermit(msg, signal) {
|
|
384
406
|
if (msg.chat.type === 'private' || !msg.from?.id)
|
|
385
407
|
return undefined;
|
|
@@ -389,22 +411,52 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
389
411
|
const cached = this.#chatMemberCache.get(key);
|
|
390
412
|
if (cached)
|
|
391
413
|
return cached;
|
|
392
|
-
|
|
393
|
-
const member = await this.callApi('getChatMember', {
|
|
394
|
-
chat_id: chatId,
|
|
395
|
-
user_id: userId,
|
|
396
|
-
}, signal);
|
|
397
|
-
if (signal.aborted)
|
|
398
|
-
return undefined;
|
|
399
|
-
const normalized = normalizeTelegramChatMember(member);
|
|
400
|
-
const entry = { ...normalized };
|
|
401
|
-
this.#chatMemberCache.set(key, entry);
|
|
402
|
-
return entry;
|
|
403
|
-
}
|
|
404
|
-
catch {
|
|
405
|
-
// 保守拒绝:无角色快照
|
|
414
|
+
if (this.#chatMemberFailures.get(key))
|
|
406
415
|
return undefined;
|
|
416
|
+
let task = this.#chatMemberInflight.get(key)?.promise;
|
|
417
|
+
if (!task) {
|
|
418
|
+
const lookup = {};
|
|
419
|
+
task = (async () => {
|
|
420
|
+
await undefined; // registered in #chatMemberInflight before the body runs
|
|
421
|
+
try {
|
|
422
|
+
const member = await this.callApi('getChatMember', {
|
|
423
|
+
chat_id: chatId,
|
|
424
|
+
user_id: userId,
|
|
425
|
+
}, signal);
|
|
426
|
+
if (signal.aborted)
|
|
427
|
+
return undefined;
|
|
428
|
+
const entry = telegramChatMemberStanding(member);
|
|
429
|
+
// Invalidated (chat_member update) while in flight: do not store the older answer.
|
|
430
|
+
if (this.#chatMemberInflight.get(key) === lookup)
|
|
431
|
+
this.#chatMemberCache.set(key, entry);
|
|
432
|
+
return entry;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// 保守拒绝:无角色快照;取消不算失败
|
|
436
|
+
if (!signal.aborted && this.#chatMemberInflight.get(key) === lookup)
|
|
437
|
+
this.#chatMemberFailures.set(key, true);
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
if (this.#chatMemberInflight.get(key) === lookup)
|
|
442
|
+
this.#chatMemberInflight.delete(key);
|
|
443
|
+
}
|
|
444
|
+
})();
|
|
445
|
+
lookup.promise = task;
|
|
446
|
+
this.#chatMemberInflight.set(key, lookup);
|
|
407
447
|
}
|
|
448
|
+
const entry = await task;
|
|
449
|
+
return signal.aborted ? undefined : entry;
|
|
450
|
+
}
|
|
451
|
+
/** `chat_member` / `my_chat_member`: the update carries the new status, so it replaces the cached one. */
|
|
452
|
+
#applyChatMemberUpdate(update) {
|
|
453
|
+
const userId = update?.new_chat_member?.user?.id;
|
|
454
|
+
if (!update?.chat || userId == null)
|
|
455
|
+
return;
|
|
456
|
+
const key = `${Number(update.chat.id)}:${userId}`;
|
|
457
|
+
this.#chatMemberInflight.delete(key);
|
|
458
|
+
this.#chatMemberFailures.delete(key);
|
|
459
|
+
this.#chatMemberCache.set(key, telegramChatMemberStanding(update.new_chat_member));
|
|
408
460
|
}
|
|
409
461
|
/** Test / internal: admit a callback query when open. */
|
|
410
462
|
admitCallback(query) {
|
|
@@ -482,6 +534,8 @@ export class TelegramEndpoint extends TelegramClient {
|
|
|
482
534
|
await this[endpointSpec].events.platform(eventName, update);
|
|
483
535
|
}
|
|
484
536
|
else {
|
|
537
|
+
this.#applyChatMemberUpdate(update.chat_member);
|
|
538
|
+
this.#applyChatMemberUpdate(update.my_chat_member);
|
|
485
539
|
await this[endpointSpec].events.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
486
540
|
return;
|
|
487
541
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { TelegramClient, TelegramEndpoint, type TelegramClientApi, type Telegram
|
|
|
2
2
|
export { botApiUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, formatOutboundPlan, normalizeWebhookPath, resolveTelegramConfig, senderDisplayName, type ResolvedTelegramConfig, type TelegramEndpointConfig, type TelegramCallbackQuery, type TelegramChat, type TelegramChatMember, type TelegramMessage, type TelegramOutboundAction, type TelegramOutboundPlan, type TelegramOutboundUpload, type TelegramUpdate, type TelegramUser, type TelegramWireSegment, } from './protocol.js';
|
|
3
3
|
export { type TelegramClientEventMap } from './client.js';
|
|
4
4
|
export { TelegramAdapter } from './adapter.js';
|
|
5
|
-
export {
|
|
5
|
+
export { telegramChatMemberStanding, platformPermit, } from './platform-permit.js';
|
|
6
6
|
export type { TelegramPollRequest } from './telegram-client.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { TelegramClient, TelegramEndpoint, } from './endpoint.js';
|
|
2
2
|
export { botApiUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, formatOutboundPlan, normalizeWebhookPath, resolveTelegramConfig, senderDisplayName, } from './protocol.js';
|
|
3
3
|
export { TelegramAdapter } from './adapter.js';
|
|
4
|
-
export {
|
|
4
|
+
export { telegramChatMemberStanding, platformPermit, } from './platform-permit.js';
|
package/lib/platform-permit.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram platform
|
|
2
|
+
* Telegram platform standing — ChatMember 状态 + 管理员权限(ADR 0056 §1)
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
* 细粒度能力放在 `permissions`。
|
|
4
|
+
* `standing` 使用跨平台规范名 owner / admin / member / guest;细粒度管理员权限放在 `permissions`。
|
|
6
5
|
*/
|
|
7
|
-
import type {
|
|
6
|
+
import type { PlatformStanding } from '@zhin.js/im-contract';
|
|
8
7
|
export declare function platformPermit(perm: string): string;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
/**
|
|
9
|
+
* ADR 0056 §1:`getChatMember` 结果 → `sender.platform`。
|
|
10
|
+
* creator → `owner`(拥有全部管理员权限);administrator → `admin`,其 `can_*` 权限去掉前缀进
|
|
11
|
+
* `permissions`(如 `restrict_members`、`pin_messages`);member / 仍在群内的 restricted → `member`;
|
|
12
|
+
* left / kicked → `guest`。Telegram 没有身份组,`roles` 不上报。
|
|
13
|
+
*
|
|
14
|
+
* 只有管理员权限作为平台权限上报:restricted 成员的 `can_*`(如 `can_pin_messages`)是对其个人的
|
|
15
|
+
* 发言许可,普通成员同样可能按群默认权限拥有,两者都不上报,`platform-permission:*` 只由管理员满足。
|
|
16
|
+
*/
|
|
17
|
+
export declare function telegramChatMemberStanding(member: {
|
|
18
|
+
readonly status?: string;
|
|
19
|
+
readonly is_member?: boolean;
|
|
20
|
+
}): PlatformStanding;
|
package/lib/platform-permit.js
CHANGED
|
@@ -1,51 +1,32 @@
|
|
|
1
|
-
const ADAPTER = 'telegram';
|
|
2
1
|
export function platformPermit(perm) {
|
|
3
|
-
return `platform
|
|
2
|
+
return `platform-permission:${perm}`;
|
|
4
3
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
/** Administrator rights a chat creator implicitly holds (Bot API `ChatAdministratorRights`). */
|
|
5
|
+
const TELEGRAM_ADMIN_RIGHTS = [
|
|
6
|
+
'manage_chat', 'delete_messages', 'manage_video_chats', 'restrict_members', 'promote_members', 'change_info',
|
|
7
|
+
'invite_users', 'post_stories', 'edit_stories', 'delete_stories', 'post_messages', 'edit_messages', 'pin_messages',
|
|
8
|
+
'manage_topics',
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* ADR 0056 §1:`getChatMember` 结果 → `sender.platform`。
|
|
12
|
+
* creator → `owner`(拥有全部管理员权限);administrator → `admin`,其 `can_*` 权限去掉前缀进
|
|
13
|
+
* `permissions`(如 `restrict_members`、`pin_messages`);member / 仍在群内的 restricted → `member`;
|
|
14
|
+
* left / kicked → `guest`。Telegram 没有身份组,`roles` 不上报。
|
|
15
|
+
*
|
|
16
|
+
* 只有管理员权限作为平台权限上报:restricted 成员的 `can_*`(如 `can_pin_messages`)是对其个人的
|
|
17
|
+
* 发言许可,普通成员同样可能按群默认权限拥有,两者都不上报,`platform-permission:*` 只由管理员满足。
|
|
18
|
+
*/
|
|
19
|
+
export function telegramChatMemberStanding(member) {
|
|
14
20
|
const status = member.status;
|
|
15
|
-
if (status === 'creator')
|
|
16
|
-
|
|
17
|
-
return { role: 'owner', permissions };
|
|
18
|
-
}
|
|
21
|
+
if (status === 'creator')
|
|
22
|
+
return { standing: 'owner', permissions: [...TELEGRAM_ADMIN_RIGHTS] };
|
|
19
23
|
if (status === 'administrator') {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
permissions.push('pin_messages');
|
|
25
|
-
if (member.can_delete_messages)
|
|
26
|
-
permissions.push('delete_messages');
|
|
27
|
-
if (member.can_manage_chat)
|
|
28
|
-
permissions.push('manage_chat');
|
|
29
|
-
return { role: 'admin', permissions };
|
|
30
|
-
}
|
|
31
|
-
return { role: 'member', permissions };
|
|
32
|
-
}
|
|
33
|
-
export function checkTelegramPlatformPermit(perm, subject) {
|
|
34
|
-
const role = subject.sender?.role?.[0];
|
|
35
|
-
const permissions = subject.sender?.permissions ?? [];
|
|
36
|
-
const has = (t) => permissions.includes(t);
|
|
37
|
-
switch (perm) {
|
|
38
|
-
case 'chat_creator':
|
|
39
|
-
return role === 'owner' || role === 'creator' || has('chat_creator');
|
|
40
|
-
case 'chat_administrator':
|
|
41
|
-
return role === 'owner' || role === 'admin' || role === 'creator' || role === 'administrator' || has('chat_administrator');
|
|
42
|
-
case 'restrict_members':
|
|
43
|
-
return has('restrict_members') || role === 'owner' || role === 'creator';
|
|
44
|
-
case 'pin_messages':
|
|
45
|
-
return has('pin_messages') || role === 'owner' || role === 'creator';
|
|
46
|
-
case 'manage_chat':
|
|
47
|
-
return has('manage_chat') || role === 'owner' || role === 'creator';
|
|
48
|
-
default:
|
|
49
|
-
return false;
|
|
24
|
+
const granted = Object.entries(member)
|
|
25
|
+
.filter(([key, value]) => key.startsWith('can_') && value === true)
|
|
26
|
+
.map(([key]) => key.slice(4));
|
|
27
|
+
return { standing: 'admin', ...(granted.length ? { permissions: granted } : {}) };
|
|
50
28
|
}
|
|
29
|
+
if (status === 'left' || status === 'kicked' || (status === 'restricted' && member.is_member === false))
|
|
30
|
+
return { standing: 'guest' };
|
|
31
|
+
return { standing: 'member' };
|
|
51
32
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
3
3
|
* Telegram Bot API protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
*/
|
|
6
|
-
import type { IncomingMessage } from 'node:http';
|
|
7
6
|
import { type ConversationKind, type ConversationRef } from '@zhin.js/im-contract';
|
|
8
7
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
8
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
@@ -130,6 +129,17 @@ export interface TelegramUpdate {
|
|
|
130
129
|
readonly message?: TelegramMessage;
|
|
131
130
|
readonly edited_message?: TelegramMessage;
|
|
132
131
|
readonly callback_query?: TelegramCallbackQuery;
|
|
132
|
+
/** A member's status changed (only delivered when `allowed_updates` lists it and the Bot is an admin). */
|
|
133
|
+
readonly chat_member?: TelegramChatMemberUpdated;
|
|
134
|
+
/** The Bot's own member status changed. */
|
|
135
|
+
readonly my_chat_member?: TelegramChatMemberUpdated;
|
|
136
|
+
}
|
|
137
|
+
export interface TelegramChatMemberUpdated {
|
|
138
|
+
readonly chat: TelegramChat;
|
|
139
|
+
readonly from?: TelegramUser;
|
|
140
|
+
readonly date?: number;
|
|
141
|
+
readonly old_chat_member?: TelegramChatMember;
|
|
142
|
+
readonly new_chat_member: TelegramChatMember;
|
|
133
143
|
}
|
|
134
144
|
export interface TelegramChatMember {
|
|
135
145
|
readonly status: string;
|
|
@@ -138,6 +148,8 @@ export interface TelegramChatMember {
|
|
|
138
148
|
readonly can_pin_messages?: boolean;
|
|
139
149
|
readonly can_delete_messages?: boolean;
|
|
140
150
|
readonly can_manage_chat?: boolean;
|
|
151
|
+
/** `restricted` only: whether the user is still a member of the chat. */
|
|
152
|
+
readonly is_member?: boolean;
|
|
141
153
|
}
|
|
142
154
|
export interface TelegramWireSegment {
|
|
143
155
|
readonly type: string;
|
|
@@ -237,13 +249,22 @@ export type TelegramOutboundAction = {
|
|
|
237
249
|
readonly message_id: number;
|
|
238
250
|
};
|
|
239
251
|
};
|
|
252
|
+
} | {
|
|
253
|
+
readonly method: 'sendPoll';
|
|
254
|
+
readonly params: {
|
|
255
|
+
readonly chat_id: number | string;
|
|
256
|
+
readonly question: string;
|
|
257
|
+
readonly options: readonly string[];
|
|
258
|
+
readonly is_anonymous: boolean;
|
|
259
|
+
readonly allows_multiple_answers: boolean;
|
|
260
|
+
readonly reply_parameters?: {
|
|
261
|
+
readonly message_id: number;
|
|
262
|
+
};
|
|
263
|
+
};
|
|
240
264
|
};
|
|
241
265
|
export declare function resolveTelegramConfig(config: TelegramEndpointConfig): ResolvedTelegramConfig;
|
|
242
266
|
export declare function normalizeWebhookPath(path: string): string;
|
|
243
267
|
export declare function buildWebhookUrl(webhook: NonNullable<ResolvedTelegramConfig['webhook']>): string;
|
|
244
|
-
export declare function readTextBody(request: IncomingMessage, options?: {
|
|
245
|
-
readonly limit?: number;
|
|
246
|
-
}): Promise<string>;
|
|
247
268
|
export declare function botApiUrl(config: Pick<ResolvedTelegramConfig, 'apiBaseUrl' | 'token'>, method: string): string;
|
|
248
269
|
/** Telegram chat.type → canonical 会话 kind(supergroup 即 group,无容器层级故无 parent)。 */
|
|
249
270
|
export declare function resolveTelegramChannelType(chatType: TelegramChat['type']): ConversationKind;
|
package/lib/protocol.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Bot API protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
1
5
|
import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
|
|
2
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
7
|
import { escapeTelegramHtml, markdownToTelegramHtml } from './markdown-to-html.js';
|
|
@@ -12,7 +16,8 @@ export function resolveTelegramConfig(config) {
|
|
|
12
16
|
const webhookSource = config.webhook;
|
|
13
17
|
const mode = polling === false ? 'webhook' : 'polling';
|
|
14
18
|
const apiBaseUrl = (config.apiBaseUrl ?? 'https://api.telegram.org').replace(/\/$/, '');
|
|
15
|
-
|
|
19
|
+
// chat_member / my_chat_member keep the cached sender standing fresh (ADR 0056 §1).
|
|
20
|
+
const allowedUpdates = config.allowedUpdates ?? ['message', 'callback_query', 'chat_member', 'my_chat_member'];
|
|
16
21
|
const webhook = mode === 'webhook'
|
|
17
22
|
? {
|
|
18
23
|
domain: webhookSource?.domain ?? '',
|
|
@@ -49,21 +54,6 @@ export function buildWebhookUrl(webhook) {
|
|
|
49
54
|
}
|
|
50
55
|
return `${domain}${webhook.path}`;
|
|
51
56
|
}
|
|
52
|
-
export async function readTextBody(request, options = {}) {
|
|
53
|
-
const limit = options.limit ?? 1_048_576;
|
|
54
|
-
const chunks = [];
|
|
55
|
-
let size = 0;
|
|
56
|
-
for await (const chunk of request) {
|
|
57
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
58
|
-
size += buffer.length;
|
|
59
|
-
if (size > limit) {
|
|
60
|
-
request.destroy();
|
|
61
|
-
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
62
|
-
}
|
|
63
|
-
chunks.push(buffer);
|
|
64
|
-
}
|
|
65
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
66
|
-
}
|
|
67
57
|
export function botApiUrl(config, method) {
|
|
68
58
|
return `${config.apiBaseUrl}/bot${config.token}/${method}`;
|
|
69
59
|
}
|
|
@@ -474,6 +464,31 @@ function buildOutboundActions(target, payload, uploads) {
|
|
|
474
464
|
}
|
|
475
465
|
break;
|
|
476
466
|
}
|
|
467
|
+
case 'telegram:sticker': {
|
|
468
|
+
// Agent-authored sticker: an existing Telegram file_id or an http(s) URL only, never a host path.
|
|
469
|
+
const sticker = typeof data.sticker === 'string' ? data.sticker.trim() : '';
|
|
470
|
+
if (!sticker || /^(?:file:|base64:|attach:)/i.test(sticker) || (/[\\/]/.test(sticker) && !/^https?:\/\//i.test(sticker))) {
|
|
471
|
+
throw new EndpointDeliveryError('invalid_payload', 'telegram:sticker requires a Telegram file_id or an http(s) URL', 'not_sent');
|
|
472
|
+
}
|
|
473
|
+
flushText();
|
|
474
|
+
actions.push({ method: 'sendSticker', params: { chat_id: chatId, sticker, ...replyParams() } });
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'telegram:poll': {
|
|
478
|
+
const question = typeof data.question === 'string' ? data.question.trim() : '';
|
|
479
|
+
const options = Array.isArray(data.options) ? data.options.map(String).filter(Boolean) : [];
|
|
480
|
+
if (!question || options.length < 2) {
|
|
481
|
+
throw new EndpointDeliveryError('invalid_payload', 'telegram:poll requires a question and at least two options', 'not_sent');
|
|
482
|
+
}
|
|
483
|
+
flushText();
|
|
484
|
+
actions.push({ method: 'sendPoll', params: {
|
|
485
|
+
chat_id: chatId, question, options,
|
|
486
|
+
is_anonymous: data.is_anonymous !== false,
|
|
487
|
+
allows_multiple_answers: data.allows_multiple_answers === true,
|
|
488
|
+
...replyParams(),
|
|
489
|
+
} });
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
477
492
|
case 'location': {
|
|
478
493
|
flushText();
|
|
479
494
|
actions.push({
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
1
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
3
2
|
import { type ResolvedTelegramConfig, type TelegramUpdate } from './protocol.js';
|
|
4
3
|
/** 等长时才 timingSafeEqual,避免长度差异直接抛异常。 */
|
|
@@ -9,4 +8,4 @@ export interface TelegramWebhookHandler {
|
|
|
9
8
|
handleUpdate(update: TelegramUpdate): void | Promise<void>;
|
|
10
9
|
}
|
|
11
10
|
export declare function registerTelegramWebhookRoutes(http: HttpHost, handler: TelegramWebhookHandler): HttpRouteRegistration[];
|
|
12
|
-
export declare function handleTelegramWebhookRequest(request:
|
|
11
|
+
export declare function handleTelegramWebhookRequest(request: Request, handler: TelegramWebhookHandler): Promise<Response>;
|
package/lib/webhook.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Telegram webhook HTTP: secret token → parse → handle update.
|
|
3
3
|
*/
|
|
4
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { readTextBody } from '@zhin.js/host-http-contract';
|
|
5
6
|
import { getLogger } from '@zhin.js/logger';
|
|
6
|
-
import { readTextBody } from './protocol.js';
|
|
7
7
|
const logger = getLogger('telegram');
|
|
8
8
|
/** 等长时才 timingSafeEqual,避免长度差异直接抛异常。 */
|
|
9
9
|
export function safeTokenEqual(a, b) {
|
|
@@ -14,21 +14,16 @@ export function safeTokenEqual(a, b) {
|
|
|
14
14
|
export function registerTelegramWebhookRoutes(http, handler) {
|
|
15
15
|
const path = handler.config.webhook.path;
|
|
16
16
|
return [
|
|
17
|
-
http.route('POST', path,
|
|
18
|
-
await handleTelegramWebhookRequest(request, response, handler);
|
|
19
|
-
}, { summary: 'Telegram Bot API webhook', tags: ['telegram'] }),
|
|
17
|
+
http.route('POST', path, (request) => handleTelegramWebhookRequest(request, handler), { summary: 'Telegram Bot API webhook', tags: ['telegram'] }),
|
|
20
18
|
];
|
|
21
19
|
}
|
|
22
|
-
export async function handleTelegramWebhookRequest(request,
|
|
20
|
+
export async function handleTelegramWebhookRequest(request, handler) {
|
|
23
21
|
try {
|
|
24
22
|
const secret = handler.config.webhook?.secretToken;
|
|
25
23
|
if (secret) {
|
|
26
|
-
const
|
|
27
|
-
const token = Array.isArray(header) ? header[0] : header;
|
|
24
|
+
const token = request.headers.get('x-telegram-bot-api-secret-token');
|
|
28
25
|
if (!token || !safeTokenEqual(token, secret)) {
|
|
29
|
-
|
|
30
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid secret token' }));
|
|
31
|
-
return;
|
|
26
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid secret token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
29
|
const rawBody = await readTextBody(request);
|
|
@@ -37,24 +32,18 @@ export async function handleTelegramWebhookRequest(request, response, handler) {
|
|
|
37
32
|
update = JSON.parse(rawBody);
|
|
38
33
|
}
|
|
39
34
|
catch {
|
|
40
|
-
|
|
41
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid JSON' }));
|
|
42
|
-
return;
|
|
35
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
43
36
|
}
|
|
44
37
|
if (!update || !Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
45
|
-
|
|
46
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid update_id' }));
|
|
47
|
-
return;
|
|
38
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid update_id' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
48
39
|
}
|
|
49
40
|
if (!handler.isOpen)
|
|
50
41
|
throw new Error('Telegram endpoint is not accepting updates');
|
|
51
42
|
await handler.handleUpdate(update);
|
|
52
|
-
|
|
53
|
-
response.end(JSON.stringify({ ok: true }));
|
|
43
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
54
44
|
}
|
|
55
45
|
catch (error) {
|
|
56
46
|
logger.error('Telegram webhook error:', error);
|
|
57
|
-
|
|
58
|
-
response.end(JSON.stringify({ ok: false, description: 'Update was not admitted' }));
|
|
47
|
+
return new Response(JSON.stringify({ ok: false, description: 'Update was not admitted' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
59
48
|
}
|
|
60
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-telegram",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.27",
|
|
4
4
|
"description": "Zhin.js Telegram Bot API adapter for Plugin Runtime (long-poll getUpdates)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -31,25 +31,26 @@
|
|
|
31
31
|
"directory": "plugins/adapters/telegram"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"@zhin.js/
|
|
36
|
-
"@zhin.js/
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
41
|
-
"@zhin.js/
|
|
42
|
-
"@zhin.js/
|
|
43
|
-
"@zhin.js/
|
|
44
|
-
"@zhin.js/
|
|
34
|
+
"zod": "^4.6.7",
|
|
35
|
+
"@zhin.js/adapter": "1.1.33",
|
|
36
|
+
"@zhin.js/agent-feature": "1.1.17",
|
|
37
|
+
"@zhin.js/console-contract": "1.1.6",
|
|
38
|
+
"@zhin.js/core": "1.2.0",
|
|
39
|
+
"@zhin.js/feature-kit": "1.1.16",
|
|
40
|
+
"@zhin.js/host-http": "1.2.0",
|
|
41
|
+
"@zhin.js/host-http-contract": "1.2.0",
|
|
42
|
+
"@zhin.js/im-contract": "1.1.14",
|
|
43
|
+
"@zhin.js/logger": "1.1.3",
|
|
44
|
+
"@zhin.js/page": "1.1.18",
|
|
45
|
+
"@zhin.js/skill": "1.1.17",
|
|
46
|
+
"@zhin.js/tool": "1.1.19"
|
|
45
47
|
},
|
|
46
48
|
"peerDependencies": {
|
|
47
|
-
"
|
|
48
|
-
"@zhin.js/
|
|
49
|
-
"@zhin.js/
|
|
50
|
-
"@zhin.js/
|
|
51
|
-
"
|
|
52
|
-
"zhin.js": "^1.1.24"
|
|
49
|
+
"@zhin.js/adapter": "^1.1.33",
|
|
50
|
+
"@zhin.js/command": "^1.2.0",
|
|
51
|
+
"@zhin.js/core": "^1.2.0",
|
|
52
|
+
"@zhin.js/permission": "^1.2.0",
|
|
53
|
+
"zhin.js": "^1.1.26"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"@zhin.js/command": {
|
|
@@ -57,9 +58,6 @@
|
|
|
57
58
|
},
|
|
58
59
|
"zhin.js": {
|
|
59
60
|
"optional": true
|
|
60
|
-
},
|
|
61
|
-
"zod": {
|
|
62
|
-
"optional": true
|
|
63
61
|
}
|
|
64
62
|
},
|
|
65
63
|
"devDependencies": {
|
|
@@ -67,10 +65,9 @@
|
|
|
67
65
|
"htmlparser2": "12.0.0",
|
|
68
66
|
"typescript": "^6.0.3",
|
|
69
67
|
"vitest": "^4.1.10",
|
|
70
|
-
"
|
|
71
|
-
"@zhin.js/
|
|
72
|
-
"
|
|
73
|
-
"zhin.js": "1.1.24"
|
|
68
|
+
"@zhin.js/host-http": "1.2.0",
|
|
69
|
+
"@zhin.js/pagemanager": "1.1.25",
|
|
70
|
+
"zhin.js": "1.1.26"
|
|
74
71
|
},
|
|
75
72
|
"files": [
|
|
76
73
|
"adapters",
|
|
@@ -80,7 +77,6 @@
|
|
|
80
77
|
"schema.json",
|
|
81
78
|
"src",
|
|
82
79
|
"lib",
|
|
83
|
-
"tools",
|
|
84
80
|
"agents",
|
|
85
81
|
"README.md",
|
|
86
82
|
"CHANGELOG.md",
|
|
@@ -130,6 +126,6 @@
|
|
|
130
126
|
"scripts": {
|
|
131
127
|
"build": "tsc",
|
|
132
128
|
"clean": "rimraf lib",
|
|
133
|
-
"test": "
|
|
129
|
+
"test": "vitest run --root ../../.. plugins/adapters/telegram/tests"
|
|
134
130
|
}
|
|
135
131
|
}
|
package/plugin.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
2
|
import { definePlugin } from 'zhin.js';
|
|
3
|
-
import { permissionHostToken } from '@zhin.js/permission';
|
|
4
|
-
import { checkTelegramPlatformPermit } from "./lib/platform-permit.js";
|
|
5
3
|
import { createTelegramConsoleRpc } from "./lib/console.js";
|
|
6
4
|
export default definePlugin({
|
|
7
5
|
name: 'telegram',
|
|
@@ -10,12 +8,8 @@ export default definePlugin({
|
|
|
10
8
|
displayName: 'Telegram Bot API Adapter',
|
|
11
9
|
},
|
|
12
10
|
setup(context) {
|
|
13
|
-
const dispose = context.resources.has(permissionHostToken)
|
|
14
|
-
? context.resources.use(permissionHostToken).registerPlatform('telegram', checkTelegramPlatformPermit)
|
|
15
|
-
: undefined;
|
|
16
11
|
return {
|
|
17
12
|
consoleRpc: createTelegramConsoleRpc(() => context.config.get()),
|
|
18
|
-
...(dispose ? { dispose } : {}),
|
|
19
13
|
};
|
|
20
14
|
},
|
|
21
15
|
});
|