telegram-mcp-server
Version:
MCP server for a Telegram client built with the MTProto library and FastMCP framework
422 lines (359 loc) • 12.3 kB
JavaScript
import MTProto from '@mtproto/core';
import path from 'path';
import fs from 'fs';
import readline from 'readline';
import { fileURLToPath } from 'url';
// Get current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class TelegramClient {
constructor(apiId, apiHash, phoneNumber, sessionPath = './data/session.json') {
this.apiId = apiId;
this.apiHash = apiHash;
this.phoneNumber = phoneNumber;
this.sessionPath = path.resolve(sessionPath);
// Create data directory if it doesn't exist
const dataDir = path.dirname(this.sessionPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Initialize dialog cache for storing access hashes
this.dialogCache = new Map();
// Initialize the MTProto client
this.mtproto = new MTProto({
api_id: this.apiId,
api_hash: this.apiHash,
storageOptions: {
path: this.sessionPath,
},
});
}
// Helper function to prompt for user input
async _askQuestion(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => rl.question(query, answer => {
rl.close();
resolve(answer);
}));
}
// Check if session exists
hasSession() {
return fs.existsSync(this.sessionPath);
}
// Log in to Telegram
async login() {
try {
// Check if session file exists
if (this.hasSession()) {
console.log('Session file found, attempting to validate session.');
try {
// Attempt a simple API call to validate the session
await this.mtproto.call('users.getUsers', {
id: [{ _: 'inputUserSelf' }],
});
console.log('Existing session is valid.');
return true; // Session is valid
} catch (error) {
console.warn(`Session validation failed: ${error.message || JSON.stringify(error)}. Assuming session is invalid.`);
// Delete the invalid session file
try {
fs.unlinkSync(this.sessionPath);
console.log('Deleted invalid session file.');
} catch (unlinkError) {
console.error('Error deleting invalid session file:', unlinkError);
}
// Proceed to log in again
}
}
console.log('No valid session found. Starting login process...');
// Request to send a code to the user's phone
const { phone_code_hash } = await this.mtproto.call('auth.sendCode', {
phone_number: this.phoneNumber,
settings: {
_: 'codeSettings',
},
});
// Ask the user to enter the code they received
const code = await this._askQuestion('Enter the code you received: ');
let signInResult;
try {
// Attempt to sign in using the code
signInResult = await this.mtproto.call('auth.signIn', {
phone_number: this.phoneNumber,
phone_code_hash,
phone_code: code,
});
} catch (error) {
if (error.error_message === 'SESSION_PASSWORD_NEEDED') {
// If 2FA is enabled, ask for the password
const password = await this._askQuestion('Enter your 2FA password: ');
// Get the password hash
const { srp_id, current_algo, srp_B } = await this.mtproto.call('account.getPassword');
const { g, p, salt1, salt2 } = current_algo;
// Compute the password hash
const { A, M1 } = await this.mtproto.crypto.getSRPParams({
g,
p,
salt1,
salt2,
gB: srp_B,
password,
});
// Complete the sign-in process with the password
signInResult = await this.mtproto.call('auth.checkPassword', {
password: {
_: 'inputCheckPasswordSRP',
srp_id,
A,
M1,
},
});
} else {
throw error;
}
}
console.log('Logged in successfully!');
return true;
} catch (error) {
console.error('Error during login:', error);
return false;
}
}
// Get all dialogs (chats)
async getDialogs(limit = 100, offset = 0) {
try {
console.log(`Fetching up to ${limit} dialogs with offset ${offset}`);
const result = await this.mtproto.call('messages.getDialogs', {
offset,
limit,
offset_peer: { _: 'inputPeerSelf' },
});
console.log(`Retrieved ${result.chats.length} chats`);
// Update dialog cache with the received chats
this._updateDialogCache(result.chats);
return {
chats: result.chats,
users: result.users,
messages: result.messages,
dialogs: result.dialogs
};
} catch (error) {
console.error('Error fetching dialogs:', error);
throw error;
}
}
// Get all dialogs by making multiple requests
async getAllDialogs(batchSize = 100) {
try {
console.log('Fetching all dialogs...');
let allChats = [];
let allUsers = [];
let allMessages = [];
let allDialogs = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const result = await this.mtproto.call('messages.getDialogs', {
offset,
limit: batchSize,
offset_peer: { _: 'inputPeerSelf' },
});
allChats = [...allChats, ...result.chats];
allUsers = [...allUsers, ...result.users];
allMessages = [...allMessages, ...result.messages];
allDialogs = [...allDialogs, ...result.dialogs];
// Update the dialog cache with the received chats
this._updateDialogCache(result.chats);
// Check if we received fewer dialogs than requested, which means we've reached the end
if (result.dialogs.length < batchSize) {
hasMore = false;
} else {
offset += batchSize;
}
console.log(`Retrieved ${result.chats.length} chats at offset ${offset}`);
}
console.log(`Total chats retrieved: ${allChats.length}`);
return {
chats: allChats,
users: allUsers,
messages: allMessages,
dialogs: allDialogs
};
} catch (error) {
console.error('Error fetching all dialogs:', error);
throw error;
}
}
// Update the dialog cache with chat information
_updateDialogCache(chats) {
if (!chats || !Array.isArray(chats)) return;
for (const chat of chats) {
if (!chat || !chat.id) continue;
// Store only channels and users that have access_hash
if ((chat._ === 'channel' || chat._ === 'user') && chat.access_hash) {
this.dialogCache.set(chat.id, {
type: chat._,
id: chat.id,
access_hash: chat.access_hash,
title: chat.title || chat.username || chat.first_name
});
} else if (chat._ === 'chat') {
this.dialogCache.set(chat.id, {
type: chat._,
id: chat.id,
title: chat.title
});
}
}
}
// Get peer input for a chat by ID
getPeerInputById(id) {
const cachedChat = this.dialogCache.get(`${id}`);
if (!cachedChat) {
throw new Error(`Chat with ID ${id} not found in cache. Run getAllDialogs first.`);
}
if (cachedChat.type === 'channel') {
return {
_: 'inputPeerChannel',
channel_id: cachedChat.id,
access_hash: cachedChat.access_hash,
};
} else if (cachedChat.type === 'chat') {
return {
_: 'inputPeerChat',
chat_id: cachedChat.id,
};
} else if (cachedChat.type === 'user') {
return {
_: 'inputPeerUser',
user_id: cachedChat.id,
access_hash: cachedChat.access_hash,
};
}
throw new Error(`Unsupported chat type: ${cachedChat.type}`);
}
// Get messages from a specific chat
async getChatMessages(chat, limit = 100) {
try {
if (!chat || !chat.id) {
throw new Error('Invalid chat object provided');
}
// Determine the correct peer type based on chat properties
let peer;
if (chat._ === 'channel' || chat._ === 'channelForbidden') {
peer = {
_: 'inputPeerChannel',
channel_id: chat.id,
access_hash: chat.access_hash,
};
} else if (chat._ === 'chat' || chat._ === 'chatForbidden') {
peer = {
_: 'inputPeerChat',
chat_id: chat.id,
};
} else if (chat._ === 'user') {
peer = {
_: 'inputPeerUser',
user_id: chat.id,
access_hash: chat.access_hash,
};
} else {
throw new Error(`Unsupported chat type: ${chat._}`);
}
console.log(`Fetching up to ${limit} messages from chat: ${chat.title || chat.id}`);
const messagesResult = await this.mtproto.call('messages.getHistory', {
peer,
offset_id: 0,
offset_date: 0,
add_offset: 0,
limit,
max_id: 0,
min_id: 0,
hash: 0,
});
return messagesResult.messages;
} catch (error) {
console.error('Error fetching chat messages:', error);
throw error;
}
}
// Filter messages by a regex pattern
filterMessagesByPattern(messages, pattern) {
if (!messages || !Array.isArray(messages)) {
return [];
}
const regex = new RegExp(pattern);
return messages
.map(msg => msg.message || '')
.filter(msg => regex.test(msg));
}
// Get messages from a specific chat using ID
async getMessagesByChannelId(channelId, limit = 100) {
try {
// Convert string IDs to numbers if necessary
const id = typeof channelId === 'string' ? parseInt(channelId, 10) : channelId;
if (isNaN(id)) {
throw new Error('Invalid channel ID provided');
}
// Get peer input from cache
const peer = this.getPeerInputById(id);
const cachedChat = this.dialogCache.get(`${id}`);
console.log(`Fetching up to ${limit} messages from channel ID ${id} (${cachedChat.title || 'Unknown'})`);
const messagesResult = await this.mtproto.call('messages.getHistory', {
peer,
offset_id: 0,
offset_date: 0,
add_offset: 0,
limit,
max_id: 0,
min_id: 0,
hash: 0,
});
return messagesResult.messages;
} catch (error) {
console.error('Error fetching channel messages:', error);
throw error;
}
}
// Save dialog cache to file
async saveDialogCache(cachePath = './data/dialog_cache.json') {
try {
const resolvedPath = path.resolve(cachePath);
const cacheDir = path.dirname(resolvedPath);
// Create directory if it doesn't exist
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// Convert Map to object for serialization
const cacheObject = Object.fromEntries(this.dialogCache);
fs.writeFileSync(resolvedPath, JSON.stringify(cacheObject, null, 2));
console.log(`Dialog cache saved to ${resolvedPath}`);
return true;
} catch (error) {
console.error('Error saving dialog cache:', error);
return false;
}
}
// Load dialog cache from file
async loadDialogCache(cachePath = './data/dialog_cache.json') {
try {
const resolvedPath = path.resolve(cachePath);
if (!fs.existsSync(resolvedPath)) {
console.log(`Dialog cache file not found at ${resolvedPath}`);
return false;
}
const cacheData = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
// Convert object back to Map
this.dialogCache = new Map(Object.entries(cacheData));
console.log(`Dialog cache loaded from ${resolvedPath} with ${this.dialogCache.size} entries`);
return true;
} catch (error) {
console.error('Error loading dialog cache:', error);
return false;
}
}
}
export default TelegramClient;