@zhin.js/adapter-telegram 5.0.1 → 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 +56 -0
- package/README.md +65 -139
- package/adapters/telegram.ts +27 -0
- package/agent/tools/create_invite.ts +20 -0
- package/agent/tools/list_admins.ts +26 -0
- package/agent/tools/member_count.ts +18 -0
- package/agent/tools/pin_message.ts +21 -0
- package/agent/tools/react.ts +20 -0
- package/agent/tools/send_poll.ts +34 -0
- package/agent/tools/send_sticker.ts +19 -0
- package/agent/tools/set_description.ts +19 -0
- package/agent/tools/set_permissions.ts +33 -0
- package/agent/tools/unpin_message.ts +21 -0
- package/lib/endpoint.d.ts +58 -148
- package/lib/endpoint.js +273 -987
- package/lib/index.d.ts +4 -18
- package/lib/index.js +4 -462
- package/lib/platform-permit.d.ts +1 -2
- package/lib/platform-permit.js +4 -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 +46 -33
- package/plugin.ts +13 -0
- package/schema.json +39 -0
- package/src/endpoint.ts +339 -1103
- package/src/index.ts +39 -435
- package/src/platform-permit.ts +1 -2
- package/src/polling.ts +71 -0
- package/src/protocol.ts +573 -0
- package/src/telegram-agent-deps.ts +63 -0
- 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/adapter.d.ts +0 -21
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -58
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts +0 -29
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/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/{skills/telegram → agent}/PERMITS.md +0 -0
- /package/{skills/telegram/SKILL.md → agent/skills/telegram.md} +0 -0
package/lib/endpoint.js
CHANGED
|
@@ -1,1046 +1,332 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this
|
|
25
|
-
this
|
|
26
|
-
}
|
|
27
|
-
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { runTelegramPollLoop } from './polling.js';
|
|
3
|
+
import { normalizeTelegramChatMember } from './platform-permit.js';
|
|
4
|
+
import { botApiUrl, buildWebhookUrl, formatCallbackContent, formatInboundContent, formatOutboundActions, resolveChannel, senderDisplayName, } from './protocol.js';
|
|
5
|
+
import { registerTelegramAgentEndpoint } from './telegram-agent-deps.js';
|
|
6
|
+
import { registerTelegramWebhookRoutes } from './webhook.js';
|
|
7
|
+
const logger = getLogger('telegram');
|
|
8
|
+
const CHAT_MEMBER_CACHE_TTL_MS = 60_000;
|
|
9
|
+
const CHAT_MEMBER_CACHE_MAX = 2_000;
|
|
10
|
+
export class TelegramEndpoint {
|
|
11
|
+
#options;
|
|
12
|
+
#fetch;
|
|
13
|
+
#pollAbort;
|
|
14
|
+
#pollPromise;
|
|
15
|
+
#routeReleases = [];
|
|
16
|
+
#open = false;
|
|
17
|
+
#started = false;
|
|
18
|
+
#unregisterAgent;
|
|
19
|
+
#updateOffset = 0;
|
|
20
|
+
#botUserId;
|
|
21
|
+
#botUsername;
|
|
22
|
+
#chatMemberCache = new Map();
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#options = options;
|
|
25
|
+
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
26
|
+
}
|
|
27
|
+
/** Used by webhook handler. */
|
|
28
|
+
get isOpen() {
|
|
29
|
+
return this.#open;
|
|
30
|
+
}
|
|
31
|
+
get config() {
|
|
32
|
+
return this.#options.config;
|
|
33
|
+
}
|
|
34
|
+
get allowedUpdates() {
|
|
35
|
+
return this.#options.config.allowedUpdates;
|
|
36
|
+
}
|
|
37
|
+
getUpdateOffset() {
|
|
38
|
+
return this.#updateOffset;
|
|
39
|
+
}
|
|
40
|
+
setUpdateOffset(offset) {
|
|
41
|
+
this.#updateOffset = offset;
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
if (this.#started)
|
|
45
|
+
return;
|
|
46
|
+
this.#started = true;
|
|
28
47
|
try {
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
await ctx.answerCbQuery();
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// already answered
|
|
41
|
-
}
|
|
42
|
-
const message = this.$formatCallbackQuery(ctx);
|
|
43
|
-
this.adapter.emit("message.receive", message);
|
|
44
|
-
this.pluginLogger.debug(`${this.$config.name} recv callback ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`);
|
|
48
|
+
this.#unregisterAgent = registerTelegramAgentEndpoint(this.#options.config.name, this);
|
|
49
|
+
const me = await this.callApi('getMe');
|
|
50
|
+
this.#botUserId = me.id;
|
|
51
|
+
this.#botUsername = me.username;
|
|
52
|
+
if (this.#options.config.mode === 'webhook') {
|
|
53
|
+
if (!this.#options.http) {
|
|
54
|
+
throw new TypeError('Telegram webhook mode requires httpHostToken');
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
else if (this.$config.webhook) {
|
|
55
|
-
// Use webhook
|
|
56
|
-
const { domain, path = "/telegram-webhook", port } = this.$config.webhook;
|
|
57
|
-
await this.launch({
|
|
58
|
-
webhook: {
|
|
59
|
-
domain,
|
|
60
|
-
port,
|
|
61
|
-
hookPath: path,
|
|
62
|
-
},
|
|
63
|
-
allowedUpdates: this.$config.allowedUpdates,
|
|
56
|
+
this.#routeReleases.push(...registerTelegramWebhookRoutes(this.#options.http, this));
|
|
57
|
+
const webhook = this.#options.config.webhook;
|
|
58
|
+
const url = buildWebhookUrl(webhook);
|
|
59
|
+
await this.callApi('setWebhook', {
|
|
60
|
+
url,
|
|
61
|
+
allowed_updates: this.#options.config.allowedUpdates,
|
|
62
|
+
...(webhook.secretToken ? { secret_token: webhook.secretToken } : {}),
|
|
64
63
|
});
|
|
64
|
+
logger.info(formatCompact({
|
|
65
|
+
op: 'connect',
|
|
66
|
+
endpoint: this.#options.config.name,
|
|
67
|
+
mode: 'webhook',
|
|
68
|
+
path: webhook.path,
|
|
69
|
+
username: me.username,
|
|
70
|
+
}));
|
|
71
|
+
return;
|
|
65
72
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
await this.callApi('deleteWebhook', { drop_pending_updates: false });
|
|
74
|
+
this.#pollAbort = new AbortController();
|
|
75
|
+
this.#pollPromise = runTelegramPollLoop(this, this.#pollAbort.signal);
|
|
76
|
+
logger.info(formatCompact({
|
|
77
|
+
op: 'connect',
|
|
78
|
+
endpoint: this.#options.config.name,
|
|
79
|
+
mode: 'polling',
|
|
80
|
+
username: me.username,
|
|
81
|
+
}));
|
|
72
82
|
}
|
|
73
83
|
catch (error) {
|
|
74
|
-
this.pluginLogger.error("Failed to connect Telegram bot:", error);
|
|
75
|
-
this.$connected = false;
|
|
76
|
-
throw error;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async $disconnect() {
|
|
80
|
-
try {
|
|
81
|
-
this.removeAllListeners();
|
|
82
|
-
this.chatMemberCache.clear();
|
|
83
84
|
await this.stop();
|
|
84
|
-
|
|
85
|
-
this.pluginLogger.info(`Telegram endpoint ${this.$config.name} disconnected`);
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
this.pluginLogger.error("Error disconnecting Telegram bot:", error);
|
|
85
|
+
logger.error('Failed to connect Telegram bot:', error);
|
|
89
86
|
throw error;
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
chatMemberCache = new Map();
|
|
95
|
-
sweepChatMemberCache(now) {
|
|
96
|
-
const ttl = TelegramEndpoint.CHAT_MEMBER_CACHE_TTL_MS;
|
|
97
|
-
for (const [key, entry] of this.chatMemberCache) {
|
|
98
|
-
if (now - entry.at >= ttl)
|
|
99
|
-
this.chatMemberCache.delete(key);
|
|
100
|
-
}
|
|
101
|
-
if (this.chatMemberCache.size > TelegramEndpoint.CHAT_MEMBER_CACHE_MAX) {
|
|
102
|
-
const excess = this.chatMemberCache.size - TelegramEndpoint.CHAT_MEMBER_CACHE_MAX;
|
|
103
|
-
let removed = 0;
|
|
104
|
-
for (const [key] of this.chatMemberCache) {
|
|
105
|
-
if (removed >= excess)
|
|
106
|
-
break;
|
|
107
|
-
this.chatMemberCache.delete(key);
|
|
108
|
-
removed++;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
89
|
+
open() {
|
|
90
|
+
this.#open = true;
|
|
111
91
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const now = Date.now();
|
|
119
|
-
this.sweepChatMemberCache(now);
|
|
120
|
-
const cached = this.chatMemberCache.get(key);
|
|
121
|
-
if (cached && now - cached.at < TelegramEndpoint.CHAT_MEMBER_CACHE_TTL_MS) {
|
|
122
|
-
message.$sender.role = cached.role;
|
|
123
|
-
message.$sender.permissions = cached.permissions;
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
92
|
+
close() {
|
|
93
|
+
this.#open = false;
|
|
94
|
+
}
|
|
95
|
+
async stop() {
|
|
96
|
+
this.#open = false;
|
|
97
|
+
this.#pollAbort?.abort();
|
|
126
98
|
try {
|
|
127
|
-
|
|
128
|
-
const normalized = normalizeTelegramChatMember(member);
|
|
129
|
-
this.chatMemberCache.set(key, { at: now, ...normalized });
|
|
130
|
-
message.$sender.role = normalized.role;
|
|
131
|
-
message.$sender.permissions = normalized.permissions;
|
|
99
|
+
await this.#pollPromise;
|
|
132
100
|
}
|
|
133
101
|
catch {
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
102
|
+
/* poll loop exit */
|
|
103
|
+
}
|
|
104
|
+
for (const release of this.#routeReleases.splice(0))
|
|
105
|
+
release();
|
|
106
|
+
this.#unregisterAgent?.();
|
|
107
|
+
this.#unregisterAgent = undefined;
|
|
108
|
+
this.#chatMemberCache.clear();
|
|
109
|
+
this.#started = false;
|
|
110
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
111
|
+
}
|
|
112
|
+
async send({ target, payload }) {
|
|
113
|
+
const actions = formatOutboundActions(target, payload);
|
|
114
|
+
let lastId = '';
|
|
115
|
+
for (const action of actions) {
|
|
116
|
+
const result = await this.callApi(action.method, action.params);
|
|
117
|
+
if (result.message_id != null)
|
|
118
|
+
lastId = String(result.message_id);
|
|
119
|
+
}
|
|
120
|
+
return lastId || `telegram-${Date.now()}`;
|
|
121
|
+
}
|
|
122
|
+
/** Test / internal: admit a message when open. */
|
|
123
|
+
admit(msg) {
|
|
124
|
+
if (!this.#open)
|
|
139
125
|
return;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const channelType = msg.chat.type === "private" ? "private" : "group";
|
|
148
|
-
const channelId = msg.chat.id.toString();
|
|
149
|
-
// Parse message content
|
|
150
|
-
const wire = this.parseMessageContent(msg);
|
|
151
|
-
const quoteId = Message.quoteIdFromContent(wire);
|
|
152
|
-
Message.alignReplySegments(wire, quoteId);
|
|
153
|
-
const content = toCanonicalSegments(wire);
|
|
154
|
-
const result = Message.from(msg, {
|
|
155
|
-
$id: msg.message_id.toString(),
|
|
156
|
-
$adapter: "telegram",
|
|
157
|
-
$endpoint: this.$config.name,
|
|
158
|
-
$sender: {
|
|
159
|
-
id: msg.from?.id.toString() || "",
|
|
160
|
-
name: msg.from?.username || msg.from?.first_name || "Unknown",
|
|
161
|
-
},
|
|
162
|
-
$channel: {
|
|
163
|
-
id: channelId,
|
|
164
|
-
type: channelType,
|
|
165
|
-
},
|
|
166
|
-
$content: content,
|
|
167
|
-
$quote_id: quoteId,
|
|
168
|
-
$raw: "text" in msg ? msg.text || "" : "",
|
|
169
|
-
$timestamp: msg.date * 1000,
|
|
170
|
-
$recall: async () => {
|
|
171
|
-
try {
|
|
172
|
-
await this.telegram.deleteMessage(parseInt(channelId), parseInt(result.$id));
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
this.pluginLogger.error("Error recalling Telegram message:", error);
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
$reply: async (content, quote) => {
|
|
180
|
-
if (!Array.isArray(content))
|
|
181
|
-
content = [content];
|
|
182
|
-
// Handle reply
|
|
183
|
-
if (quote) {
|
|
184
|
-
const replyToMessageId = typeof quote === "boolean" ? result.$id : quote;
|
|
185
|
-
content.unshift({ type: "reply", data: { id: replyToMessageId } });
|
|
186
|
-
}
|
|
187
|
-
return await this.adapter.sendMessage({
|
|
188
|
-
context: "telegram",
|
|
189
|
-
endpoint: this.$config.name,
|
|
190
|
-
id: channelId,
|
|
191
|
-
type: channelType,
|
|
192
|
-
content: content,
|
|
193
|
-
});
|
|
194
|
-
},
|
|
126
|
+
const { channelId } = resolveChannel(msg);
|
|
127
|
+
void this.#admitWithSenderRole(msg, channelId).catch((err) => {
|
|
128
|
+
logger.warn(formatCompact({
|
|
129
|
+
op: 'telegram_gateway_receive_failed',
|
|
130
|
+
target: channelId,
|
|
131
|
+
error: err instanceof Error ? err.message : String(err),
|
|
132
|
+
}));
|
|
195
133
|
});
|
|
196
|
-
this.messageChatMap.set(result.$id, channelId);
|
|
197
|
-
return result;
|
|
198
134
|
}
|
|
199
|
-
async
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
time: msg.date,
|
|
219
|
-
};
|
|
220
|
-
this.quotedPayloadCache.set(payload.messageId, payload);
|
|
221
|
-
}
|
|
222
|
-
$formatCallbackQuery(ctx) {
|
|
223
|
-
if (!ctx.callbackQuery || !("data" in ctx.callbackQuery)) {
|
|
224
|
-
throw new Error("Invalid callback query");
|
|
225
|
-
}
|
|
226
|
-
const query = ctx.callbackQuery;
|
|
227
|
-
const msg = query.message;
|
|
228
|
-
const channelType = msg && "chat" in msg && msg.chat.type === "private" ? "private" : "group";
|
|
229
|
-
const channelId = msg && "chat" in msg ? msg.chat.id.toString() : query.from.id.toString();
|
|
230
|
-
const result = Message.from(query, {
|
|
231
|
-
$id: query.id,
|
|
232
|
-
$adapter: "telegram",
|
|
233
|
-
$endpoint: this.$config.name,
|
|
234
|
-
$sender: {
|
|
235
|
-
id: query.from.id.toString(),
|
|
236
|
-
name: query.from.username || query.from.first_name,
|
|
237
|
-
},
|
|
238
|
-
$channel: {
|
|
239
|
-
id: channelId,
|
|
240
|
-
type: channelType,
|
|
241
|
-
},
|
|
242
|
-
$content: [{
|
|
243
|
-
type: "action",
|
|
244
|
-
data: {
|
|
245
|
-
id: query.id,
|
|
246
|
-
payload: query.data,
|
|
247
|
-
sourceMessageId: msg && "message_id" in msg ? String(msg.message_id) : undefined,
|
|
248
|
-
},
|
|
249
|
-
}],
|
|
250
|
-
$raw: query.data,
|
|
251
|
-
$timestamp: Date.now(),
|
|
252
|
-
$recall: async () => {
|
|
253
|
-
// Callback queries cannot be recalled
|
|
254
|
-
},
|
|
255
|
-
$reply: async (content) => {
|
|
256
|
-
if (!Array.isArray(content))
|
|
257
|
-
content = [content];
|
|
258
|
-
const sentMsg = await this.sendContentToChat(parseInt(channelId), content);
|
|
259
|
-
return sentMsg.message_id.toString();
|
|
260
|
-
},
|
|
135
|
+
async #admitWithSenderRole(msg, channelId) {
|
|
136
|
+
const permit = await this.#resolveGroupSenderPermit(msg);
|
|
137
|
+
// 新 Runtime Message.content 为纯文本:@ 本机只能经 metadata 传递
|
|
138
|
+
const mentioned = this.#isBotMentioned(msg);
|
|
139
|
+
await this.#options.gateway.receive({
|
|
140
|
+
adapter: this.#options.id,
|
|
141
|
+
target: channelId,
|
|
142
|
+
content: formatInboundContent(msg),
|
|
143
|
+
sender: senderDisplayName(msg.from),
|
|
144
|
+
id: String(msg.message_id),
|
|
145
|
+
metadata: Object.freeze({
|
|
146
|
+
endpoint: this.#options.config.name,
|
|
147
|
+
chatType: msg.chat.type,
|
|
148
|
+
userId: msg.from?.id,
|
|
149
|
+
date: msg.date,
|
|
150
|
+
...(permit?.role ? { senderRole: permit.role } : {}),
|
|
151
|
+
...(permit?.permissions.length ? { senderPermissions: [...permit.permissions] } : {}),
|
|
152
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
153
|
+
}),
|
|
261
154
|
});
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
264
|
-
parseMessageContent(msg) {
|
|
265
|
-
const segments = [];
|
|
266
|
-
// Handle text messages
|
|
267
|
-
if ("text" in msg && msg.text) {
|
|
268
|
-
// Check for reply
|
|
269
|
-
if (msg.reply_to_message) {
|
|
270
|
-
this.cacheQuotedTelegramMessage(msg.reply_to_message);
|
|
271
|
-
const replyId = msg.reply_to_message.message_id.toString();
|
|
272
|
-
segments.push({
|
|
273
|
-
type: "reply",
|
|
274
|
-
data: { id: replyId, message_id: replyId },
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
// Parse text with entities
|
|
278
|
-
if (msg.entities && msg.entities.length > 0) {
|
|
279
|
-
segments.push(...this.parseTextWithEntities(msg.text, msg.entities));
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
segments.push({ type: "text", data: { text: msg.text } });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// Handle photo
|
|
286
|
-
if ("photo" in msg && msg.photo) {
|
|
287
|
-
const largestPhoto = msg.photo[msg.photo.length - 1];
|
|
288
|
-
segments.push({
|
|
289
|
-
type: "image",
|
|
290
|
-
data: {
|
|
291
|
-
file_id: largestPhoto.file_id,
|
|
292
|
-
file_unique_id: largestPhoto.file_unique_id,
|
|
293
|
-
width: largestPhoto.width,
|
|
294
|
-
height: largestPhoto.height,
|
|
295
|
-
file_size: largestPhoto.file_size,
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
if (msg.caption) {
|
|
299
|
-
segments.push({ type: "text", data: { text: msg.caption } });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
// Handle video
|
|
303
|
-
if ("video" in msg && msg.video) {
|
|
304
|
-
segments.push({
|
|
305
|
-
type: "video",
|
|
306
|
-
data: {
|
|
307
|
-
file_id: msg.video.file_id,
|
|
308
|
-
file_unique_id: msg.video.file_unique_id,
|
|
309
|
-
width: msg.video.width,
|
|
310
|
-
height: msg.video.height,
|
|
311
|
-
duration: msg.video.duration,
|
|
312
|
-
file_size: msg.video.file_size,
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
if (msg.caption) {
|
|
316
|
-
segments.push({ type: "text", data: { text: msg.caption } });
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// Handle audio
|
|
320
|
-
if ("audio" in msg && msg.audio) {
|
|
321
|
-
segments.push({
|
|
322
|
-
type: "audio",
|
|
323
|
-
data: {
|
|
324
|
-
file_id: msg.audio.file_id,
|
|
325
|
-
file_unique_id: msg.audio.file_unique_id,
|
|
326
|
-
duration: msg.audio.duration,
|
|
327
|
-
performer: msg.audio.performer,
|
|
328
|
-
title: msg.audio.title,
|
|
329
|
-
file_size: msg.audio.file_size,
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
// Handle voice
|
|
334
|
-
if ("voice" in msg && msg.voice) {
|
|
335
|
-
segments.push({
|
|
336
|
-
type: "voice",
|
|
337
|
-
data: {
|
|
338
|
-
file_id: msg.voice.file_id,
|
|
339
|
-
file_unique_id: msg.voice.file_unique_id,
|
|
340
|
-
duration: msg.voice.duration,
|
|
341
|
-
file_size: msg.voice.file_size,
|
|
342
|
-
},
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
// Handle document
|
|
346
|
-
if ("document" in msg && msg.document) {
|
|
347
|
-
segments.push({
|
|
348
|
-
type: "file",
|
|
349
|
-
data: {
|
|
350
|
-
file_id: msg.document.file_id,
|
|
351
|
-
file_unique_id: msg.document.file_unique_id,
|
|
352
|
-
file_name: msg.document.file_name,
|
|
353
|
-
mime_type: msg.document.mime_type,
|
|
354
|
-
file_size: msg.document.file_size,
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
// Handle sticker
|
|
359
|
-
if ("sticker" in msg && msg.sticker) {
|
|
360
|
-
segments.push({
|
|
361
|
-
type: "sticker",
|
|
362
|
-
data: {
|
|
363
|
-
file_id: msg.sticker.file_id,
|
|
364
|
-
file_unique_id: msg.sticker.file_unique_id,
|
|
365
|
-
width: msg.sticker.width,
|
|
366
|
-
height: msg.sticker.height,
|
|
367
|
-
is_animated: msg.sticker.is_animated,
|
|
368
|
-
is_video: msg.sticker.is_video,
|
|
369
|
-
emoji: msg.sticker.emoji,
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
// Handle location
|
|
374
|
-
if ("location" in msg && msg.location) {
|
|
375
|
-
segments.push({
|
|
376
|
-
type: "location",
|
|
377
|
-
data: {
|
|
378
|
-
longitude: msg.location.longitude,
|
|
379
|
-
latitude: msg.location.latitude,
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
return segments.length > 0
|
|
384
|
-
? segments
|
|
385
|
-
: [{ type: "text", data: { text: "" } }];
|
|
386
155
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (entity.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
segments.push({ type: "text", data: { text: beforeText } });
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
const entityText = text.slice(entity.offset, entity.offset + entity.length);
|
|
399
|
-
switch (entity.type) {
|
|
400
|
-
case "mention":
|
|
401
|
-
case "text_mention":
|
|
402
|
-
segments.push({
|
|
403
|
-
type: "at",
|
|
404
|
-
data: {
|
|
405
|
-
id: ("user" in entity && entity.user?.id.toString()) || entityText.slice(1),
|
|
406
|
-
name: ("user" in entity && entity.user?.username) || entityText,
|
|
407
|
-
text: entityText,
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
break;
|
|
411
|
-
case "url":
|
|
412
|
-
case "text_link":
|
|
413
|
-
segments.push({
|
|
414
|
-
type: "link",
|
|
415
|
-
data: {
|
|
416
|
-
url: ("url" in entity && entity.url) || entityText,
|
|
417
|
-
text: entityText,
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
break;
|
|
421
|
-
case "hashtag":
|
|
422
|
-
segments.push({
|
|
423
|
-
type: "text",
|
|
424
|
-
data: { text: entityText },
|
|
425
|
-
});
|
|
426
|
-
break;
|
|
427
|
-
case "bold":
|
|
428
|
-
case "italic":
|
|
429
|
-
case "code":
|
|
430
|
-
case "pre":
|
|
431
|
-
case "underline":
|
|
432
|
-
case "strikethrough":
|
|
433
|
-
segments.push({
|
|
434
|
-
type: "text",
|
|
435
|
-
data: { text: entityText, format: entity.type },
|
|
436
|
-
});
|
|
437
|
-
break;
|
|
438
|
-
default:
|
|
439
|
-
segments.push({ type: "text", data: { text: entityText } });
|
|
440
|
-
}
|
|
441
|
-
lastOffset = entity.offset + entity.length;
|
|
442
|
-
}
|
|
443
|
-
// Add remaining text
|
|
444
|
-
if (lastOffset < text.length) {
|
|
445
|
-
const remainingText = text.slice(lastOffset);
|
|
446
|
-
if (remainingText) {
|
|
447
|
-
segments.push({ type: "text", data: { text: remainingText } });
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return segments;
|
|
451
|
-
}
|
|
452
|
-
async $sendMessage(options) {
|
|
453
|
-
try {
|
|
454
|
-
const chatId = parseInt(options.id);
|
|
455
|
-
const canonical = expandInteractiveSegmentsInContent(options.content);
|
|
456
|
-
const wire = fromCanonicalSegments(canonical);
|
|
457
|
-
const result = await this.sendContentToChat(chatId, wire);
|
|
458
|
-
this.messageChatMap.set(result.message_id.toString(), options.id);
|
|
459
|
-
this.pluginLogger.debug(`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`);
|
|
460
|
-
return result.message_id.toString();
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
this.pluginLogger.error("Failed to send Telegram message:", error);
|
|
464
|
-
throw error;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
async $editMessage(options) {
|
|
468
|
-
const chatId = parseInt(options.id);
|
|
469
|
-
const messageId = parseInt(options.messageId);
|
|
470
|
-
const content = Array.isArray(options.content) ? options.content : [options.content];
|
|
471
|
-
let textContent = "";
|
|
472
|
-
let keyboard;
|
|
473
|
-
for (const seg of content) {
|
|
474
|
-
if (typeof seg === "string") {
|
|
475
|
-
textContent += seg;
|
|
156
|
+
/** entities 里 mention 文本命中 bot username(getMe 缓存),或 text_mention 指向 bot 用户。 */
|
|
157
|
+
#isBotMentioned(msg) {
|
|
158
|
+
if (!msg.entities?.length)
|
|
159
|
+
return false;
|
|
160
|
+
for (const entity of msg.entities) {
|
|
161
|
+
if (entity.type === 'text_mention') {
|
|
162
|
+
if (this.#botUserId != null && entity.user?.id === this.#botUserId)
|
|
163
|
+
return true;
|
|
476
164
|
continue;
|
|
477
165
|
}
|
|
478
|
-
if (
|
|
479
|
-
textContent += seg.data.text || "";
|
|
480
|
-
if (seg.type === "keyboard") {
|
|
481
|
-
keyboard = {
|
|
482
|
-
inline_keyboard: (seg.data.rows ?? []).map((row) => row.map((btn) => ({
|
|
483
|
-
text: btn.label,
|
|
484
|
-
callback_data: String(btn.payload).slice(0, 64),
|
|
485
|
-
}))),
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
await this.telegram.editMessageText(chatId, messageId, undefined, textContent.trim() || " ", keyboard ? { reply_markup: keyboard } : {});
|
|
490
|
-
}
|
|
491
|
-
async sendContentToChat(chatId, content, extraOptions = {}) {
|
|
492
|
-
if (!Array.isArray(content))
|
|
493
|
-
content = [content];
|
|
494
|
-
let textContent = "";
|
|
495
|
-
let hasMedia = false;
|
|
496
|
-
for (const segment of content) {
|
|
497
|
-
if (typeof segment === "string") {
|
|
498
|
-
textContent += segment;
|
|
166
|
+
if (entity.type !== 'mention' || !this.#botUsername)
|
|
499
167
|
continue;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
case "text":
|
|
504
|
-
textContent += data.text || "";
|
|
505
|
-
break;
|
|
506
|
-
case "at":
|
|
507
|
-
if (data.id) {
|
|
508
|
-
textContent += `@${data.name || data.id}`;
|
|
509
|
-
}
|
|
510
|
-
break;
|
|
511
|
-
case "image":
|
|
512
|
-
hasMedia = true;
|
|
513
|
-
if (data.file_id) {
|
|
514
|
-
// Send by file_id
|
|
515
|
-
return await this.telegram.sendPhoto(chatId, data.file_id, {
|
|
516
|
-
caption: textContent || undefined,
|
|
517
|
-
...extraOptions,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
else if (data.url) {
|
|
521
|
-
// Send by URL
|
|
522
|
-
return await this.telegram.sendPhoto(chatId, data.url, {
|
|
523
|
-
caption: textContent || undefined,
|
|
524
|
-
...extraOptions,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
else if (data.file) {
|
|
528
|
-
// Send by file path
|
|
529
|
-
return await this.telegram.sendPhoto(chatId, { source: data.file }, {
|
|
530
|
-
caption: textContent || undefined,
|
|
531
|
-
...extraOptions,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
break;
|
|
535
|
-
case "video":
|
|
536
|
-
hasMedia = true;
|
|
537
|
-
if (data.file_id) {
|
|
538
|
-
return await this.telegram.sendVideo(chatId, data.file_id, {
|
|
539
|
-
caption: textContent || undefined,
|
|
540
|
-
...extraOptions,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
else if (data.url) {
|
|
544
|
-
return await this.telegram.sendVideo(chatId, data.url, {
|
|
545
|
-
caption: textContent || undefined,
|
|
546
|
-
...extraOptions,
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
else if (data.file) {
|
|
550
|
-
return await this.telegram.sendVideo(chatId, { source: data.file }, {
|
|
551
|
-
caption: textContent || undefined,
|
|
552
|
-
...extraOptions,
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
break;
|
|
556
|
-
case "audio":
|
|
557
|
-
hasMedia = true;
|
|
558
|
-
if (data.file_id) {
|
|
559
|
-
return await this.telegram.sendAudio(chatId, data.file_id, {
|
|
560
|
-
caption: textContent || undefined,
|
|
561
|
-
...extraOptions,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
else if (data.url) {
|
|
565
|
-
return await this.telegram.sendAudio(chatId, data.url, {
|
|
566
|
-
caption: textContent || undefined,
|
|
567
|
-
...extraOptions,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
else if (data.file) {
|
|
571
|
-
return await this.telegram.sendAudio(chatId, { source: data.file }, {
|
|
572
|
-
caption: textContent || undefined,
|
|
573
|
-
...extraOptions,
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
break;
|
|
577
|
-
case "voice":
|
|
578
|
-
hasMedia = true;
|
|
579
|
-
if (data.file_id) {
|
|
580
|
-
return await this.telegram.sendVoice(chatId, data.file_id, {
|
|
581
|
-
caption: textContent || undefined,
|
|
582
|
-
...extraOptions,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
else if (data.url) {
|
|
586
|
-
return await this.telegram.sendVoice(chatId, data.url, {
|
|
587
|
-
caption: textContent || undefined,
|
|
588
|
-
...extraOptions,
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
else if (data.file) {
|
|
592
|
-
return await this.telegram.sendVoice(chatId, { source: data.file }, {
|
|
593
|
-
caption: textContent || undefined,
|
|
594
|
-
...extraOptions,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
break;
|
|
598
|
-
case "file":
|
|
599
|
-
hasMedia = true;
|
|
600
|
-
if (data.file_id) {
|
|
601
|
-
return await this.telegram.sendDocument(chatId, data.file_id, {
|
|
602
|
-
caption: textContent || undefined,
|
|
603
|
-
...extraOptions,
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
else if (data.url) {
|
|
607
|
-
return await this.telegram.sendDocument(chatId, data.url, {
|
|
608
|
-
caption: textContent || undefined,
|
|
609
|
-
...extraOptions,
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
else if (data.file) {
|
|
613
|
-
return await this.telegram.sendDocument(chatId, { source: data.file }, {
|
|
614
|
-
caption: textContent || undefined,
|
|
615
|
-
...extraOptions,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
case "sticker":
|
|
620
|
-
if (data.file_id) {
|
|
621
|
-
hasMedia = true;
|
|
622
|
-
return await this.telegram.sendSticker(chatId, data.file_id, extraOptions);
|
|
623
|
-
}
|
|
624
|
-
break;
|
|
625
|
-
case "location":
|
|
626
|
-
return await this.telegram.sendLocation(chatId, data.latitude, data.longitude, extraOptions);
|
|
627
|
-
case "reply":
|
|
628
|
-
return await this.telegram.sendMessage(chatId, data.id, {
|
|
629
|
-
reply_parameters: { message_id: parseInt(data.id) },
|
|
630
|
-
...extraOptions,
|
|
631
|
-
});
|
|
632
|
-
case "keyboard": {
|
|
633
|
-
const rows = (data.rows ?? []).map((row) => row.map((btn) => ({
|
|
634
|
-
text: btn.label,
|
|
635
|
-
callback_data: String(btn.payload).slice(0, 64),
|
|
636
|
-
})));
|
|
637
|
-
return await this.telegram.sendMessage(chatId, textContent.trim() || " ", {
|
|
638
|
-
reply_markup: { inline_keyboard: rows },
|
|
639
|
-
...extraOptions,
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
default:
|
|
643
|
-
// Unknown segment type, add as text
|
|
644
|
-
textContent += data.text || `[${type}]`;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
// If no media was sent, send as text message
|
|
648
|
-
if (!hasMedia && textContent.trim()) {
|
|
649
|
-
return await this.telegram.sendMessage(chatId, textContent.trim(), extraOptions);
|
|
650
|
-
}
|
|
651
|
-
// If neither media nor text was sent, this is an error
|
|
652
|
-
throw new Error("TelegramEndpoint.$sendMessage: No media or text content to send.");
|
|
653
|
-
}
|
|
654
|
-
async $recallMessage(id) {
|
|
655
|
-
// The Endpoint interface only provides message_id, making recall impossible
|
|
656
|
-
// Users should use message.$recall() instead, which has the full context
|
|
657
|
-
throw new Error("TelegramEndpoint.$recallMessage: Message recall not supported without chat_id. " +
|
|
658
|
-
"Use message.$recall() method instead, which contains the required context.");
|
|
659
|
-
}
|
|
660
|
-
resolveTelegramMessageRef(messageId) {
|
|
661
|
-
if (messageId.includes(':')) {
|
|
662
|
-
const [chatIdRaw, msgIdRaw] = messageId.split(':');
|
|
663
|
-
const chatId = Number(chatIdRaw);
|
|
664
|
-
const msgId = Number(msgIdRaw);
|
|
665
|
-
if (Number.isFinite(chatId) && Number.isFinite(msgId)) {
|
|
666
|
-
this.messageChatMap.set(String(msgId), String(chatId));
|
|
667
|
-
return { chatId, msgId };
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
const mappedChatId = this.messageChatMap.get(messageId);
|
|
671
|
-
if (!mappedChatId)
|
|
672
|
-
return null;
|
|
673
|
-
const chatId = Number(mappedChatId);
|
|
674
|
-
const msgId = Number(messageId);
|
|
675
|
-
if (!Number.isFinite(chatId) || !Number.isFinite(msgId))
|
|
676
|
-
return null;
|
|
677
|
-
return { chatId, msgId };
|
|
678
|
-
}
|
|
679
|
-
async $addReaction(messageId, emoji) {
|
|
680
|
-
const ref = this.resolveTelegramMessageRef(messageId);
|
|
681
|
-
if (!ref) {
|
|
682
|
-
this.pluginLogger.warn(`Telegram Endpoint ${this.$id} 无法根据 message_id=${messageId} 定位 chat_id,跳过 addReaction`);
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
await this.setMessageReaction(ref.chatId, ref.msgId, emoji);
|
|
687
|
-
return emoji;
|
|
688
|
-
}
|
|
689
|
-
catch (error) {
|
|
690
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 添加 reaction 失败:`, error);
|
|
691
|
-
return null;
|
|
168
|
+
const slice = (msg.text ?? '').slice(entity.offset, entity.offset + entity.length);
|
|
169
|
+
if (slice.toLowerCase() === `@${this.#botUsername.toLowerCase()}`)
|
|
170
|
+
return true;
|
|
692
171
|
}
|
|
172
|
+
return false;
|
|
693
173
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (!
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
174
|
+
/** 群消息 sender role 解析:getChatMember + 60s 缓存(对齐旧 enrichGroupSender)。 */
|
|
175
|
+
async #resolveGroupSenderPermit(msg) {
|
|
176
|
+
if (msg.chat.type === 'private' || !msg.from?.id)
|
|
177
|
+
return undefined;
|
|
178
|
+
const chatId = Number(msg.chat.id);
|
|
179
|
+
const userId = msg.from.id;
|
|
180
|
+
const key = `${chatId}:${userId}`;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
this.#sweepChatMemberCache(now);
|
|
183
|
+
const cached = this.#chatMemberCache.get(key);
|
|
184
|
+
if (cached && now - cached.at < CHAT_MEMBER_CACHE_TTL_MS)
|
|
185
|
+
return cached;
|
|
700
186
|
try {
|
|
701
|
-
await this.
|
|
702
|
-
chat_id:
|
|
703
|
-
|
|
704
|
-
reaction: [],
|
|
187
|
+
const member = await this.callApi('getChatMember', {
|
|
188
|
+
chat_id: chatId,
|
|
189
|
+
user_id: userId,
|
|
705
190
|
});
|
|
191
|
+
const normalized = normalizeTelegramChatMember(member);
|
|
192
|
+
const entry = { at: now, ...normalized };
|
|
193
|
+
this.#chatMemberCache.set(key, entry);
|
|
194
|
+
return entry;
|
|
706
195
|
}
|
|
707
|
-
catch
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
// ==================== 群组管理 API ====================
|
|
712
|
-
/**
|
|
713
|
-
* 踢出用户
|
|
714
|
-
* @param chatId 聊天 ID
|
|
715
|
-
* @param userId 用户 ID
|
|
716
|
-
* @param untilDate 封禁截止时间(Unix 时间戳),0 表示永久
|
|
717
|
-
*/
|
|
718
|
-
async kickMember(chatId, userId, untilDate) {
|
|
719
|
-
try {
|
|
720
|
-
await this.telegram.banChatMember(chatId, userId, untilDate);
|
|
721
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 踢出用户 ${userId} 从聊天 ${chatId}`);
|
|
722
|
-
return true;
|
|
723
|
-
}
|
|
724
|
-
catch (error) {
|
|
725
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 踢出用户失败:`, error);
|
|
726
|
-
throw error;
|
|
196
|
+
catch {
|
|
197
|
+
// 保守拒绝:无角色快照
|
|
198
|
+
return undefined;
|
|
727
199
|
}
|
|
728
200
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
*/
|
|
734
|
-
async unbanMember(chatId, userId) {
|
|
735
|
-
try {
|
|
736
|
-
await this.telegram.unbanChatMember(chatId, userId, { only_if_banned: true });
|
|
737
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 解除用户 ${userId} 的封禁(聊天 ${chatId})`);
|
|
738
|
-
return true;
|
|
201
|
+
#sweepChatMemberCache(now) {
|
|
202
|
+
for (const [key, entry] of this.#chatMemberCache) {
|
|
203
|
+
if (now - entry.at >= CHAT_MEMBER_CACHE_TTL_MS)
|
|
204
|
+
this.#chatMemberCache.delete(key);
|
|
739
205
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
206
|
+
if (this.#chatMemberCache.size > CHAT_MEMBER_CACHE_MAX) {
|
|
207
|
+
const excess = this.#chatMemberCache.size - CHAT_MEMBER_CACHE_MAX;
|
|
208
|
+
let removed = 0;
|
|
209
|
+
for (const [key] of this.#chatMemberCache) {
|
|
210
|
+
if (removed >= excess)
|
|
211
|
+
break;
|
|
212
|
+
this.#chatMemberCache.delete(key);
|
|
213
|
+
removed++;
|
|
214
|
+
}
|
|
743
215
|
}
|
|
744
216
|
}
|
|
745
|
-
/**
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
217
|
+
/** Test / internal: admit a callback query when open. */
|
|
218
|
+
admitCallback(query) {
|
|
219
|
+
if (!this.#open)
|
|
220
|
+
return;
|
|
221
|
+
const msg = query.message;
|
|
222
|
+
const channelId = msg ? resolveChannel(msg).channelId : String(query.from.id);
|
|
223
|
+
void this.#options.gateway.receive({
|
|
224
|
+
adapter: this.#options.id,
|
|
225
|
+
target: channelId,
|
|
226
|
+
content: formatCallbackContent(query),
|
|
227
|
+
sender: senderDisplayName(query.from),
|
|
228
|
+
id: query.id,
|
|
229
|
+
metadata: Object.freeze({
|
|
230
|
+
endpoint: this.#options.config.name,
|
|
231
|
+
eventType: 'callback_query',
|
|
232
|
+
payload: query.data,
|
|
233
|
+
sourceMessageId: msg ? String(msg.message_id) : undefined,
|
|
234
|
+
}),
|
|
235
|
+
}).catch((err) => {
|
|
236
|
+
logger.warn(formatCompact({
|
|
237
|
+
op: 'telegram_gateway_receive_failed',
|
|
238
|
+
target: channelId,
|
|
239
|
+
error: err instanceof Error ? err.message : String(err),
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
765
242
|
}
|
|
766
|
-
/**
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
*/
|
|
772
|
-
async muteMember(chatId, userId, duration = 600) {
|
|
773
|
-
try {
|
|
774
|
-
if (duration === 0) {
|
|
775
|
-
// 解除禁言 - 恢复发送消息权限
|
|
776
|
-
await this.telegram.restrictChatMember(chatId, userId, {
|
|
777
|
-
permissions: {
|
|
778
|
-
can_send_messages: true,
|
|
779
|
-
can_send_audios: true,
|
|
780
|
-
can_send_documents: true,
|
|
781
|
-
can_send_photos: true,
|
|
782
|
-
can_send_videos: true,
|
|
783
|
-
can_send_video_notes: true,
|
|
784
|
-
can_send_voice_notes: true,
|
|
785
|
-
can_send_polls: true,
|
|
786
|
-
can_send_other_messages: true,
|
|
787
|
-
can_add_web_page_previews: true,
|
|
788
|
-
},
|
|
789
|
-
});
|
|
790
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 解除用户 ${userId} 禁言(聊天 ${chatId})`);
|
|
791
|
-
}
|
|
792
|
-
else {
|
|
793
|
-
const untilDate = Math.floor(Date.now() / 1000) + duration;
|
|
794
|
-
await this.telegram.restrictChatMember(chatId, userId, {
|
|
795
|
-
permissions: {
|
|
796
|
-
can_send_messages: false,
|
|
797
|
-
can_send_audios: false,
|
|
798
|
-
can_send_documents: false,
|
|
799
|
-
can_send_photos: false,
|
|
800
|
-
can_send_videos: false,
|
|
801
|
-
can_send_video_notes: false,
|
|
802
|
-
can_send_voice_notes: false,
|
|
803
|
-
can_send_polls: false,
|
|
804
|
-
can_send_other_messages: false,
|
|
805
|
-
can_add_web_page_previews: false,
|
|
806
|
-
},
|
|
807
|
-
until_date: untilDate,
|
|
808
|
-
});
|
|
809
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 禁言用户 ${userId} ${duration}秒(聊天 ${chatId})`);
|
|
810
|
-
}
|
|
811
|
-
return true;
|
|
812
|
-
}
|
|
813
|
-
catch (error) {
|
|
814
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 禁言操作失败:`, error);
|
|
815
|
-
throw error;
|
|
243
|
+
/** Used by webhook / polling handlers. */
|
|
244
|
+
handleUpdate(update) {
|
|
245
|
+
if (update.message) {
|
|
246
|
+
this.admit(update.message);
|
|
247
|
+
return;
|
|
816
248
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
* @param promote 是否提升为管理员
|
|
823
|
-
*/
|
|
824
|
-
async setAdmin(chatId, userId, promote = true) {
|
|
825
|
-
try {
|
|
826
|
-
if (promote) {
|
|
827
|
-
await this.telegram.promoteChatMember(chatId, userId, {
|
|
828
|
-
can_manage_chat: true,
|
|
829
|
-
can_delete_messages: true,
|
|
830
|
-
can_manage_video_chats: true,
|
|
831
|
-
can_restrict_members: true,
|
|
832
|
-
can_promote_members: false,
|
|
833
|
-
can_change_info: true,
|
|
834
|
-
can_invite_users: true,
|
|
835
|
-
can_pin_messages: true,
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
await this.telegram.promoteChatMember(chatId, userId, {
|
|
840
|
-
can_manage_chat: false,
|
|
841
|
-
can_delete_messages: false,
|
|
842
|
-
can_manage_video_chats: false,
|
|
843
|
-
can_restrict_members: false,
|
|
844
|
-
can_promote_members: false,
|
|
845
|
-
can_change_info: false,
|
|
846
|
-
can_invite_users: false,
|
|
847
|
-
can_pin_messages: false,
|
|
249
|
+
if (update.callback_query) {
|
|
250
|
+
const query = update.callback_query;
|
|
251
|
+
if (query.data) {
|
|
252
|
+
void this.callApi('answerCallbackQuery', { callback_query_id: query.id }).catch(() => {
|
|
253
|
+
/* already answered */
|
|
848
254
|
});
|
|
849
255
|
}
|
|
850
|
-
this.
|
|
851
|
-
return true;
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 设置管理员失败:`, error);
|
|
855
|
-
throw error;
|
|
256
|
+
this.admitCallback(query);
|
|
856
257
|
}
|
|
857
258
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
259
|
+
async callApi(method, params = {}, signal) {
|
|
260
|
+
const url = botApiUrl(this.#options.config, method);
|
|
261
|
+
const response = await this.#fetch(url, {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify(params),
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
const text = await response.text();
|
|
268
|
+
let body;
|
|
864
269
|
try {
|
|
865
|
-
|
|
866
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 设置聊天 ${chatId} 标题为 "${title}"`);
|
|
867
|
-
return true;
|
|
270
|
+
body = JSON.parse(text);
|
|
868
271
|
}
|
|
869
|
-
catch
|
|
870
|
-
|
|
871
|
-
throw error;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* 设置聊天描述
|
|
876
|
-
* @param chatId 聊天 ID
|
|
877
|
-
* @param description 新描述
|
|
878
|
-
*/
|
|
879
|
-
async setChatDescription(chatId, description) {
|
|
880
|
-
try {
|
|
881
|
-
await this.telegram.setChatDescription(chatId, description);
|
|
882
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 设置聊天 ${chatId} 描述`);
|
|
883
|
-
return true;
|
|
272
|
+
catch {
|
|
273
|
+
throw new Error(`Telegram API ${method} invalid JSON (${response.status}): ${text.slice(0, 200)}`);
|
|
884
274
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
throw error;
|
|
275
|
+
if (!body.ok) {
|
|
276
|
+
throw new Error(`Telegram API ${method} failed (${body.error_code ?? response.status}): ${body.description ?? text}`);
|
|
888
277
|
}
|
|
278
|
+
return body.result;
|
|
889
279
|
}
|
|
890
|
-
|
|
891
|
-
* 置顶消息
|
|
892
|
-
* @param chatId 聊天 ID
|
|
893
|
-
* @param messageId 消息 ID
|
|
894
|
-
*/
|
|
280
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
895
281
|
async pinMessage(chatId, messageId) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 置顶消息 ${messageId}(聊天 ${chatId})`);
|
|
899
|
-
return true;
|
|
900
|
-
}
|
|
901
|
-
catch (error) {
|
|
902
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 置顶消息失败:`, error);
|
|
903
|
-
throw error;
|
|
904
|
-
}
|
|
282
|
+
await this.callApi('pinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
283
|
+
return true;
|
|
905
284
|
}
|
|
906
|
-
/**
|
|
907
|
-
* 取消置顶消息
|
|
908
|
-
* @param chatId 聊天 ID
|
|
909
|
-
* @param messageId 消息 ID(可选,不提供则取消所有置顶)
|
|
910
|
-
*/
|
|
911
285
|
async unpinMessage(chatId, messageId) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
await this.telegram.unpinChatMessage(chatId, messageId);
|
|
915
|
-
}
|
|
916
|
-
else {
|
|
917
|
-
await this.telegram.unpinAllChatMessages(chatId);
|
|
918
|
-
}
|
|
919
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 取消置顶消息(聊天 ${chatId})`);
|
|
920
|
-
return true;
|
|
286
|
+
if (messageId != null) {
|
|
287
|
+
await this.callApi('unpinChatMessage', { chat_id: chatId, message_id: messageId });
|
|
921
288
|
}
|
|
922
|
-
|
|
923
|
-
this.
|
|
924
|
-
throw error;
|
|
289
|
+
else {
|
|
290
|
+
await this.callApi('unpinAllChatMessages', { chat_id: chatId });
|
|
925
291
|
}
|
|
292
|
+
return true;
|
|
926
293
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
*/
|
|
931
|
-
async getChatInfo(chatId) {
|
|
932
|
-
try {
|
|
933
|
-
return await this.telegram.getChat(chatId);
|
|
934
|
-
}
|
|
935
|
-
catch (error) {
|
|
936
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 获取聊天信息失败:`, error);
|
|
937
|
-
throw error;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* 获取聊天成员
|
|
942
|
-
* @param chatId 聊天 ID
|
|
943
|
-
* @param userId 用户 ID
|
|
944
|
-
*/
|
|
945
|
-
async getChatMember(chatId, userId) {
|
|
946
|
-
try {
|
|
947
|
-
return await this.telegram.getChatMember(chatId, userId);
|
|
948
|
-
}
|
|
949
|
-
catch (error) {
|
|
950
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 获取成员信息失败:`, error);
|
|
951
|
-
throw error;
|
|
952
|
-
}
|
|
294
|
+
async setChatDescription(chatId, description) {
|
|
295
|
+
await this.callApi('setChatDescription', { chat_id: chatId, description });
|
|
296
|
+
return true;
|
|
953
297
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
catch (error) {
|
|
963
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 获取管理员列表失败:`, error);
|
|
964
|
-
throw error;
|
|
965
|
-
}
|
|
298
|
+
async setMessageReaction(chatId, messageId, reaction) {
|
|
299
|
+
await this.callApi('setMessageReaction', {
|
|
300
|
+
chat_id: chatId,
|
|
301
|
+
message_id: messageId,
|
|
302
|
+
reaction: [{ type: 'emoji', emoji: reaction }],
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
966
305
|
}
|
|
967
|
-
/**
|
|
968
|
-
* 获取聊天成员数量
|
|
969
|
-
* @param chatId 聊天 ID
|
|
970
|
-
*/
|
|
971
306
|
async getChatMemberCount(chatId) {
|
|
972
|
-
|
|
973
|
-
return await this.telegram.getChatMembersCount(chatId);
|
|
974
|
-
}
|
|
975
|
-
catch (error) {
|
|
976
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 获取成员数量失败:`, error);
|
|
977
|
-
throw error;
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* 创建邀请链接
|
|
982
|
-
* @param chatId 聊天 ID
|
|
983
|
-
*/
|
|
984
|
-
async createInviteLink(chatId) {
|
|
985
|
-
try {
|
|
986
|
-
const link = await this.telegram.createChatInviteLink(chatId, {});
|
|
987
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 创建邀请链接(聊天 ${chatId})`);
|
|
988
|
-
return link.invite_link;
|
|
989
|
-
}
|
|
990
|
-
catch (error) {
|
|
991
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 创建邀请链接失败:`, error);
|
|
992
|
-
throw error;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
async sendPoll(chatId, question, options, isAnonymous = true, allowsMultipleAnswers = false) {
|
|
996
|
-
try {
|
|
997
|
-
const result = await this.telegram.sendPoll(chatId, question, options, {
|
|
998
|
-
is_anonymous: isAnonymous,
|
|
999
|
-
allows_multiple_answers: allowsMultipleAnswers,
|
|
1000
|
-
});
|
|
1001
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 发送投票到 ${chatId}`);
|
|
1002
|
-
return result;
|
|
1003
|
-
}
|
|
1004
|
-
catch (error) {
|
|
1005
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 发送投票失败:`, error);
|
|
1006
|
-
throw error;
|
|
1007
|
-
}
|
|
307
|
+
return this.callApi('getChatMemberCount', { chat_id: chatId });
|
|
1008
308
|
}
|
|
1009
|
-
async
|
|
1010
|
-
|
|
1011
|
-
await this.telegram.callApi('setMessageReaction', {
|
|
1012
|
-
chat_id: chatId,
|
|
1013
|
-
message_id: messageId,
|
|
1014
|
-
reaction: [{ type: 'emoji', emoji: reaction }],
|
|
1015
|
-
});
|
|
1016
|
-
return true;
|
|
1017
|
-
}
|
|
1018
|
-
catch (error) {
|
|
1019
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 设置反应失败:`, error);
|
|
1020
|
-
throw error;
|
|
1021
|
-
}
|
|
309
|
+
async getChatAdmins(chatId) {
|
|
310
|
+
return this.callApi('getChatAdministrators', { chat_id: chatId });
|
|
1022
311
|
}
|
|
1023
312
|
async sendStickerMessage(chatId, sticker) {
|
|
1024
|
-
|
|
1025
|
-
const result = await this.telegram.sendSticker(chatId, sticker);
|
|
1026
|
-
this.pluginLogger.info(`Telegram Endpoint ${this.$id} 发送贴纸到 ${chatId}`);
|
|
1027
|
-
return result;
|
|
1028
|
-
}
|
|
1029
|
-
catch (error) {
|
|
1030
|
-
this.pluginLogger.error(`Telegram Endpoint ${this.$id} 发送贴纸失败:`, error);
|
|
1031
|
-
throw error;
|
|
1032
|
-
}
|
|
313
|
+
return this.callApi('sendSticker', { chat_id: chatId, sticker });
|
|
1033
314
|
}
|
|
1034
315
|
async setChatPermissionsAll(chatId, permissions) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
316
|
+
await this.callApi('setChatPermissions', { chat_id: chatId, permissions });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
async createInviteLink(chatId) {
|
|
320
|
+
const link = await this.callApi('createChatInviteLink', { chat_id: chatId });
|
|
321
|
+
return link.invite_link;
|
|
322
|
+
}
|
|
323
|
+
async sendPoll(chatId, question, options, isAnonymous = true, allowsMultipleAnswers = false) {
|
|
324
|
+
return this.callApi('sendPoll', {
|
|
325
|
+
chat_id: chatId,
|
|
326
|
+
question,
|
|
327
|
+
options,
|
|
328
|
+
is_anonymous: isAnonymous,
|
|
329
|
+
allows_multiple_answers: allowsMultipleAnswers,
|
|
330
|
+
});
|
|
1044
331
|
}
|
|
1045
332
|
}
|
|
1046
|
-
//# sourceMappingURL=endpoint.js.map
|