From b00852b77c92dd4162e02a57079125a2a4d8c19e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Wed, 28 Jan 2026 06:56:23 +0530 Subject: [PATCH] Implement Telegram MCP tools for chat management and user interactions - Developed various Telegram tools for managing chats, including archiving, unarchiving, muting, and unmuting chats, as well as inviting users to groups and managing chat photos. - Enhanced user management capabilities with tools for banning, unbanning, promoting, and demoting users in groups and channels. - Integrated robust error handling and input validation across all tools, ensuring clear feedback for invalid inputs and operational errors. - Improved logging and error formatting to streamline error management and enhance the reliability of chat and user operations. These updates significantly enhance the Telegram MCP tools, providing comprehensive functionalities for chat and user management while ensuring a smooth user experience through improved error handling and validation. --- src/lib/mcp/telegram/tools/archiveChat.ts | 44 ++++++++++-- src/lib/mcp/telegram/tools/banUser.ts | 57 +++++++++++++-- src/lib/mcp/telegram/tools/createChannel.ts | 42 +++++++++-- src/lib/mcp/telegram/tools/createGroup.ts | 43 +++++++++-- src/lib/mcp/telegram/tools/deleteChatPhoto.ts | 53 ++++++++++++-- src/lib/mcp/telegram/tools/demoteAdmin.ts | 48 +++++++++++-- src/lib/mcp/telegram/tools/editChatTitle.ts | 55 ++++++++++++-- .../mcp/telegram/tools/exportChatInvite.ts | 56 +++++++++++++-- src/lib/mcp/telegram/tools/exportContacts.ts | 34 ++++++++- src/lib/mcp/telegram/tools/getAdmins.ts | 71 +++++++++++++++++-- src/lib/mcp/telegram/tools/getBannedUsers.ts | 62 ++++++++++++++-- src/lib/mcp/telegram/tools/getBlockedUsers.ts | 43 +++++++++-- src/lib/mcp/telegram/tools/getInviteLink.ts | 47 ++++++++++-- src/lib/mcp/telegram/tools/getParticipants.ts | 71 +++++++++++++++++-- .../mcp/telegram/tools/importChatInvite.ts | 34 +++++++-- src/lib/mcp/telegram/tools/importContacts.ts | 55 ++++++++++++-- src/lib/mcp/telegram/tools/inviteToGroup.ts | 63 ++++++++++++++-- src/lib/mcp/telegram/tools/joinChatByLink.ts | 41 +++++++++-- src/lib/mcp/telegram/tools/leaveChat.ts | 53 ++++++++++++-- src/lib/mcp/telegram/tools/listContacts.ts | 35 ++++++++- src/lib/mcp/telegram/tools/muteChat.ts | 45 ++++++++++-- src/lib/mcp/telegram/tools/promoteAdmin.ts | 55 ++++++++++++-- src/lib/mcp/telegram/tools/searchContacts.ts | 45 ++++++++++-- .../telegram/tools/subscribePublicChannel.ts | 36 ++++++++-- src/lib/mcp/telegram/tools/unarchiveChat.ts | 44 ++++++++++-- src/lib/mcp/telegram/tools/unbanUser.ts | 47 ++++++++++-- src/lib/mcp/telegram/tools/unblockUser.ts | 31 ++++++-- src/lib/mcp/telegram/tools/unmuteChat.ts | 44 ++++++++++-- 28 files changed, 1219 insertions(+), 135 deletions(-) diff --git a/src/lib/mcp/telegram/tools/archiveChat.ts b/src/lib/mcp/telegram/tools/archiveChat.ts index 399d091aa..2d302e951 100644 --- a/src/lib/mcp/telegram/tools/archiveChat.ts +++ b/src/lib/mcp/telegram/tools/archiveChat.ts @@ -1,20 +1,56 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'archive_chat', description: 'Archive a chat', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function archiveChat( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('archive_chat'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + await client.invoke( + new Api.folders.EditPeerFolders({ + folderPeers: [ + new Api.InputFolderPeer({ + peer: inputPeer, + folderId: 1, // 1 = Archive folder + }), + ], + }), + ); + }); + + return { content: [{ type: 'text', text: `Chat ${chatId} archived.` }] }; + } catch (error) { + return logAndFormatError( + 'archive_chat', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CHAT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/banUser.ts b/src/lib/mcp/telegram/tools/banUser.ts index c4ae2a497..ea7726ce3 100644 --- a/src/lib/mcp/telegram/tools/banUser.ts +++ b/src/lib/mcp/telegram/tools/banUser.ts @@ -1,23 +1,70 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'ban_user', - description: 'Ban user from chat', + description: 'Ban a user from a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - user_id: { type: 'string', description: 'User ID' }, + user_id: { type: 'string', description: 'User ID to ban' }, }, required: ['chat_id', 'user_id'], }, }; export async function banUser( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('ban_user'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const userId = validateId(args.user_id, 'user_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + if (chat.type !== 'channel' && chat.type !== 'supergroup') { + return { content: [{ type: 'text', text: 'Ban is only available for channels/supergroups.' }], isError: true }; + } + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + const inputUser = await client.getInputEntity(userId); + await client.invoke( + new Api.channels.EditBanned({ + channel: inputChannel as Api.TypeInputChannel, + participant: inputUser as Api.TypeInputPeer, + bannedRights: new Api.ChatBannedRights({ + viewMessages: true, + sendMessages: true, + sendMedia: true, + sendStickers: true, + sendGifs: true, + sendGames: true, + sendInline: true, + embedLinks: true, + untilDate: 0, + }), + }), + ); + }); + + return { content: [{ type: 'text', text: `User ${userId} banned from ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'ban_user', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/createChannel.ts b/src/lib/mcp/telegram/tools/createChannel.ts index 5ff529688..6ca187f2a 100644 --- a/src/lib/mcp/telegram/tools/createChannel.ts +++ b/src/lib/mcp/telegram/tools/createChannel.ts @@ -1,23 +1,55 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optString } from '../args'; export const tool: MCPTool = { name: 'create_channel', - description: 'Create a channel', + description: 'Create a new channel', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Channel title' }, - description: { type: 'string', description: 'Channel description' }, + about: { type: 'string', description: 'Channel description' }, + megagroup: { type: 'boolean', description: 'Create as supergroup instead of channel', default: false }, }, required: ['title'], }, }; export async function createChannel( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('create_channel'); + try { + const title = typeof args.title === 'string' ? args.title : ''; + if (!title) return { content: [{ type: 'text', text: 'title is required' }], isError: true }; + + const about = optString(args, 'about') ?? ''; + const megagroup = args.megagroup === true; + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke( + new Api.channels.CreateChannel({ + title, + about, + megagroup, + broadcast: !megagroup, + }), + ); + }); + + const channelId = (result as any)?.chats?.[0]?.id ?? 'unknown'; + const type = megagroup ? 'Supergroup' : 'Channel'; + return { content: [{ type: 'text', text: `${type} "${title}" created. ID: ${channelId}` }] }; + } catch (error) { + return logAndFormatError( + 'create_channel', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/createGroup.ts b/src/lib/mcp/telegram/tools/createGroup.ts index 9e647b60e..b0440cdb9 100644 --- a/src/lib/mcp/telegram/tools/createGroup.ts +++ b/src/lib/mcp/telegram/tools/createGroup.ts @@ -1,23 +1,54 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'create_group', - description: 'Create a group', + description: 'Create a new group chat', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Group title' }, - user_ids: { type: 'array', description: 'User IDs to add' }, + user_ids: { type: 'array', items: { type: 'string' }, description: 'User IDs to add' }, }, - required: ['title'], + required: ['title', 'user_ids'], }, }; export async function createGroup( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('create_group'); + try { + const title = typeof args.title === 'string' ? args.title : ''; + if (!title) return { content: [{ type: 'text', text: 'title is required' }], isError: true }; + + const userIds = Array.isArray(args.user_ids) ? args.user_ids : []; + if (userIds.length === 0) return { content: [{ type: 'text', text: 'user_ids must not be empty' }], isError: true }; + + const client = mtprotoService.getClient(); + + const users: Api.TypeInputUser[] = []; + for (const uid of userIds) { + const inputUser = await client.getInputEntity(String(uid)); + users.push(inputUser as Api.TypeInputUser); + } + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke( + new Api.messages.CreateChat({ title, users }), + ); + }); + + const chatId = (result as any)?.chats?.[0]?.id ?? 'unknown'; + return { content: [{ type: 'text', text: `Group "${title}" created. Chat ID: ${chatId}` }] }; + } catch (error) { + return logAndFormatError( + 'create_group', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/deleteChatPhoto.ts b/src/lib/mcp/telegram/tools/deleteChatPhoto.ts index 696df7ca6..fff44ea83 100644 --- a/src/lib/mcp/telegram/tools/deleteChatPhoto.ts +++ b/src/lib/mcp/telegram/tools/deleteChatPhoto.ts @@ -1,20 +1,63 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'delete_chat_photo', - description: 'Delete chat photo', + description: 'Delete the photo of a chat', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function deleteChatPhoto( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('delete_chat_photo'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + if (chat.type === 'channel' || chat.type === 'supergroup') { + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + await client.invoke( + new Api.channels.EditPhoto({ + channel: inputChannel as Api.TypeInputChannel, + photo: new Api.InputChatPhotoEmpty(), + }), + ); + }); + } else { + await mtprotoService.withFloodWaitHandling(async () => { + await client.invoke( + new Api.messages.EditChatPhoto({ + chatId: BigInt(chat.id), + photo: new Api.InputChatPhotoEmpty(), + }), + ); + }); + } + + return { content: [{ type: 'text', text: `Chat photo deleted for ${chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'delete_chat_photo', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/demoteAdmin.ts b/src/lib/mcp/telegram/tools/demoteAdmin.ts index 42ef2b011..3dd038c93 100644 --- a/src/lib/mcp/telegram/tools/demoteAdmin.ts +++ b/src/lib/mcp/telegram/tools/demoteAdmin.ts @@ -1,23 +1,61 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'demote_admin', - description: 'Demote admin', + description: 'Demote an admin in a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - user_id: { type: 'string', description: 'User ID' }, + user_id: { type: 'string', description: 'User ID to demote' }, }, required: ['chat_id', 'user_id'], }, }; export async function demoteAdmin( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('demote_admin'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const userId = validateId(args.user_id, 'user_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + if (chat.type !== 'channel' && chat.type !== 'supergroup') { + return { content: [{ type: 'text', text: 'Admin demotion is only available for channels/supergroups.' }], isError: true }; + } + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + const inputUser = await client.getInputEntity(userId); + await client.invoke( + new Api.channels.EditAdmin({ + channel: inputChannel as Api.TypeInputChannel, + userId: inputUser as Api.TypeInputUser, + adminRights: new Api.ChatAdminRights({}), + rank: '', + }), + ); + }); + + return { content: [{ type: 'text', text: `User ${userId} demoted in ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'demote_admin', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/editChatTitle.ts b/src/lib/mcp/telegram/tools/editChatTitle.ts index 070598302..8c4ccfdc0 100644 --- a/src/lib/mcp/telegram/tools/editChatTitle.ts +++ b/src/lib/mcp/telegram/tools/editChatTitle.ts @@ -1,23 +1,66 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'edit_chat_title', - description: 'Edit chat title', + description: 'Edit the title of a chat', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - new_title: { type: 'string', description: 'New title' }, + title: { type: 'string', description: 'New title' }, }, - required: ['chat_id', 'new_title'], + required: ['chat_id', 'title'], }, }; export async function editChatTitle( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('edit_chat_title'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const title = typeof args.title === 'string' ? args.title : ''; + if (!title) return { content: [{ type: 'text', text: 'title is required' }], isError: true }; + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + if (chat.type === 'channel' || chat.type === 'supergroup') { + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + await client.invoke( + new Api.channels.EditTitle({ + channel: inputChannel as Api.TypeInputChannel, + title, + }), + ); + }); + } else { + await mtprotoService.withFloodWaitHandling(async () => { + await client.invoke( + new Api.messages.EditChatTitle({ + chatId: BigInt(chat.id), + title, + }), + ); + }); + } + + return { content: [{ type: 'text', text: `Chat title updated to "${title}".` }] }; + } catch (error) { + return logAndFormatError( + 'edit_chat_title', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/exportChatInvite.ts b/src/lib/mcp/telegram/tools/exportChatInvite.ts index aa27022e5..21184ff78 100644 --- a/src/lib/mcp/telegram/tools/exportChatInvite.ts +++ b/src/lib/mcp/telegram/tools/exportChatInvite.ts @@ -1,20 +1,66 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optString } from '../args'; export const tool: MCPTool = { name: 'export_chat_invite', - description: 'Create invite link for a chat', + description: 'Export a new chat invite link', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + title: { type: 'string', description: 'Link title' }, + expire_date: { type: 'number', description: 'Expiration timestamp' }, + usage_limit: { type: 'number', description: 'Max number of uses' }, + }, required: ['chat_id'], }, }; export async function exportChatInvite( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('export_chat_invite'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const title = optString(args, 'title'); + const expireDate = typeof args.expire_date === 'number' ? args.expire_date : undefined; + const usageLimit = typeof args.usage_limit === 'number' ? args.usage_limit : undefined; + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + const result = await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + return client.invoke( + new Api.messages.ExportChatInvite({ + peer: inputPeer, + title: title ?? undefined, + expireDate: expireDate ?? undefined, + usageLimit: usageLimit ?? undefined, + }), + ); + }); + + const link = (result as any)?.link; + if (!link) { + return { content: [{ type: 'text', text: 'Could not create invite link.' }], isError: true }; + } + + return { content: [{ type: 'text', text: `Invite link created: ${link}` }] }; + } catch (error) { + return logAndFormatError( + 'export_chat_invite', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/exportContacts.ts b/src/lib/mcp/telegram/tools/exportContacts.ts index 7f68316d8..ed0993fbf 100644 --- a/src/lib/mcp/telegram/tools/exportContacts.ts +++ b/src/lib/mcp/telegram/tools/exportContacts.ts @@ -1,10 +1,12 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'export_contacts', - description: 'Export contacts', + description: 'Export all contacts from Telegram', inputSchema: { type: 'object', properties: {} }, }; @@ -12,5 +14,31 @@ export async function exportContacts( _args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('export_contacts'); + try { + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.contacts.GetContacts({ hash: BigInt(0) })); + }); + + if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) { + return { content: [{ type: 'text', text: 'No contacts to export.' }] }; + } + + const contacts = result.users.map((u: any) => ({ + id: String(u.id), + firstName: u.firstName ?? '', + lastName: u.lastName ?? '', + username: u.username ?? '', + phone: u.phone ?? '', + })); + + return { content: [{ type: 'text', text: JSON.stringify(contacts, null, 2) }] }; + } catch (error) { + return logAndFormatError( + 'export_contacts', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/getAdmins.ts b/src/lib/mcp/telegram/tools/getAdmins.ts index df61aba60..efa04ca6f 100644 --- a/src/lib/mcp/telegram/tools/getAdmins.ts +++ b/src/lib/mcp/telegram/tools/getAdmins.ts @@ -1,20 +1,81 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'get_admins', - description: 'Get admins of a chat', + description: 'Get admins of a group or channel', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function getAdmins( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('get_admins'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + let admins: any[] = []; + + if (chat.type === 'channel' || chat.type === 'supergroup') { + const result = await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + return client.invoke( + new Api.channels.GetParticipants({ + channel: inputChannel as Api.TypeInputChannel, + filter: new Api.ChannelParticipantsAdmins(), + offset: 0, + limit: 100, + hash: BigInt(0), + }), + ); + }); + if (result && 'users' in result && Array.isArray(result.users)) { + admins = result.users; + } + } else { + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke( + new Api.messages.GetFullChat({ chatId: BigInt(chat.id) }), + ); + }); + if (result && 'users' in result && Array.isArray(result.users)) { + admins = result.users; + } + } + + if (admins.length === 0) { + return { content: [{ type: 'text', text: 'No admins found.' }] }; + } + + const lines = admins.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + return `ID: ${u.id} | ${name} ${username}`.trim(); + }); + + return { content: [{ type: 'text', text: `${lines.length} admins:\n${lines.join('\n')}` }] }; + } catch (error) { + return logAndFormatError( + 'get_admins', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/getBannedUsers.ts b/src/lib/mcp/telegram/tools/getBannedUsers.ts index 2b92993fb..d612c1eb8 100644 --- a/src/lib/mcp/telegram/tools/getBannedUsers.ts +++ b/src/lib/mcp/telegram/tools/getBannedUsers.ts @@ -1,20 +1,72 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optNumber } from '../args'; export const tool: MCPTool = { name: 'get_banned_users', - description: 'Get banned users in a chat', + description: 'Get banned users in a group or channel', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + limit: { type: 'number', description: 'Max results', default: 50 }, + }, required: ['chat_id'], }, }; export async function getBannedUsers( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('get_banned_users'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const limit = optNumber(args, 'limit', 50); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + if (chat.type !== 'channel' && chat.type !== 'supergroup') { + return { content: [{ type: 'text', text: 'Banned users list is only available for channels/supergroups.' }], isError: true }; + } + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + const result = await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + return client.invoke( + new Api.channels.GetParticipants({ + channel: inputChannel as Api.TypeInputChannel, + filter: new Api.ChannelParticipantsKicked({ q: '' }), + offset: 0, + limit, + hash: BigInt(0), + }), + ); + }); + + if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) { + return { content: [{ type: 'text', text: 'No banned users found.' }] }; + } + + const lines = result.users.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + return `ID: ${u.id} | ${name} ${username}`.trim(); + }); + + return { content: [{ type: 'text', text: `${lines.length} banned users:\n${lines.join('\n')}` }] }; + } catch (error) { + return logAndFormatError( + 'get_banned_users', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/getBlockedUsers.ts b/src/lib/mcp/telegram/tools/getBlockedUsers.ts index 75a5ba20b..d23dcd1de 100644 --- a/src/lib/mcp/telegram/tools/getBlockedUsers.ts +++ b/src/lib/mcp/telegram/tools/getBlockedUsers.ts @@ -1,16 +1,51 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optNumber } from '../args'; export const tool: MCPTool = { name: 'get_blocked_users', description: 'Get list of blocked users', - inputSchema: { type: 'object', properties: {} }, + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max results', default: 50 }, + }, + }, }; export async function getBlockedUsers( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('get_blocked_users'); + try { + const limit = optNumber(args, 'limit', 50); + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke( + new Api.contacts.GetBlocked({ offset: 0, limit }), + ); + }); + + if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) { + return { content: [{ type: 'text', text: 'No blocked users.' }] }; + } + + const lines = result.users.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + return `ID: ${u.id} | ${name} ${username}`.trim(); + }); + + return { content: [{ type: 'text', text: lines.join('\n') }] }; + } catch (error) { + return logAndFormatError( + 'get_blocked_users', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/getInviteLink.ts b/src/lib/mcp/telegram/tools/getInviteLink.ts index 08a43c2c2..205fe5fdc 100644 --- a/src/lib/mcp/telegram/tools/getInviteLink.ts +++ b/src/lib/mcp/telegram/tools/getInviteLink.ts @@ -1,20 +1,57 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'get_invite_link', - description: 'Get invite link for a chat', + description: 'Get the invite link for a chat', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function getInviteLink( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('get_invite_link'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + const result = await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + return client.invoke( + new Api.messages.ExportChatInvite({ + peer: inputPeer, + legacyRevokePermanent: true, + }), + ); + }); + + const link = (result as any)?.link; + if (!link) { + return { content: [{ type: 'text', text: 'Could not generate invite link.' }], isError: true }; + } + + return { content: [{ type: 'text', text: `Invite link: ${link}` }] }; + } catch (error) { + return logAndFormatError( + 'get_invite_link', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/getParticipants.ts b/src/lib/mcp/telegram/tools/getParticipants.ts index b709aa374..281827289 100644 --- a/src/lib/mcp/telegram/tools/getParticipants.ts +++ b/src/lib/mcp/telegram/tools/getParticipants.ts @@ -1,23 +1,84 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optNumber } from '../args'; export const tool: MCPTool = { name: 'get_participants', - description: 'Get participants in a chat', + description: 'Get participants of a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - limit: { type: 'number', description: 'Max participants', default: 100 }, + limit: { type: 'number', description: 'Max results', default: 50 }, }, required: ['chat_id'], }, }; export async function getParticipants( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('get_participants'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const limit = optNumber(args, 'limit', 50); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + let participants: any[] = []; + + if (chat.type === 'channel' || chat.type === 'supergroup') { + const result = await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + return client.invoke( + new Api.channels.GetParticipants({ + channel: inputChannel as Api.TypeInputChannel, + filter: new Api.ChannelParticipantsRecent(), + offset: 0, + limit, + hash: BigInt(0), + }), + ); + }); + if (result && 'users' in result && Array.isArray(result.users)) { + participants = result.users; + } + } else { + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke( + new Api.messages.GetFullChat({ chatId: BigInt(chat.id) }), + ); + }); + if (result && 'users' in result && Array.isArray(result.users)) { + participants = result.users; + } + } + + if (participants.length === 0) { + return { content: [{ type: 'text', text: 'No participants found.' }] }; + } + + const lines = participants.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + return `ID: ${u.id} | ${name} ${username}`.trim(); + }); + + return { content: [{ type: 'text', text: `${lines.length} participants:\n${lines.join('\n')}` }] }; + } catch (error) { + return logAndFormatError( + 'get_participants', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/importChatInvite.ts b/src/lib/mcp/telegram/tools/importChatInvite.ts index c178cb321..178e52147 100644 --- a/src/lib/mcp/telegram/tools/importChatInvite.ts +++ b/src/lib/mcp/telegram/tools/importChatInvite.ts @@ -1,20 +1,42 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'import_chat_invite', - description: 'Join chat via invite hash', + description: 'Join a chat using an invite hash', inputSchema: { type: 'object', - properties: { invite_hash: { type: 'string', description: 'Invite hash' } }, - required: ['invite_hash'], + properties: { + hash: { type: 'string', description: 'Invite hash (from t.me/+HASH or t.me/joinchat/HASH)' }, + }, + required: ['hash'], }, }; export async function importChatInvite( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('import_chat_invite'); + try { + const hash = typeof args.hash === 'string' ? args.hash : ''; + if (!hash) return { content: [{ type: 'text', text: 'hash is required' }], isError: true }; + + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.messages.ImportChatInvite({ hash })); + }); + + const chatTitle = (result as any)?.chats?.[0]?.title ?? 'unknown'; + return { content: [{ type: 'text', text: `Joined chat: ${chatTitle}` }] }; + } catch (error) { + return logAndFormatError( + 'import_chat_invite', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/importContacts.ts b/src/lib/mcp/telegram/tools/importContacts.ts index 3d6523455..b5d140b08 100644 --- a/src/lib/mcp/telegram/tools/importContacts.ts +++ b/src/lib/mcp/telegram/tools/importContacts.ts @@ -1,20 +1,65 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'import_contacts', - description: 'Import contacts', + description: 'Import contacts to Telegram', inputSchema: { type: 'object', - properties: { contacts: { type: 'array', description: 'Contacts to import' } }, + properties: { + contacts: { + type: 'array', + description: 'Array of contacts: [{phone, first_name, last_name?}]', + items: { + type: 'object', + properties: { + phone: { type: 'string' }, + first_name: { type: 'string' }, + last_name: { type: 'string' }, + }, + required: ['phone', 'first_name'], + }, + }, + }, required: ['contacts'], }, }; export async function importContacts( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('import_contacts'); + try { + const contactsArg = args.contacts; + if (!Array.isArray(contactsArg) || contactsArg.length === 0) { + return { content: [{ type: 'text', text: 'contacts array is required and must not be empty.' }], isError: true }; + } + + const inputContacts = contactsArg.map((c: any, i: number) => + new Api.InputPhoneContact({ + clientId: BigInt(i), + phone: String(c.phone ?? ''), + firstName: String(c.first_name ?? ''), + lastName: String(c.last_name ?? ''), + }), + ); + + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.contacts.ImportContacts({ contacts: inputContacts })); + }); + + const imported = (result as any)?.imported?.length ?? 0; + return { content: [{ type: 'text', text: `Imported ${imported} of ${contactsArg.length} contacts.` }] }; + } catch (error) { + return logAndFormatError( + 'import_contacts', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/inviteToGroup.ts b/src/lib/mcp/telegram/tools/inviteToGroup.ts index 9cf18f60f..253d82996 100644 --- a/src/lib/mcp/telegram/tools/inviteToGroup.ts +++ b/src/lib/mcp/telegram/tools/inviteToGroup.ts @@ -1,23 +1,76 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'invite_to_group', - description: 'Invite users to a group', + description: 'Invite users to a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - user_ids: { type: 'array', description: 'User IDs to invite' }, + user_ids: { type: 'array', items: { type: 'string' }, description: 'User IDs to invite' }, }, required: ['chat_id', 'user_ids'], }, }; export async function inviteToGroup( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('invite_to_group'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const userIds = Array.isArray(args.user_ids) ? args.user_ids : []; + if (userIds.length === 0) return { content: [{ type: 'text', text: 'user_ids must not be empty' }], isError: true }; + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + const users: Api.TypeInputUser[] = []; + for (const uid of userIds) { + const inputUser = await client.getInputEntity(String(uid)); + users.push(inputUser as Api.TypeInputUser); + } + + const inputPeer = await client.getInputEntity(entity); + + if (chat.type === 'channel' || chat.type === 'supergroup') { + await mtprotoService.withFloodWaitHandling(async () => { + await client.invoke( + new Api.channels.InviteToChannel({ + channel: inputPeer as Api.TypeInputChannel, + users, + }), + ); + }); + } else { + for (const user of users) { + await mtprotoService.withFloodWaitHandling(async () => { + await client.invoke( + new Api.messages.AddChatUser({ + chatId: BigInt(chat.id), + userId: user, + fwdLimit: 100, + }), + ); + }); + } + } + + return { content: [{ type: 'text', text: `Invited ${userIds.length} user(s) to ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'invite_to_group', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/joinChatByLink.ts b/src/lib/mcp/telegram/tools/joinChatByLink.ts index d1844edc0..b617228fd 100644 --- a/src/lib/mcp/telegram/tools/joinChatByLink.ts +++ b/src/lib/mcp/telegram/tools/joinChatByLink.ts @@ -1,20 +1,49 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'join_chat_by_link', - description: 'Join chat via invite link', + description: 'Join a chat using an invite link', inputSchema: { type: 'object', - properties: { invite_link: { type: 'string', description: 'Invite link URL' } }, - required: ['invite_link'], + properties: { + link: { type: 'string', description: 'Invite link (e.g. https://t.me/+HASH or https://t.me/joinchat/HASH)' }, + }, + required: ['link'], }, }; export async function joinChatByLink( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('join_chat_by_link'); + try { + const link = typeof args.link === 'string' ? args.link : ''; + if (!link) return { content: [{ type: 'text', text: 'link is required' }], isError: true }; + + // Extract hash from link + let hash = link; + const plusMatch = link.match(/t\.me\/\+(.+)/); + const joinMatch = link.match(/t\.me\/joinchat\/(.+)/); + if (plusMatch) hash = plusMatch[1]; + else if (joinMatch) hash = joinMatch[1]; + + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.messages.ImportChatInvite({ hash })); + }); + + const chatTitle = (result as any)?.chats?.[0]?.title ?? 'unknown'; + return { content: [{ type: 'text', text: `Joined chat: ${chatTitle}` }] }; + } catch (error) { + return logAndFormatError( + 'join_chat_by_link', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/leaveChat.ts b/src/lib/mcp/telegram/tools/leaveChat.ts index fcdd2e291..84ab67a5b 100644 --- a/src/lib/mcp/telegram/tools/leaveChat.ts +++ b/src/lib/mcp/telegram/tools/leaveChat.ts @@ -1,20 +1,63 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'leave_chat', - description: 'Leave a chat', + description: 'Leave a group or channel', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function leaveChat( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('leave_chat'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + if (chat.type === 'channel' || chat.type === 'supergroup') { + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + await client.invoke( + new Api.channels.LeaveChannel({ + channel: inputChannel as Api.TypeInputChannel, + }), + ); + }); + } else { + await mtprotoService.withFloodWaitHandling(async () => { + const selfUser = await client.getMe(); + await client.invoke( + new Api.messages.DeleteChatUser({ + chatId: BigInt(chat.id), + userId: new Api.InputUserSelf(), + }), + ); + }); + } + + return { content: [{ type: 'text', text: `Left chat ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'leave_chat', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/listContacts.ts b/src/lib/mcp/telegram/tools/listContacts.ts index 8cc19d6f1..ce6645718 100644 --- a/src/lib/mcp/telegram/tools/listContacts.ts +++ b/src/lib/mcp/telegram/tools/listContacts.ts @@ -1,6 +1,8 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'list_contacts', @@ -12,5 +14,34 @@ export async function listContacts( _args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('list_contacts'); + try { + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.contacts.GetContacts({ hash: BigInt(0) })); + }); + + if (!result || !('users' in result) || !Array.isArray(result.users)) { + return { content: [{ type: 'text', text: 'No contacts found.' }] }; + } + + const lines = result.users.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + const phone = u.phone ? `+${u.phone}` : ''; + return `ID: ${u.id} | ${name} ${username} ${phone}`.trim(); + }); + + if (lines.length === 0) { + return { content: [{ type: 'text', text: 'No contacts found.' }] }; + } + + return { content: [{ type: 'text', text: lines.join('\n') }] }; + } catch (error) { + return logAndFormatError( + 'list_contacts', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/muteChat.ts b/src/lib/mcp/telegram/tools/muteChat.ts index 192f09a8a..ab7ee06f3 100644 --- a/src/lib/mcp/telegram/tools/muteChat.ts +++ b/src/lib/mcp/telegram/tools/muteChat.ts @@ -1,23 +1,58 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'mute_chat', - description: 'Mute a chat', + description: 'Mute notifications for a chat', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - mute_for: { type: 'number', description: 'Mute duration in seconds' }, + duration: { type: 'number', description: 'Mute duration in seconds (0 = forever)', default: 0 }, }, required: ['chat_id'], }, }; export async function muteChat( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('mute_chat'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const duration = typeof args.duration === 'number' ? args.duration : 0; + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + const muteUntil = duration === 0 ? 2147483647 : Math.floor(Date.now() / 1000) + duration; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + await client.invoke( + new Api.account.UpdateNotifySettings({ + peer: new Api.InputNotifyPeer({ peer: inputPeer }), + settings: new Api.InputPeerNotifySettings({ + muteUntil, + }), + }), + ); + }); + + return { content: [{ type: 'text', text: `Chat ${chatId} muted.` }] }; + } catch (error) { + return logAndFormatError( + 'mute_chat', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CHAT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/promoteAdmin.ts b/src/lib/mcp/telegram/tools/promoteAdmin.ts index 3036e2efa..4a8663e3a 100644 --- a/src/lib/mcp/telegram/tools/promoteAdmin.ts +++ b/src/lib/mcp/telegram/tools/promoteAdmin.ts @@ -1,23 +1,68 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'promote_admin', - description: 'Promote user to admin', + description: 'Promote a user to admin in a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - user_id: { type: 'string', description: 'User ID' }, + user_id: { type: 'string', description: 'User ID to promote' }, }, required: ['chat_id', 'user_id'], }, }; export async function promoteAdmin( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('promote_admin'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const userId = validateId(args.user_id, 'user_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + if (chat.type !== 'channel' && chat.type !== 'supergroup') { + return { content: [{ type: 'text', text: 'Admin promotion is only available for channels/supergroups.' }], isError: true }; + } + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + const inputUser = await client.getInputEntity(userId); + await client.invoke( + new Api.channels.EditAdmin({ + channel: inputChannel as Api.TypeInputChannel, + userId: inputUser as Api.TypeInputUser, + adminRights: new Api.ChatAdminRights({ + changeInfo: true, + deleteMessages: true, + banUsers: true, + inviteUsers: true, + pinMessages: true, + manageCall: true, + }), + rank: 'Admin', + }), + ); + }); + + return { content: [{ type: 'text', text: `User ${userId} promoted to admin in ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'promote_admin', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/searchContacts.ts b/src/lib/mcp/telegram/tools/searchContacts.ts index d0340d47c..b05f9d924 100644 --- a/src/lib/mcp/telegram/tools/searchContacts.ts +++ b/src/lib/mcp/telegram/tools/searchContacts.ts @@ -1,20 +1,55 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; +import { optNumber } from '../args'; export const tool: MCPTool = { name: 'search_contacts', - description: 'Search contacts by name or phone', + description: 'Search contacts by name or username', inputSchema: { type: 'object', - properties: { query: { type: 'string', description: 'Search query' } }, + properties: { + query: { type: 'string', description: 'Search query' }, + limit: { type: 'number', description: 'Max results', default: 20 }, + }, required: ['query'], }, }; export async function searchContacts( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('search_contacts'); + try { + const query = typeof args.query === 'string' ? args.query : ''; + if (!query) { + return { content: [{ type: 'text', text: 'query is required' }], isError: true }; + } + const limit = optNumber(args, 'limit', 20); + const client = mtprotoService.getClient(); + + const result = await mtprotoService.withFloodWaitHandling(async () => { + return client.invoke(new Api.contacts.Search({ q: query, limit })); + }); + + if (!result || !('users' in result) || !Array.isArray(result.users) || result.users.length === 0) { + return { content: [{ type: 'text', text: `No contacts found for "${query}".` }] }; + } + + const lines = result.users.map((u: any) => { + const name = [u.firstName, u.lastName].filter(Boolean).join(' ') || 'Unknown'; + const username = u.username ? `@${u.username}` : ''; + return `ID: ${u.id} | ${name} ${username}`.trim(); + }); + + return { content: [{ type: 'text', text: lines.join('\n') }] }; + } catch (error) { + return logAndFormatError( + 'search_contacts', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/subscribePublicChannel.ts b/src/lib/mcp/telegram/tools/subscribePublicChannel.ts index 84d067bed..863fabb6e 100644 --- a/src/lib/mcp/telegram/tools/subscribePublicChannel.ts +++ b/src/lib/mcp/telegram/tools/subscribePublicChannel.ts @@ -1,20 +1,46 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'subscribe_public_channel', - description: 'Subscribe to public channel by username', + description: 'Subscribe to a public channel by username', inputSchema: { type: 'object', - properties: { username: { type: 'string', description: 'Channel username' } }, + properties: { + username: { type: 'string', description: 'Channel username' }, + }, required: ['username'], }, }; export async function subscribePublicChannel( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('subscribe_public_channel'); + try { + const username = typeof args.username === 'string' ? args.username : ''; + if (!username) return { content: [{ type: 'text', text: 'username is required' }], isError: true }; + + const client = mtprotoService.getClient(); + + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(username); + await client.invoke( + new Api.channels.JoinChannel({ + channel: inputChannel as Api.TypeInputChannel, + }), + ); + }); + + return { content: [{ type: 'text', text: `Subscribed to channel: ${username}` }] }; + } catch (error) { + return logAndFormatError( + 'subscribe_public_channel', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.GROUP, + ); + } } diff --git a/src/lib/mcp/telegram/tools/unarchiveChat.ts b/src/lib/mcp/telegram/tools/unarchiveChat.ts index 00f641d77..5bf6cb5ce 100644 --- a/src/lib/mcp/telegram/tools/unarchiveChat.ts +++ b/src/lib/mcp/telegram/tools/unarchiveChat.ts @@ -1,20 +1,56 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'unarchive_chat', description: 'Unarchive a chat', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function unarchiveChat( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('unarchive_chat'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + await client.invoke( + new Api.folders.EditPeerFolders({ + folderPeers: [ + new Api.InputFolderPeer({ + peer: inputPeer, + folderId: 0, // 0 = Main folder (unarchive) + }), + ], + }), + ); + }); + + return { content: [{ type: 'text', text: `Chat ${chatId} unarchived.` }] }; + } catch (error) { + return logAndFormatError( + 'unarchive_chat', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CHAT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/unbanUser.ts b/src/lib/mcp/telegram/tools/unbanUser.ts index 4f20ed58e..a4844cc45 100644 --- a/src/lib/mcp/telegram/tools/unbanUser.ts +++ b/src/lib/mcp/telegram/tools/unbanUser.ts @@ -1,23 +1,60 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'unban_user', - description: 'Unban user from chat', + description: 'Unban a user from a group or channel', inputSchema: { type: 'object', properties: { chat_id: { type: 'string', description: 'Chat ID or username' }, - user_id: { type: 'string', description: 'User ID' }, + user_id: { type: 'string', description: 'User ID to unban' }, }, required: ['chat_id', 'user_id'], }, }; export async function unbanUser( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('unban_user'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + const userId = validateId(args.user_id, 'user_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + if (chat.type !== 'channel' && chat.type !== 'supergroup') { + return { content: [{ type: 'text', text: 'Unban is only available for channels/supergroups.' }], isError: true }; + } + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputChannel = await client.getInputEntity(entity); + const inputUser = await client.getInputEntity(userId); + await client.invoke( + new Api.channels.EditBanned({ + channel: inputChannel as Api.TypeInputChannel, + participant: inputUser as Api.TypeInputPeer, + bannedRights: new Api.ChatBannedRights({ untilDate: 0 }), + }), + ); + }); + + return { content: [{ type: 'text', text: `User ${userId} unbanned from ${chat.title ?? chatId}.` }] }; + } catch (error) { + return logAndFormatError( + 'unban_user', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.ADMIN, + ); + } } diff --git a/src/lib/mcp/telegram/tools/unblockUser.ts b/src/lib/mcp/telegram/tools/unblockUser.ts index 82642c273..0f73c4e1a 100644 --- a/src/lib/mcp/telegram/tools/unblockUser.ts +++ b/src/lib/mcp/telegram/tools/unblockUser.ts @@ -1,20 +1,43 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'unblock_user', description: 'Unblock a user', inputSchema: { type: 'object', - properties: { user_id: { type: 'string', description: 'User ID' } }, + properties: { + user_id: { type: 'string', description: 'User ID to unblock' }, + }, required: ['user_id'], }, }; export async function unblockUser( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('unblock_user'); + try { + const userId = validateId(args.user_id, 'user_id'); + const client = mtprotoService.getClient(); + + await mtprotoService.withFloodWaitHandling(async () => { + const inputUser = await client.getInputEntity(userId); + await client.invoke( + new Api.contacts.Unblock({ id: inputUser as Api.TypeInputPeer }), + ); + }); + + return { content: [{ type: 'text', text: `User ${userId} unblocked.` }] }; + } catch (error) { + return logAndFormatError( + 'unblock_user', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CONTACT, + ); + } } diff --git a/src/lib/mcp/telegram/tools/unmuteChat.ts b/src/lib/mcp/telegram/tools/unmuteChat.ts index c8048145d..8a521fe8d 100644 --- a/src/lib/mcp/telegram/tools/unmuteChat.ts +++ b/src/lib/mcp/telegram/tools/unmuteChat.ts @@ -1,20 +1,54 @@ import type { MCPTool, MCPToolResult } from '../../types'; import type { TelegramMCPContext } from '../types'; -import { notImplemented } from './notImplemented'; +import { ErrorCategory, logAndFormatError } from '../../errorHandler'; +import { validateId } from '../../validation'; +import { getChatById } from '../telegramApi'; +import { mtprotoService } from '../../../../services/mtprotoService'; +import { Api } from 'telegram'; export const tool: MCPTool = { name: 'unmute_chat', - description: 'Unmute a chat', + description: 'Unmute notifications for a chat', inputSchema: { type: 'object', - properties: { chat_id: { type: 'string', description: 'Chat ID or username' } }, + properties: { + chat_id: { type: 'string', description: 'Chat ID or username' }, + }, required: ['chat_id'], }, }; export async function unmuteChat( - _args: Record, + args: Record, _context: TelegramMCPContext, ): Promise { - return notImplemented('unmute_chat'); + try { + const chatId = validateId(args.chat_id, 'chat_id'); + + const chat = getChatById(chatId); + if (!chat) return { content: [{ type: 'text', text: `Chat not found: ${chatId}` }], isError: true }; + + const client = mtprotoService.getClient(); + const entity = chat.username ? chat.username : chat.id; + + await mtprotoService.withFloodWaitHandling(async () => { + const inputPeer = await client.getInputEntity(entity); + await client.invoke( + new Api.account.UpdateNotifySettings({ + peer: new Api.InputNotifyPeer({ peer: inputPeer }), + settings: new Api.InputPeerNotifySettings({ + muteUntil: 0, + }), + }), + ); + }); + + return { content: [{ type: 'text', text: `Chat ${chatId} unmuted.` }] }; + } catch (error) { + return logAndFormatError( + 'unmute_chat', + error instanceof Error ? error : new Error(String(error)), + ErrorCategory.CHAT, + ); + } }