@zhin.js/adapter-telegram 1.1.26 → 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 +176 -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/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/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/src/endpoint.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createTelegramStreaming } from './streaming.js';
|
|
|
3
3
|
import {
|
|
4
4
|
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification, createEndpointLifecycle,
|
|
5
5
|
type EndpointLifecycle, type EndpointContentPort, type EndpointContentResolveContext,
|
|
6
|
-
type EndpointControl, type EndpointSendRequest, type EndpointTransportState,
|
|
6
|
+
type EndpointControl, type EndpointManagement, type EndpointSendRequest, type EndpointTransportState,
|
|
7
7
|
InboundDeduper, TtlLookupCache,
|
|
8
8
|
} from 'zhin.js/adapter';
|
|
9
9
|
/**
|
|
@@ -17,13 +17,14 @@ import {
|
|
|
17
17
|
type ConversationReference,
|
|
18
18
|
type ConversationResolution,
|
|
19
19
|
type MessageRef,
|
|
20
|
+
type PlatformStanding,
|
|
20
21
|
} from '@zhin.js/im-contract';
|
|
21
22
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
22
23
|
import type { CapabilityId } from 'zhin.js';
|
|
23
24
|
import { runTelegramPollLoop } from './polling.js';
|
|
24
25
|
import { TelegramApiError } from './api-error.js';
|
|
25
26
|
export { TelegramApiError } from './api-error.js';
|
|
26
|
-
import {
|
|
27
|
+
import { telegramChatMemberStanding } from './platform-permit.js';
|
|
27
28
|
import {
|
|
28
29
|
buildWebhookUrl,
|
|
29
30
|
formatCallbackContent,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
type ResolvedTelegramConfig,
|
|
38
39
|
type TelegramCallbackQuery,
|
|
39
40
|
type TelegramChatMember,
|
|
41
|
+
type TelegramChatMemberUpdated,
|
|
40
42
|
type TelegramMessage,
|
|
41
43
|
type TelegramOutboundUpload,
|
|
42
44
|
type TelegramUpdate,
|
|
@@ -46,13 +48,13 @@ import { createTelegramSelfProfile } from './self-profile.js';
|
|
|
46
48
|
|
|
47
49
|
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
48
50
|
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
51
|
+
/** A failed `getChatMember` is not retried for every message of a busy chat (unknown standing meanwhile). */
|
|
52
|
+
const CHAT_MEMBER_FAILURE_TTL_MS = 10_000;
|
|
49
53
|
const ADMITTED_UPDATE_LIMIT = 2_048;
|
|
50
54
|
const INFLIGHT_UPDATE_LIMIT = 256;
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
readonly permissions: string[];
|
|
55
|
-
}
|
|
56
|
+
/** ADR 0056 §1 normalized standing of one chat member (cached per chat + user). */
|
|
57
|
+
type ChatMemberPermit = PlatformStanding;
|
|
56
58
|
|
|
57
59
|
/** One admitted platform message and its original framework lifetime. */
|
|
58
60
|
interface TelegramMessageAdmission {
|
|
@@ -76,7 +78,8 @@ export { TelegramClient, type TelegramClientApi, type TelegramFetch } from './te
|
|
|
76
78
|
/**
|
|
77
79
|
* Telegram Bot API 无列表类接口(无 getMyChats/getChatMembers),
|
|
78
80
|
* 仅 getChat/getChatMember 按已知 id 单查,不构成列表能力;
|
|
79
|
-
* 因此本 endpoint
|
|
81
|
+
* 因此本 endpoint 的 EndpointManagement 只有 `inspectSelfStanding`(ADR 0056 §1:getChatMember bot),
|
|
82
|
+
* Console 社交面 RPC 对该平台保持未接线。
|
|
80
83
|
*/
|
|
81
84
|
export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
82
85
|
readonly [endpointSpec]: EndpointSpecification;
|
|
@@ -100,6 +103,23 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
100
103
|
ttlMs: CHAT_MEMBER_CACHE_TTL_MS,
|
|
101
104
|
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
102
105
|
});
|
|
106
|
+
/** Recently failed lookups (negative cache): the standing stays unknown until it expires. */
|
|
107
|
+
readonly #chatMemberFailures = new TtlLookupCache<string, true>({
|
|
108
|
+
ttlMs: CHAT_MEMBER_FAILURE_TTL_MS,
|
|
109
|
+
maxEntries: CHAT_MEMBER_CACHE_MAX,
|
|
110
|
+
});
|
|
111
|
+
/** Concurrent messages from one sender share one `getChatMember` call. */
|
|
112
|
+
readonly #chatMemberInflight = new Map<string, { promise?: Promise<ChatMemberPermit | undefined> }>();
|
|
113
|
+
readonly #management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
114
|
+
// ADR 0056 §1: the Bot's own standing (`bot:*` permits); the framework caches per conversation.
|
|
115
|
+
inspectSelfStanding: async ({ conversation, signal }) => {
|
|
116
|
+
if (conversation.kind === 'private' || !this.#botUserId) return undefined;
|
|
117
|
+
const member = await this.callApi<TelegramChatMember>('getChatMember', {
|
|
118
|
+
chat_id: Number(conversation.id), user_id: this.#botUserId,
|
|
119
|
+
}, signal);
|
|
120
|
+
return telegramChatMemberStanding(member);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
103
123
|
readonly #control: EndpointControl = Object.freeze<EndpointControl>({
|
|
104
124
|
recall: ({ message, signal }) => this.recallMessage(message, signal),
|
|
105
125
|
});
|
|
@@ -117,7 +137,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
117
137
|
start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
118
138
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
|
|
119
139
|
get name() { return options.config.id; }, get transportState() { return endpoint.#transportState; },
|
|
120
|
-
control: this.#control, content: this.#content,
|
|
140
|
+
control: this.#control, content: this.#content, management: this.#management,
|
|
121
141
|
streaming: options.config.streaming !== false ? createTelegramStreaming(() => this, options.config.streamingIntervalMs) : undefined,
|
|
122
142
|
profile: createTelegramSelfProfile({ client: this,
|
|
123
143
|
current: () => this.#botName === undefined ? {} : { nickname: this.#botName },
|
|
@@ -409,7 +429,7 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
409
429
|
sender: {
|
|
410
430
|
id: String(msg.from?.id ?? ''),
|
|
411
431
|
name: senderDisplayName(msg.from) || undefined,
|
|
412
|
-
...(permit
|
|
432
|
+
...(permit ? { platform: permit } : {}),
|
|
413
433
|
},
|
|
414
434
|
endpointId: this.#options.config.id,
|
|
415
435
|
...(mentioned ? { mentioned: true } : {}),
|
|
@@ -419,8 +439,6 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
419
439
|
chatType: msg.chat.type,
|
|
420
440
|
userId: msg.from?.id,
|
|
421
441
|
date: msg.date,
|
|
422
|
-
...(permit?.role ? { senderRole: permit.role } : {}),
|
|
423
|
-
...(permit?.permissions.length ? { senderPermissions: [...permit.permissions] } : {}),
|
|
424
442
|
}),
|
|
425
443
|
});
|
|
426
444
|
}
|
|
@@ -446,7 +464,10 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
446
464
|
return false;
|
|
447
465
|
}
|
|
448
466
|
|
|
449
|
-
/**
|
|
467
|
+
/**
|
|
468
|
+
* 群消息 sender role 解析:getChatMember + 60s 缓存;并发查询合并为一次调用,失败负缓存 10s,
|
|
469
|
+
* `chat_member` / `my_chat_member` 更新时以新状态覆盖缓存。
|
|
470
|
+
*/
|
|
450
471
|
async #resolveGroupSenderPermit(msg: TelegramMessage, signal: AbortSignal): Promise<ChatMemberPermit | undefined> {
|
|
451
472
|
if (msg.chat.type === 'private' || !msg.from?.id) return undefined;
|
|
452
473
|
const chatId = Number(msg.chat.id);
|
|
@@ -454,20 +475,45 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
454
475
|
const key = `${chatId}:${userId}`;
|
|
455
476
|
const cached = this.#chatMemberCache.get(key);
|
|
456
477
|
if (cached) return cached;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
478
|
+
if (this.#chatMemberFailures.get(key)) return undefined;
|
|
479
|
+
let task = this.#chatMemberInflight.get(key)?.promise;
|
|
480
|
+
if (!task) {
|
|
481
|
+
const lookup: { promise?: Promise<ChatMemberPermit | undefined> } = {};
|
|
482
|
+
task = (async () => {
|
|
483
|
+
await undefined; // registered in #chatMemberInflight before the body runs
|
|
484
|
+
try {
|
|
485
|
+
const member = await this.callApi<TelegramChatMember>('getChatMember', {
|
|
486
|
+
chat_id: chatId,
|
|
487
|
+
user_id: userId,
|
|
488
|
+
}, signal);
|
|
489
|
+
if (signal.aborted) return undefined;
|
|
490
|
+
const entry: ChatMemberPermit = telegramChatMemberStanding(member);
|
|
491
|
+
// Invalidated (chat_member update) while in flight: do not store the older answer.
|
|
492
|
+
if (this.#chatMemberInflight.get(key) === lookup) this.#chatMemberCache.set(key, entry);
|
|
493
|
+
return entry;
|
|
494
|
+
} catch {
|
|
495
|
+
// 保守拒绝:无角色快照;取消不算失败
|
|
496
|
+
if (!signal.aborted && this.#chatMemberInflight.get(key) === lookup) this.#chatMemberFailures.set(key, true);
|
|
497
|
+
return undefined;
|
|
498
|
+
} finally {
|
|
499
|
+
if (this.#chatMemberInflight.get(key) === lookup) this.#chatMemberInflight.delete(key);
|
|
500
|
+
}
|
|
501
|
+
})();
|
|
502
|
+
lookup.promise = task;
|
|
503
|
+
this.#chatMemberInflight.set(key, lookup);
|
|
470
504
|
}
|
|
505
|
+
const entry = await task;
|
|
506
|
+
return signal.aborted ? undefined : entry;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** `chat_member` / `my_chat_member`: the update carries the new status, so it replaces the cached one. */
|
|
510
|
+
#applyChatMemberUpdate(update: TelegramChatMemberUpdated | undefined): void {
|
|
511
|
+
const userId = update?.new_chat_member?.user?.id;
|
|
512
|
+
if (!update?.chat || userId == null) return;
|
|
513
|
+
const key = `${Number(update.chat.id)}:${userId}`;
|
|
514
|
+
this.#chatMemberInflight.delete(key);
|
|
515
|
+
this.#chatMemberFailures.delete(key);
|
|
516
|
+
this.#chatMemberCache.set(key, telegramChatMemberStanding(update.new_chat_member));
|
|
471
517
|
}
|
|
472
518
|
|
|
473
519
|
/** Test / internal: admit a callback query when open. */
|
|
@@ -544,6 +590,8 @@ export class TelegramEndpoint extends TelegramClient implements Endpoint {
|
|
|
544
590
|
if (update.message || update.callback_query) {
|
|
545
591
|
await this[endpointSpec].events.platform(eventName, update);
|
|
546
592
|
} else {
|
|
593
|
+
this.#applyChatMemberUpdate(update.chat_member);
|
|
594
|
+
this.#applyChatMemberUpdate(update.my_chat_member);
|
|
547
595
|
await this[endpointSpec].events.emitAccepted('platform.receive', Object.freeze({ name: eventName, event: update }));
|
|
548
596
|
return;
|
|
549
597
|
}
|
package/src/index.ts
CHANGED
|
@@ -33,10 +33,8 @@ export { type TelegramClientEventMap } from './client.js';
|
|
|
33
33
|
export { TelegramAdapter } from './adapter.js';
|
|
34
34
|
|
|
35
35
|
export {
|
|
36
|
-
|
|
37
|
-
normalizeTelegramChatMember,
|
|
36
|
+
telegramChatMemberStanding,
|
|
38
37
|
platformPermit,
|
|
39
|
-
telegramGroupPermitResolver,
|
|
40
38
|
} from './platform-permit.js';
|
|
41
39
|
|
|
42
40
|
export type { TelegramPollRequest } from './telegram-client.js';
|
package/src/platform-permit.ts
CHANGED
|
@@ -1,68 +1,39 @@
|
|
|
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 {
|
|
8
|
-
|
|
9
|
-
const ADAPTER = 'telegram';
|
|
6
|
+
import type { PlatformStanding } from '@zhin.js/im-contract';
|
|
10
7
|
|
|
11
8
|
export function platformPermit(perm: string): string {
|
|
12
|
-
return `platform
|
|
9
|
+
return `platform-permission:${perm}`;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
|
|
22
|
-
}
|
|
12
|
+
/** Administrator rights a chat creator implicitly holds (Bot API `ChatAdministratorRights`). */
|
|
13
|
+
const TELEGRAM_ADMIN_RIGHTS = [
|
|
14
|
+
'manage_chat', 'delete_messages', 'manage_video_chats', 'restrict_members', 'promote_members', 'change_info',
|
|
15
|
+
'invite_users', 'post_stories', 'edit_stories', 'delete_stories', 'post_messages', 'edit_messages', 'pin_messages',
|
|
16
|
+
'manage_topics',
|
|
17
|
+
] as const;
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
/**
|
|
20
|
+
* ADR 0056 §1:`getChatMember` 结果 → `sender.platform`。
|
|
21
|
+
* creator → `owner`(拥有全部管理员权限);administrator → `admin`,其 `can_*` 权限去掉前缀进
|
|
22
|
+
* `permissions`(如 `restrict_members`、`pin_messages`);member / 仍在群内的 restricted → `member`;
|
|
23
|
+
* left / kicked → `guest`。Telegram 没有身份组,`roles` 不上报。
|
|
24
|
+
*
|
|
25
|
+
* 只有管理员权限作为平台权限上报:restricted 成员的 `can_*`(如 `can_pin_messages`)是对其个人的
|
|
26
|
+
* 发言许可,普通成员同样可能按群默认权限拥有,两者都不上报,`platform-permission:*` 只由管理员满足。
|
|
27
|
+
*/
|
|
28
|
+
export function telegramChatMemberStanding(member: { readonly status?: string; readonly is_member?: boolean }): PlatformStanding {
|
|
32
29
|
const status = member.status;
|
|
33
|
-
if (status === 'creator') {
|
|
34
|
-
permissions.push('chat_creator', 'chat_administrator', 'restrict_members', 'pin_messages');
|
|
35
|
-
return { role: 'owner', permissions };
|
|
36
|
-
}
|
|
30
|
+
if (status === 'creator') return { standing: 'owner', permissions: [...TELEGRAM_ADMIN_RIGHTS] };
|
|
37
31
|
if (status === 'administrator') {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (member.can_manage_chat) permissions.push('manage_chat');
|
|
43
|
-
return { role: 'admin', permissions };
|
|
44
|
-
}
|
|
45
|
-
return { role: 'member', permissions };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function checkTelegramPlatformPermit(perm: string, subject: PermissionSubject): boolean {
|
|
49
|
-
const role = subject.sender?.role?.[0];
|
|
50
|
-
const permissions = subject.sender?.permissions ?? [];
|
|
51
|
-
const has = (t: string) => permissions.includes(t);
|
|
52
|
-
|
|
53
|
-
switch (perm) {
|
|
54
|
-
case 'chat_creator':
|
|
55
|
-
return role === 'owner' || role === 'creator' || has('chat_creator');
|
|
56
|
-
case 'chat_administrator':
|
|
57
|
-
return role === 'owner' || role === 'admin' || role === 'creator' || role === 'administrator' || has('chat_administrator');
|
|
58
|
-
case 'restrict_members':
|
|
59
|
-
return has('restrict_members') || role === 'owner' || role === 'creator';
|
|
60
|
-
case 'pin_messages':
|
|
61
|
-
return has('pin_messages') || role === 'owner' || role === 'creator';
|
|
62
|
-
case 'manage_chat':
|
|
63
|
-
return has('manage_chat') || role === 'owner' || role === 'creator';
|
|
64
|
-
default:
|
|
65
|
-
return false;
|
|
32
|
+
const granted = Object.entries(member as Readonly<Record<string, unknown>>)
|
|
33
|
+
.filter(([key, value]) => key.startsWith('can_') && value === true)
|
|
34
|
+
.map(([key]) => key.slice(4));
|
|
35
|
+
return { standing: 'admin', ...(granted.length ? { permissions: granted } : {}) };
|
|
66
36
|
}
|
|
37
|
+
if (status === 'left' || status === 'kicked' || (status === 'restricted' && member.is_member === false)) return { standing: 'guest' };
|
|
38
|
+
return { standing: 'member' };
|
|
67
39
|
}
|
|
68
|
-
|
package/src/protocol.ts
CHANGED
|
@@ -4,7 +4,6 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
|
4
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { IncomingMessage } from 'node:http';
|
|
8
7
|
import { EndpointDeliveryError, isMediaRef, type ConversationKind, type ConversationRef } from '@zhin.js/im-contract';
|
|
9
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
10
9
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
@@ -144,6 +143,18 @@ export interface TelegramUpdate {
|
|
|
144
143
|
readonly message?: TelegramMessage;
|
|
145
144
|
readonly edited_message?: TelegramMessage;
|
|
146
145
|
readonly callback_query?: TelegramCallbackQuery;
|
|
146
|
+
/** A member's status changed (only delivered when `allowed_updates` lists it and the Bot is an admin). */
|
|
147
|
+
readonly chat_member?: TelegramChatMemberUpdated;
|
|
148
|
+
/** The Bot's own member status changed. */
|
|
149
|
+
readonly my_chat_member?: TelegramChatMemberUpdated;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface TelegramChatMemberUpdated {
|
|
153
|
+
readonly chat: TelegramChat;
|
|
154
|
+
readonly from?: TelegramUser;
|
|
155
|
+
readonly date?: number;
|
|
156
|
+
readonly old_chat_member?: TelegramChatMember;
|
|
157
|
+
readonly new_chat_member: TelegramChatMember;
|
|
147
158
|
}
|
|
148
159
|
|
|
149
160
|
export interface TelegramChatMember {
|
|
@@ -153,6 +164,8 @@ export interface TelegramChatMember {
|
|
|
153
164
|
readonly can_pin_messages?: boolean;
|
|
154
165
|
readonly can_delete_messages?: boolean;
|
|
155
166
|
readonly can_manage_chat?: boolean;
|
|
167
|
+
/** `restricted` only: whether the user is still a member of the chat. */
|
|
168
|
+
readonly is_member?: boolean;
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
export interface TelegramWireSegment {
|
|
@@ -245,6 +258,17 @@ export type TelegramOutboundAction =
|
|
|
245
258
|
readonly longitude: number;
|
|
246
259
|
readonly reply_parameters?: { readonly message_id: number };
|
|
247
260
|
};
|
|
261
|
+
}
|
|
262
|
+
| {
|
|
263
|
+
readonly method: 'sendPoll';
|
|
264
|
+
readonly params: {
|
|
265
|
+
readonly chat_id: number | string;
|
|
266
|
+
readonly question: string;
|
|
267
|
+
readonly options: readonly string[];
|
|
268
|
+
readonly is_anonymous: boolean;
|
|
269
|
+
readonly allows_multiple_answers: boolean;
|
|
270
|
+
readonly reply_parameters?: { readonly message_id: number };
|
|
271
|
+
};
|
|
248
272
|
};
|
|
249
273
|
|
|
250
274
|
export function resolveTelegramConfig(config: TelegramEndpointConfig): ResolvedTelegramConfig {
|
|
@@ -256,7 +280,8 @@ export function resolveTelegramConfig(config: TelegramEndpointConfig): ResolvedT
|
|
|
256
280
|
const webhookSource = config.webhook;
|
|
257
281
|
const mode: 'polling' | 'webhook' = polling === false ? 'webhook' : 'polling';
|
|
258
282
|
const apiBaseUrl = (config.apiBaseUrl ?? 'https://api.telegram.org').replace(/\/$/, '');
|
|
259
|
-
|
|
283
|
+
// chat_member / my_chat_member keep the cached sender standing fresh (ADR 0056 §1).
|
|
284
|
+
const allowedUpdates = config.allowedUpdates ?? ['message', 'callback_query', 'chat_member', 'my_chat_member'];
|
|
260
285
|
const webhook = mode === 'webhook'
|
|
261
286
|
? {
|
|
262
287
|
domain: webhookSource?.domain ?? '',
|
|
@@ -297,24 +322,6 @@ export function buildWebhookUrl(webhook: NonNullable<ResolvedTelegramConfig['web
|
|
|
297
322
|
return `${domain}${webhook.path}`;
|
|
298
323
|
}
|
|
299
324
|
|
|
300
|
-
export async function readTextBody(
|
|
301
|
-
request: IncomingMessage,
|
|
302
|
-
options: { readonly limit?: number } = {},
|
|
303
|
-
): Promise<string> {
|
|
304
|
-
const limit = options.limit ?? 1_048_576;
|
|
305
|
-
const chunks: Buffer[] = [];
|
|
306
|
-
let size = 0;
|
|
307
|
-
for await (const chunk of request) {
|
|
308
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
309
|
-
size += buffer.length;
|
|
310
|
-
if (size > limit) {
|
|
311
|
-
request.destroy();
|
|
312
|
-
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
313
|
-
}
|
|
314
|
-
chunks.push(buffer);
|
|
315
|
-
}
|
|
316
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
317
|
-
}
|
|
318
325
|
|
|
319
326
|
export function botApiUrl(config: Pick<ResolvedTelegramConfig, 'apiBaseUrl' | 'token'>, method: string): string {
|
|
320
327
|
return `${config.apiBaseUrl}/bot${config.token}/${method}`;
|
|
@@ -746,6 +753,31 @@ function buildOutboundActions(
|
|
|
746
753
|
}
|
|
747
754
|
break;
|
|
748
755
|
}
|
|
756
|
+
case 'telegram:sticker': {
|
|
757
|
+
// Agent-authored sticker: an existing Telegram file_id or an http(s) URL only, never a host path.
|
|
758
|
+
const sticker = typeof data.sticker === 'string' ? data.sticker.trim() : '';
|
|
759
|
+
if (!sticker || /^(?:file:|base64:|attach:)/i.test(sticker) || (/[\\/]/.test(sticker) && !/^https?:\/\//i.test(sticker))) {
|
|
760
|
+
throw new EndpointDeliveryError('invalid_payload', 'telegram:sticker requires a Telegram file_id or an http(s) URL', 'not_sent');
|
|
761
|
+
}
|
|
762
|
+
flushText();
|
|
763
|
+
actions.push({ method: 'sendSticker', params: { chat_id: chatId, sticker, ...replyParams() } });
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case 'telegram:poll': {
|
|
767
|
+
const question = typeof data.question === 'string' ? data.question.trim() : '';
|
|
768
|
+
const options = Array.isArray(data.options) ? data.options.map(String).filter(Boolean) : [];
|
|
769
|
+
if (!question || options.length < 2) {
|
|
770
|
+
throw new EndpointDeliveryError('invalid_payload', 'telegram:poll requires a question and at least two options', 'not_sent');
|
|
771
|
+
}
|
|
772
|
+
flushText();
|
|
773
|
+
actions.push({ method: 'sendPoll', params: {
|
|
774
|
+
chat_id: chatId, question, options,
|
|
775
|
+
is_anonymous: data.is_anonymous !== false,
|
|
776
|
+
allows_multiple_answers: data.allows_multiple_answers === true,
|
|
777
|
+
...replyParams(),
|
|
778
|
+
} });
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
749
781
|
case 'location': {
|
|
750
782
|
flushText();
|
|
751
783
|
actions.push({
|
package/src/webhook.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Telegram webhook HTTP: secret token → parse → handle update.
|
|
3
3
|
*/
|
|
4
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
5
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
6
|
+
import { readTextBody } from '@zhin.js/host-http-contract';
|
|
7
7
|
import { getLogger } from '@zhin.js/logger';
|
|
8
|
-
import {
|
|
8
|
+
import { type ResolvedTelegramConfig, type TelegramUpdate } from './protocol.js';
|
|
9
9
|
|
|
10
10
|
const logger = getLogger('telegram');
|
|
11
11
|
|
|
@@ -28,26 +28,20 @@ export function registerTelegramWebhookRoutes(
|
|
|
28
28
|
): HttpRouteRegistration[] {
|
|
29
29
|
const path = handler.config.webhook!.path;
|
|
30
30
|
return [
|
|
31
|
-
http.route('POST', path,
|
|
32
|
-
await handleTelegramWebhookRequest(request, response, handler);
|
|
33
|
-
}, { summary: 'Telegram Bot API webhook', tags: ['telegram'] }),
|
|
31
|
+
http.route('POST', path, (request) => handleTelegramWebhookRequest(request, handler), { summary: 'Telegram Bot API webhook', tags: ['telegram'] }),
|
|
34
32
|
];
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export async function handleTelegramWebhookRequest(
|
|
38
|
-
request:
|
|
39
|
-
response: ServerResponse,
|
|
36
|
+
request: Request,
|
|
40
37
|
handler: TelegramWebhookHandler,
|
|
41
|
-
): Promise<
|
|
38
|
+
): Promise<Response> {
|
|
42
39
|
try {
|
|
43
40
|
const secret = handler.config.webhook?.secretToken;
|
|
44
41
|
if (secret) {
|
|
45
|
-
const
|
|
46
|
-
const token = Array.isArray(header) ? header[0] : header;
|
|
42
|
+
const token = request.headers.get('x-telegram-bot-api-secret-token');
|
|
47
43
|
if (!token || !safeTokenEqual(token, secret)) {
|
|
48
|
-
|
|
49
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid secret token' }));
|
|
50
|
-
return;
|
|
44
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid secret token' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
47
|
|
|
@@ -56,23 +50,17 @@ export async function handleTelegramWebhookRequest(
|
|
|
56
50
|
try {
|
|
57
51
|
update = JSON.parse(rawBody) as TelegramUpdate;
|
|
58
52
|
} catch {
|
|
59
|
-
|
|
60
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid JSON' }));
|
|
61
|
-
return;
|
|
53
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid JSON' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
62
54
|
}
|
|
63
55
|
if (!update || !Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
64
|
-
|
|
65
|
-
response.end(JSON.stringify({ ok: false, description: 'Invalid update_id' }));
|
|
66
|
-
return;
|
|
56
|
+
return new Response(JSON.stringify({ ok: false, description: 'Invalid update_id' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
|
|
67
57
|
}
|
|
68
58
|
if (!handler.isOpen) throw new Error('Telegram endpoint is not accepting updates');
|
|
69
59
|
await handler.handleUpdate(update);
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
response.end(JSON.stringify({ ok: true }));
|
|
61
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
73
62
|
} catch (error) {
|
|
74
63
|
logger.error('Telegram webhook error:', error);
|
|
75
|
-
|
|
76
|
-
response.end(JSON.stringify({ ok: false, description: 'Update was not admitted' }));
|
|
64
|
+
return new Response(JSON.stringify({ ok: false, description: 'Update was not admitted' }), { status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
77
65
|
}
|
|
78
66
|
}
|