UNPKG

telegram-mcp-local-server

Version:

Secure Model Context Protocol (MCP) server for Telegram integration. Runs locally, allows AI agents to read chats and message history, with built-in readonly mode for safety.

249 lines 10.1 kB
import { TelegramClient as TgClient } from "telegram"; import { StringSession } from "telegram/sessions/index.js"; import { Api } from "telegram"; export class TelegramClient { constructor(config) { this.config = config; const session = new StringSession(config.sessionString || ""); this.client = new TgClient(session, config.apiId, config.apiHash, { connectionRetries: 5, }); } async connect() { //console.error("Connecting to Telegram..."); await this.client.connect(); //console.error("Connected to Telegram successfully"); } async getChats(limit = 50) { //console.error(`Fetching ${limit} chats...`); const dialogs = await this.client.getDialogs({ limit }); const chats = []; for (const dialog of dialogs) { const entity = dialog.entity; if (!entity) continue; let chatInfo; if (entity.className === "User") { chatInfo = { id: entity.id.toString(), title: `${entity.firstName || ""} ${entity.lastName || ""}`.trim(), type: "user", username: entity.username, }; } else if (entity.className === "Chat") { chatInfo = { id: entity.id.toString(), title: entity.title, type: "group", participantsCount: entity.participantsCount, }; } else if (entity.className === "Channel") { chatInfo = { id: entity.id.toString(), title: entity.title, type: entity.broadcast ? "channel" : "supergroup", username: entity.username, participantsCount: entity.participantsCount, }; } else { continue; // Skip unknown entity types } chats.push(chatInfo); } //console.error(`Fetched ${chats.length} chats`); return chats; } async getChatHistory(chatId, limit = 50, offsetId) { //console.error(`Fetching ${limit} messages from chat ${chatId}...`); try { const entity = await this.client.getEntity(chatId); const messages = await this.client.getMessages(entity, { limit, offsetId, }); const messageInfos = []; for (const message of messages) { const msg = message; if (!msg || msg.className !== "Message") { continue; } let fromUsername; let fromFirstName; let fromLastName; if (msg.fromId) { try { const sender = await this.client.getEntity(msg.fromId); if (sender?.className === "User") { fromUsername = sender.username; fromFirstName = sender.firstName; fromLastName = sender.lastName; } } catch (error) { console.error("Error getting sender info:", error); } } const messageInfo = { id: msg.id, text: msg.message || "", date: new Date(msg.date * 1000), fromId: msg.fromId?.toString(), fromUsername, fromFirstName, fromLastName, replyToMsgId: msg.replyTo?.replyToMsgId, }; messageInfos.push(messageInfo); } // console.error(`Fetched ${messageInfos.length} messages`); return messageInfos; } catch (error) { // console.error(`Error fetching chat history for ${chatId}:`, error); throw error; } } async sendMessage(chatId, message) { // console.error(`Sending message to chat ${chatId}...`); try { const entity = await this.client.getEntity(chatId); const result = await this.client.sendMessage(entity, { message }); if (!result || result.className !== "Message") { throw new Error("Failed to send message"); } const messageInfo = { id: result.id, text: result.message || "", date: new Date(result.date * 1000), fromId: result.fromId?.toString(), }; // console.error("Message sent successfully"); return messageInfo; } catch (error) { // console.error(`Error sending message to ${chatId}:`, error); throw error; } } async getFolders() { // console.error("Fetching dialog folders..."); try { const filters = await this.client.invoke(new Api.messages.GetDialogFilters()); const folders = []; for (const f of filters) { if (f && (f instanceof Api.DialogFilter)) { const titleVal = typeof f.title === 'string' ? f.title : (f.title?.text ?? ''); folders.push({ id: Number(f.id), title: String(titleVal), emoticon: f.emoticon }); } // Skip DialogFilterDefault and other types } // console.error(`Fetched ${folders.length} folders`); return folders; } catch (error) { // console.error("Error fetching dialog folders:", error); throw error; } } async getChannelsFromFolder(folderId, limit = 50) { // console.error(`Fetching up to ${limit} channels from folder ${folderId}...`); try { const filters = await this.client.invoke(new Api.messages.GetDialogFilters()); let targetFilter; for (const f of filters) { if (f && (f instanceof Api.DialogFilter) && Number(f.id) === Number(folderId)) { targetFilter = f; break; } } if (!targetFilter) { throw new Error(`Folder with id ${folderId} not found`); } const toPeerKey = (p) => { if (!p) return null; // Handle InputPeer and Peer variants by checking known id fields if (typeof p.channelId !== 'undefined') return `channel:${p.channelId.toString()}`; if (typeof p.chatId !== 'undefined') return `chat:${p.chatId.toString()}`; if (typeof p.userId !== 'undefined') return `user:${p.userId.toString()}`; return null; }; const included = new Set(); const excluded = new Set(); const includePeers = (targetFilter.includePeers || []); const excludePeers = (targetFilter.excludePeers || []); const pinnedPeers = (targetFilter.pinnedPeers || []); for (const p of [...includePeers, ...pinnedPeers]) { const k = toPeerKey(p); if (k) included.add(k); } for (const p of excludePeers) { const k = toPeerKey(p); if (k) excluded.add(k); } // Get a reasonably large set of dialogs to filter from const dialogs = await this.client.getDialogs({ limit: Math.max(limit * 4, 200) }); const channels = []; for (const dialog of dialogs) { const entity = dialog.entity; if (!entity) continue; const entityKey = entity.className === 'Channel' ? `channel:${entity.id.toString()}` : entity.className === 'Chat' ? `chat:${entity.id.toString()}` : entity.className === 'User' ? `user:${entity.id.toString()}` : null; if (!entityKey) continue; // Determine inclusion according to filter let inFilter = true; if (included.size > 0) { inFilter = included.has(entityKey); } else { if (excluded.has(entityKey)) inFilter = false; // Note: For simplicity, we don't evaluate category flags (contacts, groups, channels, etc.) here. } if (!inFilter) continue; // Only return channels/supergroups if (entity.className === 'Channel') { channels.push({ id: entity.id.toString(), title: entity.title, type: entity.broadcast ? 'channel' : 'supergroup', username: entity.username, participantsCount: entity.participantsCount, }); } if (channels.length >= limit) break; } // console.error(`Fetched ${channels.length} channels from folder ${folderId}`); return channels; } catch (error) { // console.error(`Error fetching channels from folder ${folderId}:`, error); throw error; } } async disconnect() { // console.error("Disconnecting from Telegram..."); await this.client.disconnect(); // console.error("Disconnected from Telegram"); } } //# sourceMappingURL=telegram-client.js.map