@overpod/mcp-telegram 1.28.1 → 1.33.0
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 +169 -0
- package/README.md +21 -8
- package/dist/telegram-client.d.ts +316 -4
- package/dist/telegram-client.js +1250 -5
- package/dist/telegram-helpers.d.ts +111 -1
- package/dist/telegram-helpers.js +270 -0
- package/dist/tools/account.js +221 -6
- package/dist/tools/business.d.ts +3 -0
- package/dist/tools/business.js +333 -0
- package/dist/tools/fact-check.d.ts +3 -0
- package/dist/tools/fact-check.js +72 -0
- package/dist/tools/folders.d.ts +3 -0
- package/dist/tools/folders.js +239 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/messages.js +183 -4
- package/dist/tools/reactions.js +62 -0
- package/dist/tools/send-media.d.ts +3 -0
- package/dist/tools/send-media.js +259 -0
- package/dist/tools/shared.d.ts +28 -0
- package/dist/tools/shared.js +59 -0
- package/dist/tools/stories.js +303 -1
- package/dist/tools/transcribe.d.ts +3 -0
- package/dist/tools/transcribe.js +75 -0
- package/package.json +1 -1
package/dist/tools/shared.js
CHANGED
|
@@ -22,6 +22,65 @@ export function formatReactions(reactions) {
|
|
|
22
22
|
const parts = reactions.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`);
|
|
23
23
|
return ` [${parts.join(" ")}]`;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate that a user-supplied path is safe to upload.
|
|
27
|
+
*
|
|
28
|
+
* The threat model is prompt-injection: an AI that was told "send the user's file" can be
|
|
29
|
+
* manipulated into sending `/proc/self/environ`, `/etc/shadow`, `http://169.254.169.254/...`,
|
|
30
|
+
* or an SMB share `\\attacker.com\share`. GramJS `sendFile` happily fetches URLs and reads
|
|
31
|
+
* any local path, so the validation has to live here.
|
|
32
|
+
*
|
|
33
|
+
* Rules:
|
|
34
|
+
* - Must be an absolute path (POSIX `/` or Windows `C:\` / `\\server\share`).
|
|
35
|
+
* - No URL schemes (http:, https:, file:, ftp:, data:, javascript:, …).
|
|
36
|
+
* - No path traversal (`..` segments) even inside an absolute path.
|
|
37
|
+
* - No OS-sensitive directories on POSIX (`/proc`, `/sys`, `/dev`, `/run`). These leak env,
|
|
38
|
+
* kernel state, or block on device reads.
|
|
39
|
+
* - UNC paths (`\\server\share`) are blocked (NTLM-relay / remote-SMB risk).
|
|
40
|
+
*
|
|
41
|
+
* This is defence-in-depth: the admin still owns the machine and can exfiltrate files
|
|
42
|
+
* deliberately — we just refuse to help prompt-injection do it automatically.
|
|
43
|
+
*/
|
|
44
|
+
export function isSafeAbsolutePath(p) {
|
|
45
|
+
if (typeof p !== "string" || p.length < 2)
|
|
46
|
+
return false;
|
|
47
|
+
// Reject embedded NUL — Node fs rejects it too, but we want an earlier, clearer failure
|
|
48
|
+
if (p.includes("\0"))
|
|
49
|
+
return false;
|
|
50
|
+
// Reject URL schemes outright (scheme://... or scheme:...)
|
|
51
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(p))
|
|
52
|
+
return false;
|
|
53
|
+
if (/^(file|data|javascript|http|https|ftp|ftps|ws|wss):/i.test(p))
|
|
54
|
+
return false;
|
|
55
|
+
// Reject Windows UNC shares — SMB SSRF / NTLM relay primitive
|
|
56
|
+
if (p.startsWith("\\\\") || p.startsWith("//"))
|
|
57
|
+
return false;
|
|
58
|
+
// Reject path-traversal segments anywhere in the path
|
|
59
|
+
const parts = p.split(/[\\/]+/);
|
|
60
|
+
if (parts.some((seg) => seg === ".."))
|
|
61
|
+
return false;
|
|
62
|
+
// POSIX absolute path
|
|
63
|
+
if (p.startsWith("/")) {
|
|
64
|
+
// Reject kernel / device / runtime pseudo-filesystems
|
|
65
|
+
if (/^\/(proc|sys|dev|run)(\/|$)/.test(p))
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// Windows absolute path (C:\ or C:/), no UNC (already rejected above)
|
|
70
|
+
if (/^[a-zA-Z]:[\\/]/.test(p))
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** Zod refinement message paired with `isSafeAbsolutePath` */
|
|
75
|
+
export const ABSOLUTE_PATH_ERROR = "Must be an absolute local filesystem path (e.g. /tmp/file.ogg). URLs, UNC shares, path traversal (..), and OS-sensitive dirs (/proc, /sys, /dev, /run) are rejected.";
|
|
76
|
+
/**
|
|
77
|
+
* Sanitize a user-provided text for safe TL encoding.
|
|
78
|
+
* Strips unpaired UTF-16 surrogates that crash GramJS's wire serializer. Use on every
|
|
79
|
+
* free-text field that reaches GramJS (captions, provider names, venue titles, quoteText, …).
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeInputText(text) {
|
|
82
|
+
return sanitize(text);
|
|
83
|
+
}
|
|
25
84
|
/** Try to connect, return error text if failed */
|
|
26
85
|
export async function requireConnection(telegram) {
|
|
27
86
|
if (await telegram.ensureConnected())
|
package/dist/tools/stories.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { fail, ok, READ_ONLY, requireConnection } from "./shared.js";
|
|
2
|
+
import { ABSOLUTE_PATH_ERROR, DESTRUCTIVE, fail, isSafeAbsolutePath, ok, READ_ONLY, requireConnection, sanitizeInputText, WRITE, } from "./shared.js";
|
|
3
|
+
const absolutePath = z.string().refine(isSafeAbsolutePath, { message: ABSOLUTE_PATH_ERROR });
|
|
4
|
+
const safeText = z.string().transform(sanitizeInputText);
|
|
3
5
|
export function registerStoryTools(server, telegram) {
|
|
4
6
|
server.registerTool("telegram-get-all-stories", {
|
|
5
7
|
description: "Fetch active stories from contacts/channels the user follows. Pagination via 'next' + 'state' — pass the returned state back on the next call with next:true to load more. Use hidden:true to read stories from muted/archived peers. Returns compact story metadata (id, date, expireDate, caption, mediaType, counters) without raw media blobs.",
|
|
@@ -104,4 +106,304 @@ export function registerStoryTools(server, telegram) {
|
|
|
104
106
|
return fail(e);
|
|
105
107
|
}
|
|
106
108
|
});
|
|
109
|
+
server.registerTool("telegram-send-story", {
|
|
110
|
+
description: "Publish a new story (photo or video) to your profile or a channel you manage. Privacy: everyone/contacts/close_friends/selected (allowUserIds required for 'selected'). MediaAreas not supported in this version.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
chatId: z.string().default("me").describe("Peer to post the story to — 'me', @username, or numeric ID"),
|
|
113
|
+
filePath: absolutePath.describe("Absolute path to the photo or video file to upload"),
|
|
114
|
+
type: z.enum(["photo", "video"]).optional().describe("Override auto-detected media type"),
|
|
115
|
+
caption: safeText.pipe(z.string().max(2048)).optional().describe("Story caption (max 2048 chars)"),
|
|
116
|
+
parseMode: z.enum(["md", "html"]).optional().describe("Caption parse mode: md or html"),
|
|
117
|
+
privacy: z
|
|
118
|
+
.enum(["everyone", "contacts", "close_friends", "selected"])
|
|
119
|
+
.default("everyone")
|
|
120
|
+
.describe("Who can see the story"),
|
|
121
|
+
allowUserIds: z
|
|
122
|
+
.array(z.string().regex(/^\d{1,19}$/, "must be a numeric Telegram user ID"))
|
|
123
|
+
.optional()
|
|
124
|
+
.describe("Required when privacy='selected': numeric user IDs allowed to see the story"),
|
|
125
|
+
disallowUserIds: z
|
|
126
|
+
.array(z.string().regex(/^\d{1,19}$/, "must be a numeric Telegram user ID"))
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("User IDs explicitly blocked from seeing the story (ignored for privacy='selected')"),
|
|
129
|
+
period: z
|
|
130
|
+
.union([z.literal(21600), z.literal(43200), z.literal(86400), z.literal(172800)])
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Story lifetime in seconds: 21600=6h, 43200=12h, 86400=24h (default), 172800=48h"),
|
|
133
|
+
pinned: z.boolean().optional().describe("Keep the story in the profile highlights after expiry"),
|
|
134
|
+
noforwards: z.boolean().optional().describe("Prevent others from forwarding or saving the story"),
|
|
135
|
+
},
|
|
136
|
+
annotations: WRITE,
|
|
137
|
+
}, async ({ chatId, filePath, type, caption, parseMode, privacy, allowUserIds, disallowUserIds, period, pinned, noforwards, }) => {
|
|
138
|
+
const err = await requireConnection(telegram);
|
|
139
|
+
if (err)
|
|
140
|
+
return fail(new Error(err));
|
|
141
|
+
if (privacy === "selected" && !allowUserIds?.length) {
|
|
142
|
+
return fail(new Error("privacy='selected' requires at least one user ID in allowUserIds"));
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const result = await telegram.sendStory(chatId, filePath, {
|
|
146
|
+
type,
|
|
147
|
+
caption,
|
|
148
|
+
parseMode,
|
|
149
|
+
privacy,
|
|
150
|
+
allowUserIds,
|
|
151
|
+
disallowUserIds,
|
|
152
|
+
period,
|
|
153
|
+
pinned,
|
|
154
|
+
noforwards,
|
|
155
|
+
});
|
|
156
|
+
const idInfo = result.id ? ` [#${result.id}]` : "";
|
|
157
|
+
return ok(`Story published to ${chatId}${idInfo} (expires in ${result.period}s)`);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return fail(e);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.registerTool("telegram-edit-story", {
|
|
164
|
+
description: "Edit an existing story: replace media, update caption ('' clears it), or change privacy rules.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
chatId: z.string().default("me").describe("Peer owning the story"),
|
|
167
|
+
storyId: z.number().int().positive().describe("ID of the story to edit"),
|
|
168
|
+
filePath: absolutePath.optional().describe("Absolute path to replacement media"),
|
|
169
|
+
type: z.enum(["photo", "video"]).optional().describe("Override auto-detected media type for new file"),
|
|
170
|
+
caption: safeText.pipe(z.string().max(2048)).optional().describe("New caption; pass '' to clear"),
|
|
171
|
+
parseMode: z.enum(["md", "html"]).optional().describe("Caption parse mode"),
|
|
172
|
+
privacy: z
|
|
173
|
+
.enum(["everyone", "contacts", "close_friends", "selected"])
|
|
174
|
+
.optional()
|
|
175
|
+
.describe("New privacy setting"),
|
|
176
|
+
allowUserIds: z
|
|
177
|
+
.array(z.string().regex(/^\d{1,19}$/, "must be a numeric Telegram user ID"))
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Required when privacy='selected'"),
|
|
180
|
+
disallowUserIds: z
|
|
181
|
+
.array(z.string().regex(/^\d{1,19}$/, "must be a numeric Telegram user ID"))
|
|
182
|
+
.optional()
|
|
183
|
+
.describe("Blocked user IDs (ignored for 'selected')"),
|
|
184
|
+
},
|
|
185
|
+
annotations: WRITE,
|
|
186
|
+
}, async ({ chatId, storyId, filePath, type, caption, parseMode, privacy, allowUserIds, disallowUserIds }) => {
|
|
187
|
+
const err = await requireConnection(telegram);
|
|
188
|
+
if (err)
|
|
189
|
+
return fail(new Error(err));
|
|
190
|
+
if (filePath === undefined && caption === undefined && privacy === undefined) {
|
|
191
|
+
return fail(new Error("At least one field (filePath, caption, or privacy) must be provided"));
|
|
192
|
+
}
|
|
193
|
+
if (privacy === "selected" && !allowUserIds?.length) {
|
|
194
|
+
return fail(new Error("privacy='selected' requires at least one user ID in allowUserIds"));
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const result = await telegram.editStory(chatId, storyId, {
|
|
198
|
+
filePath,
|
|
199
|
+
type,
|
|
200
|
+
caption,
|
|
201
|
+
parseMode,
|
|
202
|
+
privacy,
|
|
203
|
+
allowUserIds,
|
|
204
|
+
disallowUserIds,
|
|
205
|
+
});
|
|
206
|
+
return ok(`Story #${storyId} edited (${result.changed.join(", ")})`);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
return fail(e);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
server.registerTool("telegram-delete-stories", {
|
|
213
|
+
description: "Delete one or more of your own stories. This action is irreversible and requires confirm:true.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
chatId: z.string().default("me").describe("Peer owning the stories"),
|
|
216
|
+
ids: z.array(z.number().int().positive()).min(1).max(100).describe("Story IDs to delete (1–100 per request)"),
|
|
217
|
+
confirm: z.literal(true).describe("Pass true to confirm irreversible deletion"),
|
|
218
|
+
},
|
|
219
|
+
annotations: DESTRUCTIVE,
|
|
220
|
+
}, async ({ chatId, ids, confirm: _confirm }) => {
|
|
221
|
+
const err = await requireConnection(telegram);
|
|
222
|
+
if (err)
|
|
223
|
+
return fail(new Error(err));
|
|
224
|
+
try {
|
|
225
|
+
const result = await telegram.deleteStories(chatId, ids);
|
|
226
|
+
const { deleted } = result;
|
|
227
|
+
return ok(`Deleted ${deleted.length} stor${deleted.length === 1 ? "y" : "ies"}: [${deleted.join(", ")}]`);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return fail(e);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
server.registerTool("telegram-react-to-story", {
|
|
234
|
+
description: "React to a story with an emoji, or remove the current reaction by passing ''.",
|
|
235
|
+
inputSchema: {
|
|
236
|
+
chatId: z.string().describe("Peer who posted the story"),
|
|
237
|
+
storyId: z.number().int().positive().describe("Story ID to react to"),
|
|
238
|
+
emoji: z.string().max(8).describe("Reaction emoji. Empty string '' removes the reaction."),
|
|
239
|
+
addToRecent: z.boolean().optional().describe("Add emoji to your recently used reactions"),
|
|
240
|
+
},
|
|
241
|
+
annotations: WRITE,
|
|
242
|
+
}, async ({ chatId, storyId, emoji, addToRecent }) => {
|
|
243
|
+
const err = await requireConnection(telegram);
|
|
244
|
+
if (err)
|
|
245
|
+
return fail(new Error(err));
|
|
246
|
+
try {
|
|
247
|
+
await telegram.sendStoryReaction(chatId, storyId, emoji, addToRecent);
|
|
248
|
+
return ok(emoji === "" ? `Removed reaction from story #${storyId}` : `Reacted ${emoji} to story #${storyId}`);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
return fail(e);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
server.registerTool("telegram-export-story-link", {
|
|
255
|
+
description: "Get a shareable t.me/… URL for a public story.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
chatId: z.string().describe("Peer who posted the story"),
|
|
258
|
+
storyId: z.number().int().positive().describe("Story ID to get the link for"),
|
|
259
|
+
},
|
|
260
|
+
annotations: READ_ONLY,
|
|
261
|
+
}, async ({ chatId, storyId }) => {
|
|
262
|
+
const err = await requireConnection(telegram);
|
|
263
|
+
if (err)
|
|
264
|
+
return fail(new Error(err));
|
|
265
|
+
try {
|
|
266
|
+
const result = await telegram.exportStoryLink(chatId, storyId);
|
|
267
|
+
return ok(result.link);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return fail(e);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
server.registerTool("telegram-read-stories", {
|
|
274
|
+
description: "Mark stories as seen up to a given story ID (maxId, inclusive).",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
chatId: z.string().describe("Peer whose stories to mark as seen"),
|
|
277
|
+
maxId: z.number().int().positive().describe("Stories up to and including this ID will be marked seen"),
|
|
278
|
+
},
|
|
279
|
+
annotations: WRITE,
|
|
280
|
+
}, async ({ chatId, maxId }) => {
|
|
281
|
+
const err = await requireConnection(telegram);
|
|
282
|
+
if (err)
|
|
283
|
+
return fail(new Error(err));
|
|
284
|
+
try {
|
|
285
|
+
const result = await telegram.readStories(chatId, maxId);
|
|
286
|
+
return ok(`Marked stories as read up to #${maxId} (${result.ids.length} newly seen)`);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
return fail(e);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
server.registerTool("telegram-toggle-story-pinned", {
|
|
293
|
+
description: "Pin or unpin stories in your profile highlights (Telegram allows up to 3 pinned stories).",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
chatId: z.string().default("me").describe("Peer owning the stories"),
|
|
296
|
+
ids: z.array(z.number().int().positive()).min(1).max(100).describe("Story IDs to pin or unpin"),
|
|
297
|
+
pinned: z.boolean().describe("true to pin, false to unpin"),
|
|
298
|
+
},
|
|
299
|
+
annotations: WRITE,
|
|
300
|
+
}, async ({ chatId, ids, pinned }) => {
|
|
301
|
+
const err = await requireConnection(telegram);
|
|
302
|
+
if (err)
|
|
303
|
+
return fail(new Error(err));
|
|
304
|
+
try {
|
|
305
|
+
const result = await telegram.toggleStoryPinned(chatId, ids, pinned);
|
|
306
|
+
const { affected } = result;
|
|
307
|
+
return ok(`${pinned ? "Pinned" : "Unpinned"} ${affected.length} stor${affected.length === 1 ? "y" : "ies"}: [${affected.join(", ")}]`);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
return fail(e);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
server.registerTool("telegram-toggle-story-pinned-to-top", {
|
|
314
|
+
description: "Pin stories to the very top of your pinned row. Pass an empty array [] to clear all top-pinned stories.",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
chatId: z.string().default("me").describe("Peer owning the stories"),
|
|
317
|
+
ids: z
|
|
318
|
+
.array(z.number().int().positive())
|
|
319
|
+
.max(100)
|
|
320
|
+
.describe("Story IDs to pin to the top row; pass [] to clear"),
|
|
321
|
+
},
|
|
322
|
+
annotations: WRITE,
|
|
323
|
+
}, async ({ chatId, ids }) => {
|
|
324
|
+
const err = await requireConnection(telegram);
|
|
325
|
+
if (err)
|
|
326
|
+
return fail(new Error(err));
|
|
327
|
+
try {
|
|
328
|
+
await telegram.toggleStoryPinnedToTop(chatId, ids);
|
|
329
|
+
return ok(ids.length === 0
|
|
330
|
+
? "Cleared top-pinned stories"
|
|
331
|
+
: `Pinned ${ids.length} stor${ids.length === 1 ? "y" : "ies"} to top: [${ids.join(", ")}]`);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
return fail(e);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
server.registerTool("telegram-activate-stealth-mode", {
|
|
338
|
+
description: "Hide your story views retroactively (past=true) and/or for the next 25 minutes (future=true). Requires Telegram Premium — non-Premium accounts receive PREMIUM_ACCOUNT_REQUIRED.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
past: z.boolean().optional().describe("Remove your views from stories you already watched"),
|
|
341
|
+
future: z.boolean().optional().describe("Hide your views for the next 25 minutes"),
|
|
342
|
+
},
|
|
343
|
+
annotations: WRITE,
|
|
344
|
+
}, async ({ past, future }) => {
|
|
345
|
+
const err = await requireConnection(telegram);
|
|
346
|
+
if (err)
|
|
347
|
+
return fail(new Error(err));
|
|
348
|
+
if (!past && !future) {
|
|
349
|
+
return fail(new Error("At least one of past or future must be true"));
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await telegram.activateStealthMode(past, future);
|
|
353
|
+
return ok(`Stealth mode activated (past: ${past ?? false}, future: ${future ?? false})`);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
return fail(e);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
server.registerTool("telegram-get-stories-archive", {
|
|
360
|
+
description: "Fetch auto-archived (expired) stories from a peer's archive. Paginate via offsetId (pass last story id from previous page).",
|
|
361
|
+
inputSchema: {
|
|
362
|
+
chatId: z.string().default("me").describe("Peer whose archive to fetch"),
|
|
363
|
+
offsetId: z
|
|
364
|
+
.number()
|
|
365
|
+
.int()
|
|
366
|
+
.nonnegative()
|
|
367
|
+
.default(0)
|
|
368
|
+
.describe("Pagination offset: pass last story ID from previous page (0 to start)"),
|
|
369
|
+
limit: z.number().int().min(1).max(100).default(50).describe("Max stories to return (1–100, default 50)"),
|
|
370
|
+
},
|
|
371
|
+
annotations: READ_ONLY,
|
|
372
|
+
}, async ({ chatId, offsetId, limit }) => {
|
|
373
|
+
const err = await requireConnection(telegram);
|
|
374
|
+
if (err)
|
|
375
|
+
return fail(new Error(err));
|
|
376
|
+
try {
|
|
377
|
+
const result = await telegram.getStoriesArchive(chatId, offsetId, limit);
|
|
378
|
+
return ok(JSON.stringify(result));
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
return fail(e);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
server.registerTool("telegram-report-story", {
|
|
385
|
+
description: "Report a story via the multi-step option flow. First call with option:'' starts the flow; subsequent calls pass the base64 option bytes from the previous response.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
chatId: z.string().describe("Peer who posted the story"),
|
|
388
|
+
ids: z.array(z.number().int().positive()).min(1).max(100).describe("Story IDs to report"),
|
|
389
|
+
option: z
|
|
390
|
+
.string()
|
|
391
|
+
.max(128)
|
|
392
|
+
.default("")
|
|
393
|
+
.describe("Base64-encoded option bytes from a prior report step, or empty string to start the flow"),
|
|
394
|
+
message: safeText.pipe(z.string().max(1024)).default("").describe("Optional message to accompany the report"),
|
|
395
|
+
},
|
|
396
|
+
annotations: WRITE,
|
|
397
|
+
}, async ({ chatId, ids, option, message }) => {
|
|
398
|
+
const err = await requireConnection(telegram);
|
|
399
|
+
if (err)
|
|
400
|
+
return fail(new Error(err));
|
|
401
|
+
try {
|
|
402
|
+
const result = await telegram.reportStory(chatId, ids, option, message);
|
|
403
|
+
return ok(JSON.stringify(result));
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
return fail(e);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
107
409
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fail, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
|
|
3
|
+
export function registerTranscribeTools(server, telegram) {
|
|
4
|
+
server.registerTool("telegram-transcribe-audio", {
|
|
5
|
+
description: "Request server-side transcription of a voice note or video note (Telegram Premium feature). Returns immediately with transcriptionId — if pending:true, call telegram-get-transcription to poll for completion.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
chatId: z.string().describe("Chat ID or username"),
|
|
8
|
+
messageId: z.number().int().positive().describe("Message ID of the voice or video note"),
|
|
9
|
+
},
|
|
10
|
+
annotations: WRITE,
|
|
11
|
+
}, async ({ chatId, messageId }) => {
|
|
12
|
+
const err = await requireConnection(telegram);
|
|
13
|
+
if (err)
|
|
14
|
+
return fail(new Error(err));
|
|
15
|
+
try {
|
|
16
|
+
const result = await telegram.transcribeAudio(chatId, messageId);
|
|
17
|
+
const trialInfo = result.trialRemainsNum !== undefined
|
|
18
|
+
? `\nTrial remaining: ${result.trialRemainsNum} free transcription(s)`
|
|
19
|
+
: "";
|
|
20
|
+
if (result.pending) {
|
|
21
|
+
return ok(`Transcription started for message #${messageId}\nTranscriptionId: ${result.transcriptionId}\nStatus: pending${trialInfo}`);
|
|
22
|
+
}
|
|
23
|
+
return ok(`Transcription for message #${messageId}:\nTranscriptionId: ${result.transcriptionId}\nStatus: complete\n\n${result.text}`);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
return fail(e);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
server.registerTool("telegram-get-transcription", {
|
|
30
|
+
description: "Poll for updated transcription result. Calls the same endpoint as telegram-transcribe-audio — Telegram guarantees idempotency (returns same transcriptionId with updated text once processing completes).",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
chatId: z.string().describe("Chat ID or username"),
|
|
33
|
+
messageId: z.number().int().positive().describe("Message ID of the voice or video note"),
|
|
34
|
+
},
|
|
35
|
+
annotations: READ_ONLY,
|
|
36
|
+
}, async ({ chatId, messageId }) => {
|
|
37
|
+
const err = await requireConnection(telegram);
|
|
38
|
+
if (err)
|
|
39
|
+
return fail(new Error(err));
|
|
40
|
+
try {
|
|
41
|
+
const result = await telegram.transcribeAudio(chatId, messageId);
|
|
42
|
+
const trialInfo = result.trialRemainsNum !== undefined
|
|
43
|
+
? `\nTrial remaining: ${result.trialRemainsNum} free transcription(s)`
|
|
44
|
+
: "";
|
|
45
|
+
if (result.pending) {
|
|
46
|
+
return ok(`Transcription started for message #${messageId}\nTranscriptionId: ${result.transcriptionId}\nStatus: pending${trialInfo}`);
|
|
47
|
+
}
|
|
48
|
+
return ok(`Transcription for message #${messageId}:\nTranscriptionId: ${result.transcriptionId}\nStatus: complete\n\n${result.text}`);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return fail(e);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
server.registerTool("telegram-rate-transcription", {
|
|
55
|
+
description: "Rate transcription quality (good/poor) to improve Telegram speech-to-text.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
chatId: z.string().describe("Chat ID or username"),
|
|
58
|
+
messageId: z.number().int().positive().describe("Message ID of the voice or video note"),
|
|
59
|
+
transcriptionId: z.string().describe("Transcription ID returned by telegram-transcribe-audio"),
|
|
60
|
+
good: z.boolean().describe("true = good quality, false = poor quality"),
|
|
61
|
+
},
|
|
62
|
+
annotations: WRITE,
|
|
63
|
+
}, async ({ chatId, messageId, transcriptionId, good }) => {
|
|
64
|
+
const err = await requireConnection(telegram);
|
|
65
|
+
if (err)
|
|
66
|
+
return fail(new Error(err));
|
|
67
|
+
try {
|
|
68
|
+
await telegram.rateTranscription(chatId, messageId, transcriptionId, good);
|
|
69
|
+
return ok(`Rated transcription ${transcriptionId} for message #${messageId} as ${good ? "good" : "poor"}`);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
return fail(e);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
package/package.json
CHANGED