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, + ); + } }