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