@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.
@@ -1,26 +1,36 @@
1
1
  import { registerAccountTools } from "./account.js";
2
2
  import { registerAuthTools } from "./auth.js";
3
3
  import { registerBoostTools } from "./boosts.js";
4
+ import { registerBusinessTools } from "./business.js";
4
5
  import { registerChatTools } from "./chats.js";
5
6
  import { registerContactTools } from "./contacts.js";
6
7
  import { registerExtraTools } from "./extras.js";
8
+ import { registerFactCheckTools } from "./fact-check.js";
9
+ import { registerFolderTools } from "./folders.js";
7
10
  import { registerGroupCallTools } from "./group-calls.js";
8
11
  import { registerMediaTools } from "./media.js";
9
12
  import { registerMessageTools } from "./messages.js";
10
13
  import { registerQuickRepliesTools } from "./quick-replies.js";
11
14
  import { registerReactionTools } from "./reactions.js";
15
+ import { registerSendMediaTools } from "./send-media.js";
12
16
  import { registerStarsTools } from "./stars.js";
13
17
  import { registerStickerTools } from "./stickers.js";
14
18
  import { registerStoryTools } from "./stories.js";
19
+ import { registerTranscribeTools } from "./transcribe.js";
15
20
  export function registerTools(server, telegram) {
16
21
  registerAuthTools(server, telegram);
17
22
  registerMessageTools(server, telegram);
18
23
  registerChatTools(server, telegram);
19
24
  registerMediaTools(server, telegram);
25
+ registerSendMediaTools(server, telegram);
20
26
  registerContactTools(server, telegram);
21
27
  registerReactionTools(server, telegram);
28
+ registerTranscribeTools(server, telegram);
29
+ registerFactCheckTools(server, telegram);
22
30
  registerExtraTools(server, telegram);
23
31
  registerAccountTools(server, telegram);
32
+ registerBusinessTools(server, telegram);
33
+ registerFolderTools(server, telegram);
24
34
  registerStickerTools(server, telegram);
25
35
  registerStoryTools(server, telegram);
26
36
  registerBoostTools(server, telegram);
@@ -1,22 +1,34 @@
1
1
  import { z } from "zod";
2
- import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, requireConnection, WRITE } from "./shared.js";
2
+ import { DESTRUCTIVE, fail, formatReactions, ok, READ_ONLY, requireConnection, sanitizeInputText, WRITE, } from "./shared.js";
3
3
  export function registerMessageTools(server, telegram) {
4
4
  server.registerTool("telegram-send-message", {
5
5
  description: "Send a message to a Telegram chat",
6
6
  inputSchema: {
7
7
  chatId: z.string().describe("Chat ID or username (e.g. @username or numeric ID)"),
8
- text: z.string().describe("Message text"),
8
+ text: z.string().transform(sanitizeInputText).describe("Message text"),
9
9
  replyTo: z.number().optional().describe("Message ID to reply to"),
10
10
  parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
11
11
  topicId: z.number().optional().describe("Forum topic ID to send message into (for groups with Topics enabled)"),
12
+ quoteText: z
13
+ .string()
14
+ .transform(sanitizeInputText)
15
+ .optional()
16
+ .describe("Optional excerpt from the replied-to message to show as a quote above your reply. " +
17
+ "Requires `replyTo` to be set. Must be a verbatim substring of the original message text."),
18
+ effect: z
19
+ .string()
20
+ .regex(/^\d{1,19}$/)
21
+ .optional()
22
+ .describe("Optional message effect ID (numeric string, up to 19 digits). Premium animated effect attached to the message."),
12
23
  },
13
24
  annotations: WRITE,
14
- }, async ({ chatId, text, replyTo, parseMode, topicId }) => {
25
+ }, async ({ chatId, text, replyTo, parseMode, topicId, quoteText, effect }) => {
15
26
  const err = await requireConnection(telegram);
16
27
  if (err)
17
28
  return fail(new Error(err));
18
29
  try {
19
- const result = await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
30
+ const extra = quoteText || effect ? { quoteText, effect } : undefined;
31
+ const result = await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId, extra);
20
32
  const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
21
33
  const messageId = result?.id;
22
34
  const idInfo = messageId ? ` [#${messageId}]` : "";
@@ -586,4 +598,171 @@ export function registerMessageTools(server, telegram) {
586
598
  return fail(e);
587
599
  }
588
600
  });
601
+ server.registerTool("telegram-get-discussion-message", {
602
+ description: "For a channel post with comments enabled, returns the linked discussion-group info: discussionGroupId, discussionMsgId, unreadCount, readInboxMaxId, readOutboxMaxId, topMessage. Use discussionGroupId + discussionMsgId with telegram-send-message (replyTo=discussionMsgId) to post a comment.",
603
+ inputSchema: {
604
+ chatId: z.string().describe("Channel ID or @username that contains the post"),
605
+ messageId: z.number().int().positive().describe("ID of the channel post to get discussion info for"),
606
+ },
607
+ annotations: READ_ONLY,
608
+ }, async ({ chatId, messageId }) => {
609
+ const err = await requireConnection(telegram);
610
+ if (err)
611
+ return fail(new Error(err));
612
+ try {
613
+ const result = await telegram.getDiscussionMessage(chatId, messageId);
614
+ return ok(JSON.stringify(result));
615
+ }
616
+ catch (e) {
617
+ return fail(e);
618
+ }
619
+ });
620
+ server.registerTool("telegram-get-groups-for-discussion", {
621
+ description: "List groups that can be linked as a discussion group to a channel you admin. Helper for channel admins setting up comment threads.",
622
+ inputSchema: {},
623
+ annotations: READ_ONLY,
624
+ }, async () => {
625
+ const err = await requireConnection(telegram);
626
+ if (err)
627
+ return fail(new Error(err));
628
+ try {
629
+ const result = await telegram.getGroupsForDiscussion();
630
+ return ok(JSON.stringify(result));
631
+ }
632
+ catch (e) {
633
+ return fail(e);
634
+ }
635
+ });
636
+ server.registerTool("telegram-get-message-read-participants", {
637
+ description: "List who has read a message in a small group (≤100 members, ≤7 days old). Returns readers with userId, readAt timestamp. Does NOT work for channels or groups over 100 members (CHAT_TOO_BIG).",
638
+ inputSchema: {
639
+ chatId: z.string().describe("Group chat ID or @username"),
640
+ messageId: z.number().int().positive().describe("ID of the message to check read status for"),
641
+ },
642
+ annotations: READ_ONLY,
643
+ }, async ({ chatId, messageId }) => {
644
+ const err = await requireConnection(telegram);
645
+ if (err)
646
+ return fail(new Error(err));
647
+ try {
648
+ const result = await telegram.getMessageReadParticipants(chatId, messageId);
649
+ return ok(JSON.stringify(result));
650
+ }
651
+ catch (e) {
652
+ return fail(e);
653
+ }
654
+ });
655
+ // ─── Poll interaction ──────────────────────────────────────────────────────
656
+ server.registerTool("telegram-vote-poll", {
657
+ description: "Vote in a poll by option index (single or multi-choice). Empty array retracts vote.",
658
+ inputSchema: {
659
+ chatId: z.string().describe("Chat ID or username"),
660
+ messageId: z.number().int().positive().describe("Message ID of the poll"),
661
+ optionIndexes: z
662
+ .array(z.number().int().min(0).max(9))
663
+ .min(0)
664
+ .max(10)
665
+ .describe("Zero-based option indexes. Empty [] retracts vote."),
666
+ },
667
+ annotations: WRITE,
668
+ }, async ({ chatId, messageId, optionIndexes }) => {
669
+ const err = await requireConnection(telegram);
670
+ if (err)
671
+ return fail(new Error(err));
672
+ try {
673
+ const result = await telegram.sendPollVote(chatId, messageId, optionIndexes);
674
+ if (optionIndexes.length === 0) {
675
+ return ok(`Retracted vote from poll #${messageId}`);
676
+ }
677
+ return ok(`Voted in poll #${messageId}: option(s) ${result.chosenLabels.join(", ")} | Total: ${result.totalVoters} voters`);
678
+ }
679
+ catch (e) {
680
+ return fail(e);
681
+ }
682
+ });
683
+ server.registerTool("telegram-get-poll-results", {
684
+ description: "Get aggregated poll results: vote counts, percentages, quiz answer status",
685
+ inputSchema: {
686
+ chatId: z.string().describe("Chat ID or username"),
687
+ messageId: z.number().int().positive().describe("Message ID of the poll"),
688
+ },
689
+ annotations: READ_ONLY,
690
+ }, async ({ chatId, messageId }) => {
691
+ const err = await requireConnection(telegram);
692
+ if (err)
693
+ return fail(new Error(err));
694
+ try {
695
+ const result = await telegram.getPollResults(chatId, messageId);
696
+ return ok(JSON.stringify(result));
697
+ }
698
+ catch (e) {
699
+ return fail(e);
700
+ }
701
+ });
702
+ server.registerTool("telegram-get-poll-voters", {
703
+ description: "List users who voted for specific poll options (public polls only, paginated)",
704
+ inputSchema: {
705
+ chatId: z.string().describe("Chat ID or username"),
706
+ messageId: z.number().int().positive().describe("Message ID of the poll"),
707
+ optionIndex: z
708
+ .number()
709
+ .int()
710
+ .min(0)
711
+ .max(9)
712
+ .optional()
713
+ .describe("Zero-based option index to filter by. Omit to get all voters"),
714
+ limit: z.number().int().min(1).max(100).default(20).describe("Max voters to return"),
715
+ offset: z.string().optional().describe("Pagination offset from previous call"),
716
+ },
717
+ annotations: READ_ONLY,
718
+ }, async ({ chatId, messageId, optionIndex, limit, offset }) => {
719
+ const err = await requireConnection(telegram);
720
+ if (err)
721
+ return fail(new Error(err));
722
+ try {
723
+ const result = await telegram.getPollVoters(chatId, messageId, { optionIndex, limit, offset });
724
+ return ok(JSON.stringify(result));
725
+ }
726
+ catch (e) {
727
+ return fail(e);
728
+ }
729
+ });
730
+ server.registerTool("telegram-close-poll", {
731
+ description: "Close a poll permanently. This is a one-way operation — closed polls cannot be reopened.",
732
+ inputSchema: {
733
+ chatId: z.string().describe("Chat ID or username"),
734
+ messageId: z.number().int().positive().describe("Message ID of the poll to close"),
735
+ },
736
+ annotations: WRITE,
737
+ }, async ({ chatId, messageId }) => {
738
+ const err = await requireConnection(telegram);
739
+ if (err)
740
+ return fail(new Error(err));
741
+ try {
742
+ const result = await telegram.closePoll(chatId, messageId);
743
+ return ok(`Closed poll #${messageId} (final: ${result.totalVoters} voters)`);
744
+ }
745
+ catch (e) {
746
+ return fail(e);
747
+ }
748
+ });
749
+ server.registerTool("telegram-get-outbox-read-date", {
750
+ description: "Get when your recipient read your outgoing message in a private chat. Returns null/Not read yet if unread. Errors if the other side disabled read receipts (YOUR_PRIVACY_RESTRICTED / USER_PRIVACY_RESTRICTED).",
751
+ inputSchema: {
752
+ chatId: z.string().describe("Private chat ID or @username of the recipient"),
753
+ messageId: z.number().int().positive().describe("ID of your outgoing message"),
754
+ },
755
+ annotations: READ_ONLY,
756
+ }, async ({ chatId, messageId }) => {
757
+ const err = await requireConnection(telegram);
758
+ if (err)
759
+ return fail(new Error(err));
760
+ try {
761
+ const result = await telegram.getOutboxReadDate(chatId, messageId);
762
+ return ok(result.readAt ? `Read at ${result.readAt}` : "Not read yet");
763
+ }
764
+ catch (e) {
765
+ return fail(e);
766
+ }
767
+ });
589
768
  }
@@ -161,4 +161,66 @@ export function registerReactionTools(server, telegram) {
161
161
  return fail(e);
162
162
  }
163
163
  });
164
+ // ─── Paid reactions ────────────────────────────────────────────────────────
165
+ server.registerTool("telegram-send-paid-reaction", {
166
+ description: "Send a paid reaction (★ Stars) on a channel post. Stars are spent from your balance. Optional private flag controls leaderboard visibility.",
167
+ inputSchema: {
168
+ chatId: z.string().describe("Chat ID or username (channel)"),
169
+ messageId: z.number().int().positive().describe("Message ID of the channel post"),
170
+ count: z.number().int().min(1).max(2500).default(1).describe("Number of Stars to send (1-2500)"),
171
+ private: z
172
+ .boolean()
173
+ .optional()
174
+ .describe("true = anonymous on leaderboard, false = show name, omit = use account default"),
175
+ },
176
+ annotations: WRITE,
177
+ }, async ({ chatId, messageId, count, private: privateFlag }) => {
178
+ const err = await requireConnection(telegram);
179
+ if (err)
180
+ return fail(new Error(err));
181
+ try {
182
+ await telegram.sendPaidReaction(chatId, messageId, count, { private: privateFlag });
183
+ const privacy = privateFlag === true ? " (anonymous)" : privateFlag === false ? " (public)" : "";
184
+ return ok(`Sent ★×${count} paid reaction to message #${messageId} in ${chatId}${privacy}`);
185
+ }
186
+ catch (e) {
187
+ return fail(e);
188
+ }
189
+ });
190
+ server.registerTool("telegram-toggle-paid-reaction-privacy", {
191
+ description: "Change leaderboard visibility of your paid reaction on a specific channel post (Layer 198 API).",
192
+ inputSchema: {
193
+ chatId: z.string().describe("Chat ID or username (channel)"),
194
+ messageId: z.number().int().positive().describe("Message ID of the channel post"),
195
+ private: z.boolean().describe("true = anonymous on leaderboard, false = show name"),
196
+ },
197
+ annotations: WRITE,
198
+ }, async ({ chatId, messageId, private: privateFlag }) => {
199
+ const err = await requireConnection(telegram);
200
+ if (err)
201
+ return fail(new Error(err));
202
+ try {
203
+ await telegram.togglePaidReactionPrivacy(chatId, messageId, privateFlag);
204
+ return ok(`Updated paid reaction privacy on message #${messageId} in ${chatId}: ${privateFlag ? "anonymous" : "show name"}`);
205
+ }
206
+ catch (e) {
207
+ return fail(e);
208
+ }
209
+ });
210
+ server.registerTool("telegram-get-paid-reaction-privacy", {
211
+ description: "Get your current default paid reaction privacy setting.",
212
+ inputSchema: {},
213
+ annotations: READ_ONLY,
214
+ }, async () => {
215
+ const err = await requireConnection(telegram);
216
+ if (err)
217
+ return fail(new Error(err));
218
+ try {
219
+ const result = await telegram.getPaidReactionPrivacy();
220
+ return ok(`Default paid reaction privacy: ${result.private ? "anonymous" : "show name"}`);
221
+ }
222
+ catch (e) {
223
+ return fail(e);
224
+ }
225
+ });
164
226
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { TelegramService } from "../telegram-client.js";
3
+ export declare function registerSendMediaTools(server: McpServer, telegram: TelegramService): void;
@@ -0,0 +1,259 @@
1
+ import { z } from "zod";
2
+ import { ABSOLUTE_PATH_ERROR, fail, isSafeAbsolutePath, ok, requireConnection, sanitizeInputText, WRITE, } from "./shared.js";
3
+ const absolutePath = z.string().min(1).refine(isSafeAbsolutePath, ABSOLUTE_PATH_ERROR);
4
+ const safeText = z.string().transform(sanitizeInputText);
5
+ const DICE_EMOJIS = ["🎲", "🎯", "🎰", "🏀", "⚽", "🎳"];
6
+ export function registerSendMediaTools(server, telegram) {
7
+ server.registerTool("telegram-send-voice", {
8
+ description: "Send a voice note (audio recording) to a Telegram chat. Shows as a voice message with waveform UI.",
9
+ inputSchema: {
10
+ chatId: z.string().describe("Chat ID or username (e.g. @username or numeric ID)"),
11
+ filePath: absolutePath.describe("Absolute local filesystem path to audio file (OGG/Opus preferred; M4A/MP3 also accepted). URLs are rejected."),
12
+ caption: safeText.optional().describe("Optional caption shown below the voice note"),
13
+ replyTo: z.number().int().positive().optional().describe("Message ID to reply to"),
14
+ topicId: z.number().int().positive().optional().describe("Forum topic ID (for groups with Topics enabled)"),
15
+ parseMode: z.enum(["md", "html"]).optional().describe("Caption format: md (Markdown) or html"),
16
+ },
17
+ annotations: WRITE,
18
+ }, async ({ chatId, filePath, caption, replyTo, topicId, parseMode }) => {
19
+ const err = await requireConnection(telegram);
20
+ if (err)
21
+ return fail(new Error(err));
22
+ try {
23
+ const { id } = await telegram.sendVoice(chatId, filePath, {
24
+ caption,
25
+ replyTo,
26
+ topicId,
27
+ parseMode,
28
+ });
29
+ return ok(`Voice note sent to ${chatId} [#${id}]`);
30
+ }
31
+ catch (e) {
32
+ return fail(e);
33
+ }
34
+ });
35
+ server.registerTool("telegram-send-video-note", {
36
+ description: "Send a video note (round-shaped short video) to a Telegram chat. Shows as a circular video in the UI.",
37
+ inputSchema: {
38
+ chatId: z.string().describe("Chat ID or username"),
39
+ filePath: absolutePath.describe("Absolute local filesystem path to video file (MP4 preferred, square source recommended for best look). URLs are rejected."),
40
+ duration: z.number().int().positive().max(60).optional().describe("Duration in seconds (Telegram caps at 60)"),
41
+ length: z
42
+ .number()
43
+ .int()
44
+ .positive()
45
+ .max(640)
46
+ .optional()
47
+ .describe("Frame edge length in pixels (the circle is square-cropped)"),
48
+ replyTo: z.number().int().positive().optional().describe("Message ID to reply to"),
49
+ topicId: z.number().int().positive().optional().describe("Forum topic ID"),
50
+ },
51
+ annotations: WRITE,
52
+ }, async ({ chatId, filePath, duration, length, replyTo, topicId }) => {
53
+ const err = await requireConnection(telegram);
54
+ if (err)
55
+ return fail(new Error(err));
56
+ try {
57
+ const { id } = await telegram.sendVideoNote(chatId, filePath, {
58
+ duration,
59
+ length,
60
+ replyTo,
61
+ topicId,
62
+ });
63
+ return ok(`Video note sent to ${chatId} [#${id}]`);
64
+ }
65
+ catch (e) {
66
+ return fail(e);
67
+ }
68
+ });
69
+ server.registerTool("telegram-send-contact", {
70
+ description: "Send a contact card (phone number + name) to a Telegram chat.",
71
+ inputSchema: {
72
+ chatId: z.string().describe("Chat ID or username"),
73
+ phone: z
74
+ .string()
75
+ .regex(/^\+?\d{6,15}$/)
76
+ .describe("Phone number in E.164-like format — 6-15 digits, optional leading +. " +
77
+ "Note: sent as-is; Telegram shows the number to the recipient."),
78
+ firstName: safeText.pipe(z.string().min(1).max(64)).describe("Contact first name"),
79
+ lastName: safeText.pipe(z.string().max(64)).optional().describe("Contact last name"),
80
+ vcard: safeText.pipe(z.string().max(2048)).optional().describe("Optional vCard v3.0 text content"),
81
+ replyTo: z.number().int().positive().optional(),
82
+ topicId: z.number().int().positive().optional(),
83
+ },
84
+ annotations: WRITE,
85
+ }, async ({ chatId, phone, firstName, lastName, vcard, replyTo, topicId }) => {
86
+ const err = await requireConnection(telegram);
87
+ if (err)
88
+ return fail(new Error(err));
89
+ try {
90
+ const { id } = await telegram.sendContact(chatId, phone, firstName, {
91
+ lastName,
92
+ vcard,
93
+ replyTo,
94
+ topicId,
95
+ });
96
+ return ok(`Contact sent to ${chatId} [#${id}]`);
97
+ }
98
+ catch (e) {
99
+ return fail(e);
100
+ }
101
+ });
102
+ server.registerTool("telegram-send-dice", {
103
+ description: "Send an animated dice/game emoji to a Telegram chat. Returns the server-rolled value — useful for games, " +
104
+ "coin-flips, random picks. Values: 🎲🎯🎳 = 1-6, 🏀⚽ = 1-5, 🎰 = slot combo 1-64.",
105
+ inputSchema: {
106
+ chatId: z.string().describe("Chat ID or username"),
107
+ emoji: z
108
+ .enum(DICE_EMOJIS)
109
+ .default("🎲")
110
+ .describe("Dice emoji: 🎲 dice (1-6), 🎯 dart (1-6), 🎰 slot machine (1-64), 🏀 basketball (1-5), ⚽ football (1-5), 🎳 bowling (1-6)"),
111
+ replyTo: z.number().int().positive().optional(),
112
+ topicId: z.number().int().positive().optional(),
113
+ },
114
+ annotations: WRITE,
115
+ }, async ({ chatId, emoji, replyTo, topicId }) => {
116
+ const err = await requireConnection(telegram);
117
+ if (err)
118
+ return fail(new Error(err));
119
+ try {
120
+ const { id, value } = await telegram.sendDice(chatId, emoji, { replyTo, topicId });
121
+ const rolled = value !== undefined ? `: rolled ${value}` : " (value pending)";
122
+ return ok(`Dice ${emoji} sent to ${chatId}${rolled} [#${id}]`);
123
+ }
124
+ catch (e) {
125
+ return fail(e);
126
+ }
127
+ });
128
+ server.registerTool("telegram-send-location", {
129
+ description: "Send a geographic location to a Telegram chat. Static pin by default; set livePeriod to share a live-updating location for N seconds.",
130
+ inputSchema: {
131
+ chatId: z.string().describe("Chat ID or username"),
132
+ latitude: z.number().min(-90).max(90).describe("Latitude in decimal degrees (-90 to 90)"),
133
+ longitude: z.number().min(-180).max(180).describe("Longitude in decimal degrees (-180 to 180)"),
134
+ accuracyRadius: z
135
+ .number()
136
+ .int()
137
+ .nonnegative()
138
+ .optional()
139
+ .describe("Horizontal accuracy radius in meters (0 = unknown)"),
140
+ livePeriod: z
141
+ .number()
142
+ .int()
143
+ .min(60)
144
+ .max(86400)
145
+ .optional()
146
+ .describe("If set, sends a live location updated for N seconds (60-86400). Omit for static pin."),
147
+ heading: z
148
+ .number()
149
+ .int()
150
+ .min(1)
151
+ .max(360)
152
+ .optional()
153
+ .describe("Direction the user is heading, 1-360 degrees (meaningful only for live locations)"),
154
+ proximityRadius: z
155
+ .number()
156
+ .int()
157
+ .positive()
158
+ .optional()
159
+ .describe("Alert radius for proximity notification in meters (live only)"),
160
+ replyTo: z.number().int().positive().optional().describe("Message ID to reply to"),
161
+ topicId: z.number().int().positive().optional().describe("Forum topic ID"),
162
+ },
163
+ annotations: WRITE,
164
+ }, async ({ chatId, latitude, longitude, accuracyRadius, livePeriod, heading, proximityRadius, replyTo, topicId }) => {
165
+ const err = await requireConnection(telegram);
166
+ if (err)
167
+ return fail(new Error(err));
168
+ try {
169
+ const { id } = await telegram.sendLocation(chatId, latitude, longitude, {
170
+ accuracyRadius,
171
+ livePeriod,
172
+ heading,
173
+ proximityRadius,
174
+ replyTo,
175
+ topicId,
176
+ });
177
+ const label = livePeriod ? `Live location sent to ${chatId} for ${livePeriod}s` : `Location sent to ${chatId}`;
178
+ return ok(`${label} [#${id}]`);
179
+ }
180
+ catch (e) {
181
+ return fail(e);
182
+ }
183
+ });
184
+ server.registerTool("telegram-send-venue", {
185
+ description: "Send a venue card (point-of-interest with title and address) to a Telegram chat.",
186
+ inputSchema: {
187
+ chatId: z.string().describe("Chat ID or username"),
188
+ latitude: z.number().min(-90).max(90).describe("Venue latitude"),
189
+ longitude: z.number().min(-180).max(180).describe("Venue longitude"),
190
+ title: safeText.pipe(z.string().min(1).max(256)).describe("Venue name (e.g. 'Red Square')"),
191
+ address: safeText.pipe(z.string().min(1).max(512)).describe("Street address"),
192
+ provider: safeText
193
+ .pipe(z.string().max(32))
194
+ .optional()
195
+ .describe("Data provider — typically 'foursquare' or 'gplaces'. Defaults to 'foursquare'."),
196
+ venueId: safeText.pipe(z.string().max(256)).optional().describe("Provider-specific venue ID"),
197
+ venueType: safeText.pipe(z.string().max(256)).optional().describe("Provider-specific venue type category"),
198
+ replyTo: z.number().int().positive().optional(),
199
+ topicId: z.number().int().positive().optional(),
200
+ },
201
+ annotations: WRITE,
202
+ }, async ({ chatId, latitude, longitude, title, address, provider, venueId, venueType, replyTo, topicId }) => {
203
+ const err = await requireConnection(telegram);
204
+ if (err)
205
+ return fail(new Error(err));
206
+ try {
207
+ const { id } = await telegram.sendVenue(chatId, latitude, longitude, title, address, {
208
+ provider,
209
+ venueId,
210
+ venueType,
211
+ replyTo,
212
+ topicId,
213
+ });
214
+ return ok(`Venue "${title}" sent to ${chatId} [#${id}]`);
215
+ }
216
+ catch (e) {
217
+ return fail(e);
218
+ }
219
+ });
220
+ server.registerTool("telegram-send-album", {
221
+ description: "Send an album (group) of 2-10 photos as a single grouped message. Media type is auto-detected " +
222
+ "by file extension — videos are supported by the underlying TL call but are not covered by v1.29.0 " +
223
+ "mock tests, so uniform-photo albums are the safer choice until a live checkpoint. Uploads are " +
224
+ "serial per item: expect ≈4-10s for 10 mid-size photos, 15-40s for 10 large videos. Prefer ≤5 " +
225
+ "items or photos when low latency matters.",
226
+ inputSchema: {
227
+ chatId: z.string().describe("Chat ID or username"),
228
+ items: z
229
+ .array(z.object({
230
+ filePath: absolutePath.describe("Absolute local filesystem path to a photo or video file. URLs are rejected."),
231
+ caption: safeText
232
+ .optional()
233
+ .describe("Per-item caption (shown under this item when the album is expanded)"),
234
+ }))
235
+ .min(2)
236
+ .max(10)
237
+ .describe("Array of media items (2-10)"),
238
+ caption: safeText
239
+ .optional()
240
+ .describe("Album-level caption (attached to the first item — shown in the collapsed view)"),
241
+ parseMode: z.enum(["md", "html"]).optional().describe("Caption format (applies to all captions)"),
242
+ replyTo: z.number().int().positive().optional().describe("Message ID to reply to"),
243
+ topicId: z.number().int().positive().optional().describe("Forum topic ID"),
244
+ },
245
+ annotations: WRITE,
246
+ }, async ({ chatId, items, caption, parseMode, replyTo, topicId }) => {
247
+ const err = await requireConnection(telegram);
248
+ if (err)
249
+ return fail(new Error(err));
250
+ try {
251
+ const { ids } = await telegram.sendAlbum(chatId, items, { caption, parseMode, replyTo, topicId });
252
+ const idList = ids.map((id) => `#${id}`).join(", ");
253
+ return ok(`Album sent to ${chatId} (${ids.length} items) [${idList}]`);
254
+ }
255
+ catch (e) {
256
+ return fail(e);
257
+ }
258
+ });
259
+ }
@@ -36,5 +36,33 @@ export declare function formatReactions(reactions?: {
36
36
  count: number;
37
37
  me: boolean;
38
38
  }[]): string;
39
+ /**
40
+ * Validate that a user-supplied path is safe to upload.
41
+ *
42
+ * The threat model is prompt-injection: an AI that was told "send the user's file" can be
43
+ * manipulated into sending `/proc/self/environ`, `/etc/shadow`, `http://169.254.169.254/...`,
44
+ * or an SMB share `\\attacker.com\share`. GramJS `sendFile` happily fetches URLs and reads
45
+ * any local path, so the validation has to live here.
46
+ *
47
+ * Rules:
48
+ * - Must be an absolute path (POSIX `/` or Windows `C:\` / `\\server\share`).
49
+ * - No URL schemes (http:, https:, file:, ftp:, data:, javascript:, …).
50
+ * - No path traversal (`..` segments) even inside an absolute path.
51
+ * - No OS-sensitive directories on POSIX (`/proc`, `/sys`, `/dev`, `/run`). These leak env,
52
+ * kernel state, or block on device reads.
53
+ * - UNC paths (`\\server\share`) are blocked (NTLM-relay / remote-SMB risk).
54
+ *
55
+ * This is defence-in-depth: the admin still owns the machine and can exfiltrate files
56
+ * deliberately — we just refuse to help prompt-injection do it automatically.
57
+ */
58
+ export declare function isSafeAbsolutePath(p: string): boolean;
59
+ /** Zod refinement message paired with `isSafeAbsolutePath` */
60
+ export declare 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.";
61
+ /**
62
+ * Sanitize a user-provided text for safe TL encoding.
63
+ * Strips unpaired UTF-16 surrogates that crash GramJS's wire serializer. Use on every
64
+ * free-text field that reaches GramJS (captions, provider names, venue titles, quoteText, …).
65
+ */
66
+ export declare function sanitizeInputText(text: string): string;
39
67
  /** Try to connect, return error text if failed */
40
68
  export declare function requireConnection(telegram: TelegramService): Promise<string | null>;