├── .env.example ├── commands ├── secret.js ├── index.js ├── control.js ├── cancel_shutdown.js ├── request_access.js ├── getip.js ├── upload.js ├── killprocess.js ├── shutdown.js ├── screenshot.js └── stats.js ├── .gitignore ├── package.json ├── middleware ├── firstUserAdmin.js └── access.js ├── callback_handlers └── approveAccess.js ├── utils ├── screenshot.js └── accessControl.js ├── bot.js └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | TELEGRAM_TOKEN=1234567890:ABC-YourTelegramBotTokenHere 2 | -------------------------------------------------------------------------------- /commands/secret.js: -------------------------------------------------------------------------------- 1 | module.exports = (bot) => { 2 | bot.command('secret', (ctx) => { 3 | ctx.reply('You have access to the secret command!'); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Environment variables 5 | .env 6 | 7 | # Logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # OS generated files 14 | .DS_Store 15 | Thumbs.db 16 | 17 | # IDE/editor directories 18 | .idea/ 19 | .vscode/ 20 | .history/ 21 | 22 | # User data files 23 | users.txt 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pc-remote-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "bot.js", 6 | "scripts": { 7 | "start": "node bot.js" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "dotenv": "^16.4.7", 12 | "robotjs": "^0.6.0", 13 | "screenshot-desktop": "^1.15.0", 14 | "telegraf": "^4.16.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /middleware/firstUserAdmin.js: -------------------------------------------------------------------------------- 1 | const { getAdmin, setAdmin } = require('../utils/accessControl'); 2 | 3 | module.exports = (ctx, next) => { 4 | const currentAdmin = getAdmin(); 5 | if (!currentAdmin && ctx.from && ctx.from.id) { 6 | const userId = ctx.from.id.toString(); 7 | setAdmin(userId); 8 | ctx.reply('You are now the admin!'); 9 | } 10 | return next(); 11 | }; 12 | -------------------------------------------------------------------------------- /commands/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = (bot) => { 5 | const commandsPath = __dirname; 6 | const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js') && file !== 'index.js'); 7 | 8 | for (const file of commandFiles) { 9 | const commandPath = path.join(commandsPath, file); 10 | const command = require(commandPath); 11 | command(bot); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /middleware/access.js: -------------------------------------------------------------------------------- 1 | const { isAdmin, isAllowedUser } = require('../utils/accessControl'); 2 | 3 | module.exports = (ctx, next) => { 4 | const fromId = ctx.from?.id; 5 | const messageText = ctx.message?.text || ''; 6 | const command = messageText.split(' ')[0]; 7 | 8 | // Always allow admin 9 | if (fromId && isAdmin(fromId.toString())) { 10 | return next(); 11 | } 12 | 13 | // Allow `/request_access` to non-allowed users 14 | if (command === '/request_access') { 15 | return next(); 16 | } 17 | 18 | // If user is not allowed and not admin 19 | if (fromId && !isAllowedUser(fromId.toString())) { 20 | return ctx.reply('Access denied. Please use /request_access to request permission.'); 21 | } 22 | 23 | return next(); 24 | }; 25 | -------------------------------------------------------------------------------- /commands/control.js: -------------------------------------------------------------------------------- 1 | const { Markup } = require('telegraf'); 2 | 3 | module.exports = (bot) => { 4 | bot.command('control', (ctx) => { 5 | ctx.reply( 6 | 'Use the buttons below to control your media playback or capture a screenshot.', 7 | Markup.inlineKeyboard([ 8 | [ 9 | Markup.button.callback('⏯️ Play/Pause', 'play_pause'), 10 | Markup.button.callback('⏭️ Next', 'next'), 11 | Markup.button.callback('⏮️ Previous', 'previous'), 12 | ], 13 | [ 14 | Markup.button.callback('🔊 Volume Up', 'volume_up'), 15 | Markup.button.callback('🔉 Volume Down', 'volume_down'), 16 | Markup.button.callback('🔇 Mute', 'mute'), 17 | ], 18 | [ 19 | Markup.button.callback('🖼️ Screenshot', 'select_screen'), 20 | ], 21 | ]) 22 | ); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /commands/cancel_shutdown.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | module.exports = (bot) => { 4 | bot.command('cancel_shutdown', async (ctx) => { 5 | const platform = process.platform; // 'win32', 'darwin', 'linux' 6 | 7 | let command; 8 | if (platform === 'win32') { 9 | command = 'shutdown /a'; 10 | } else if (platform === 'darwin' || platform === 'linux') { 11 | command = 'sudo shutdown -c'; 12 | } else { 13 | await ctx.reply('Unsupported platform for canceling shutdown.'); 14 | return; 15 | } 16 | 17 | exec(command, (error) => { 18 | if (error) { 19 | console.error('Error canceling shutdown:', error); 20 | ctx.reply(`Failed to cancel shutdown: ${error.message}`); 21 | return; 22 | } 23 | ctx.reply('Successfully canceled the scheduled shutdown.'); 24 | }); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /callback_handlers/approveAccess.js: -------------------------------------------------------------------------------- 1 | const { isAdmin, addAllowedUser } = require('../utils/accessControl'); 2 | 3 | module.exports = async (ctx, bot) => { 4 | const callbackData = ctx.callbackQuery.data; 5 | const fromId = ctx.from.id.toString(); 6 | 7 | // Only admin can approve 8 | if (!isAdmin(fromId)) { 9 | await ctx.answerCbQuery('You are not authorized to approve.', { show_alert: true }); 10 | return; 11 | } 12 | 13 | if (callbackData.startsWith('approve_access_')) { 14 | const userIdToApprove = callbackData.replace('approve_access_', ''); 15 | addAllowedUser(userIdToApprove); 16 | 17 | await ctx.answerCbQuery('User approved!', { show_alert: false }); 18 | await ctx.editMessageText(`User ID ${userIdToApprove} has been granted access!`); 19 | await bot.telegram.sendMessage(userIdToApprove, 'Your access request has been approved!'); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /utils/screenshot.js: -------------------------------------------------------------------------------- 1 | const screenshot = require('screenshot-desktop'); 2 | const fs = require('fs'); 3 | 4 | async function getScreens() { 5 | try { 6 | return await screenshot.listDisplays(); 7 | } catch (error) { 8 | console.error('Error fetching screens:', error); 9 | return []; 10 | } 11 | } 12 | 13 | async function takeScreenshot(screenId) { 14 | const tempFilePath = `screenshot_${Date.now()}.png`; 15 | try { 16 | await screenshot({ screen: screenId, filename: tempFilePath }); 17 | return tempFilePath; 18 | } catch (error) { 19 | console.error('Error taking screenshot:', error); 20 | throw error; 21 | } 22 | } 23 | 24 | function cleanupFile(filePath) { 25 | try { 26 | fs.unlinkSync(filePath); 27 | } catch (error) { 28 | console.error('Error removing file:', filePath, error); 29 | } 30 | } 31 | 32 | module.exports = { getScreens, takeScreenshot, cleanupFile }; 33 | -------------------------------------------------------------------------------- /commands/request_access.js: -------------------------------------------------------------------------------- 1 | const { Markup } = require('telegraf'); 2 | const { isAdmin, isAllowedUser, getAdmin } = require('../utils/accessControl'); 3 | 4 | module.exports = (bot) => { 5 | bot.command('request_access', async (ctx) => { 6 | const fromId = ctx.from.id.toString(); 7 | if (isAllowedUser(fromId) || isAdmin(fromId)) { 8 | return ctx.reply('You already have access.'); 9 | } 10 | 11 | const adminId = getAdmin(); 12 | if (!adminId) { 13 | return ctx.reply('No admin is set yet. Please try again later.'); 14 | } 15 | 16 | const requestText = `User ID ${fromId} is requesting access. Approve?`; 17 | await ctx.telegram.sendMessage( 18 | adminId, 19 | requestText, 20 | Markup.inlineKeyboard([ 21 | [Markup.button.callback('Approve', `approve_access_${fromId}`)] 22 | ]) 23 | ); 24 | 25 | ctx.reply('Your request has been sent to the admin. Please wait for approval.'); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /commands/getip.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | function fetchPublicIP() { 4 | return new Promise((resolve, reject) => { 5 | https.get('https://api.ipify.org?format=json', (res) => { 6 | let data = ''; 7 | 8 | res.on('data', chunk => { 9 | data += chunk; 10 | }); 11 | 12 | res.on('end', () => { 13 | try { 14 | const json = JSON.parse(data); 15 | if (json.ip) { 16 | resolve(json.ip); 17 | } else { 18 | reject(new Error('No IP found in response')); 19 | } 20 | } catch (err) { 21 | reject(err); 22 | } 23 | }); 24 | }).on('error', (err) => { 25 | reject(err); 26 | }); 27 | }); 28 | } 29 | 30 | module.exports = (bot) => { 31 | bot.command('getip', async (ctx) => { 32 | try { 33 | const publicIP = await fetchPublicIP(); 34 | await ctx.reply(`Your server's public IP is: ${publicIP}`); 35 | } catch (error) { 36 | console.error('Error fetching public IP:', error); 37 | await ctx.reply('Failed to retrieve public IP.'); 38 | } 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /commands/upload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = (bot) => { 5 | bot.command('upload', async (ctx) => { 6 | const messageText = ctx.message.text.trim(); 7 | const parts = messageText.split(' '); 8 | 9 | if (parts.length < 2) { 10 | return ctx.reply('Please specify a file path. Usage: /upload '); 11 | } 12 | 13 | const filePath = parts.slice(1).join(' '); 14 | const fullPath = path.resolve(filePath); // Resolve to absolute path if needed 15 | 16 | try { 17 | const stat = fs.statSync(fullPath); 18 | 19 | // Telegram maximum file size currently: 2GB = 2 * 1024 * 1024 * 1024 bytes 20 | const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; 21 | 22 | if (stat.size > MAX_FILE_SIZE) { 23 | return ctx.reply('File is too large to send via Telegram (limit is 2GB).'); 24 | } 25 | 26 | // Send the file as a document 27 | await ctx.replyWithDocument({ source: fullPath }); 28 | } catch (error) { 29 | console.error('Error uploading file:', error); 30 | if (error.code === 'ENOENT') { 31 | ctx.reply('File not found. Please provide a valid file path.'); 32 | } else { 33 | ctx.reply('An error occurred while uploading the file.'); 34 | } 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /commands/killprocess.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | module.exports = (bot) => { 4 | bot.command('killprocess', async (ctx) => { 5 | const messageText = ctx.message.text; 6 | const parts = messageText.split(' '); 7 | if (parts.length < 2) { 8 | return ctx.reply('Please provide a process name. Usage: /killprocess '); 9 | } 10 | 11 | const processName = parts.slice(1).join(' '); 12 | const platform = process.platform; 13 | let command; 14 | 15 | if (platform === 'win32') { 16 | // On Windows, use taskkill 17 | // If the process name includes ".exe", keep it. Otherwise, append it if you know the exact process. 18 | // For generic usage, assume the user provides the full name: 19 | // Example: /killprocess notepad.exe 20 | command = `taskkill /IM "${processName}" /F`; 21 | } else if (platform === 'linux' || platform === 'darwin') { 22 | // On Linux/macOS, use pkill 23 | command = `pkill -f "${processName}"`; 24 | } else { 25 | return ctx.reply('Unsupported platform for killing processes.'); 26 | } 27 | 28 | exec(command, (error, stdout, stderr) => { 29 | if (error) { 30 | console.error('Error killing process:', error); 31 | return ctx.reply(`Failed to kill process: ${error.message}`); 32 | } 33 | 34 | // Check stdout/stderr for output that might indicate success or failure 35 | // If nothing is returned, just assume success. 36 | ctx.reply(`Attempted to kill process: ${processName}`); 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const {Telegraf} = require('telegraf'); 3 | const {initUsers} = require('./utils/accessControl'); 4 | const approveAccessHandler = require('./callback_handlers/approveAccess'); 5 | 6 | const BOT_TOKEN = process.env.TELEGRAM_TOKEN; 7 | if (!BOT_TOKEN) { 8 | console.error('Error: TELEGRAM_TOKEN is not defined in .env file'); 9 | process.exit(1); 10 | } 11 | 12 | const bot = new Telegraf(BOT_TOKEN); 13 | 14 | initUsers(); 15 | 16 | const firstUserAdminMiddleware = require('./middleware/firstUserAdmin'); 17 | const accessMiddleware = require('./middleware/access'); 18 | 19 | bot.use(firstUserAdminMiddleware); 20 | bot.use(accessMiddleware); 21 | 22 | require('./commands')(bot); 23 | 24 | bot.on('callback_query', async (ctx) => approveAccessHandler(ctx, bot)); 25 | bot.catch((err, ctx) => { 26 | console.error(`Bot Error: ${err}`, ctx.updateType); 27 | }); 28 | 29 | bot.launch().then(async () => { 30 | await bot.telegram.setMyCommands([ 31 | {command: 'request_access', description: 'Request access to the bot'}, 32 | {command: 'secret', description: 'Access secret command (if allowed)'}, 33 | {command: 'upload', description: 'Send file from server: /upload [path]'}, 34 | {command: 'control', description: 'Control media playback and take screenshots'}, 35 | {command: 'screenshot', description: 'Take a screenshot of a screen'}, 36 | {command: 'cancel_shutdown', description: 'Cancel a scheduled shutdown'}, 37 | {command: 'stats', description: 'Show system stats and top processes'}, 38 | {command: 'getip', description: 'Show public IP of the server'}, 39 | ]); 40 | 41 | console.log('Bot startup successfully.'); 42 | }); 43 | -------------------------------------------------------------------------------- /utils/accessControl.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const USERS_FILE = 'users.txt'; 4 | 5 | let adminId = null; 6 | let allowedUsers = new Set(); 7 | 8 | function initUsers() { 9 | if (!fs.existsSync(USERS_FILE)) { 10 | fs.writeFileSync(USERS_FILE, '', 'utf8'); 11 | } 12 | const data = fs.readFileSync(USERS_FILE, 'utf8'); 13 | const lines = data.split('\n').map(l => l.trim()); 14 | if (lines.length > 0 && lines[0] !== '') { 15 | adminId = lines[0]; 16 | } 17 | 18 | const userLines = lines.slice(1).filter(l => l !== ''); 19 | allowedUsers = new Set(userLines); 20 | } 21 | 22 | function saveUsers() { 23 | const adminLine = adminId ? adminId : ''; 24 | const userLines = Array.from(allowedUsers); 25 | const fileContent = [adminLine, ...userLines].join('\n'); 26 | fs.writeFileSync(USERS_FILE, fileContent, 'utf8'); 27 | } 28 | 29 | function getAdmin() { 30 | return adminId; 31 | } 32 | 33 | function setAdmin(userId) { 34 | adminId = userId; 35 | saveUsers(); 36 | console.log(`Set admin to user: ${userId}`); 37 | } 38 | 39 | function isAdmin(userId) { 40 | return adminId && adminId === userId; 41 | } 42 | 43 | function isAllowedUser(userId) { 44 | // The admin should inherently have access, but to keep logic consistent: 45 | if (isAdmin(userId)) return true; 46 | return allowedUsers.has(userId); 47 | } 48 | 49 | function addAllowedUser(userId) { 50 | // Do not add admin as a duplicate line if they're already admin 51 | if (isAdmin(userId) || allowedUsers.has(userId)) return; 52 | allowedUsers.add(userId); 53 | saveUsers(); 54 | } 55 | 56 | module.exports = { 57 | initUsers, 58 | getAdmin, 59 | setAdmin, 60 | isAdmin, 61 | isAllowedUser, 62 | addAllowedUser 63 | }; 64 | -------------------------------------------------------------------------------- /commands/shutdown.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | async function scheduleShutdown(minutes, force = true) { 4 | const platform = process.platform; 5 | const time = parseInt(minutes, 10); 6 | 7 | if (isNaN(time) || time < 0) { 8 | throw new Error('Invalid time specified.'); 9 | } 10 | 11 | if (platform === 'win32') { 12 | const forceFlag = force ? ' /f' : ''; 13 | return new Promise((resolve, reject) => { 14 | exec(`shutdown /s /t ${time * 60}${forceFlag}`, (error) => { 15 | if (error) return reject(error); 16 | resolve(`System will shut down in ${time} minute(s).${force ? ' (Forced)' : ''}`); 17 | }); 18 | }); 19 | } else if (platform === 'darwin' || platform === 'linux') { 20 | return new Promise((resolve, reject) => { 21 | exec(`sudo shutdown -h +${time}`, (error) => { 22 | if (error) return reject(error); 23 | resolve(`System will shut down in ${time} minute(s).`); 24 | }); 25 | }); 26 | } else { 27 | throw new Error('Unsupported platform for shutdown command.'); 28 | } 29 | } 30 | 31 | module.exports = (bot) => { 32 | bot.command('shutdown', async (ctx) => { 33 | const messageText = ctx.message.text.trim(); 34 | const parts = messageText.split(' '); 35 | 36 | if (parts.length < 2) { 37 | return ctx.reply('Please specify the time in minutes. Usage: /shutdown '); 38 | } 39 | 40 | const minutes = parts[1]; 41 | 42 | try { 43 | const resultMessage = await scheduleShutdown(minutes, true); 44 | await ctx.answerCbQuery(resultMessage, { show_alert: true }); 45 | } catch (error) { 46 | console.error('Error scheduling shutdown:', error); 47 | await ctx.answerCbQuery(`Failed to schedule shutdown: ${error.message}`, { show_alert: true }); 48 | } 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /commands/screenshot.js: -------------------------------------------------------------------------------- 1 | const { Markup } = require('telegraf'); 2 | const { getScreens, takeScreenshot, cleanupFile } = require('../utils/screenshot'); 3 | 4 | module.exports = (bot) => { 5 | // /screenshot command 6 | bot.command('screenshot', async (ctx) => { 7 | const screens = await getScreens(); 8 | 9 | if (screens.length === 1) { 10 | // Directly take a screenshot if there's only one screen 11 | try { 12 | const filePath = await takeScreenshot(screens[0].id); 13 | await ctx.replyWithPhoto({ source: filePath }); 14 | cleanupFile(filePath); 15 | } catch (error) { 16 | await ctx.reply('Failed to take a screenshot.'); 17 | } 18 | } else { 19 | // Show an inline keyboard with the list of screens, indexed from 1 20 | const buttons = screens.map((screen, index) => 21 | Markup.button.callback(`Screen ${index + 1}`, `screenshot_${screen.id}`) 22 | ); 23 | await ctx.reply('Select a screen to capture:', Markup.inlineKeyboard(buttons)); 24 | } 25 | }); 26 | 27 | // Handle callback queries related to screenshots 28 | bot.action('select_screen', async (ctx) => { 29 | const screens = await getScreens(); 30 | if (screens.length === 1) { 31 | try { 32 | const filePath = await takeScreenshot(screens[0].id); 33 | await ctx.replyWithPhoto({ source: filePath }); 34 | cleanupFile(filePath); 35 | } catch (error) { 36 | await ctx.reply('Failed to take a screenshot.'); 37 | } 38 | } else { 39 | const buttons = screens.map((screen, index) => 40 | Markup.button.callback(`Screen ${index + 1}`, `screenshot_${screen.id}`) 41 | ); 42 | await ctx.editMessageText('Select a screen to capture:', Markup.inlineKeyboard(buttons)); 43 | } 44 | 45 | await ctx.answerCbQuery(); 46 | }); 47 | 48 | bot.action(/^screenshot_(.*)$/, async (ctx) => { 49 | const screenId = ctx.match[1]; // captured group from the regex 50 | try { 51 | const filePath = await takeScreenshot(screenId); 52 | await ctx.replyWithPhoto({ source: filePath }); 53 | cleanupFile(filePath); 54 | } catch (error) { 55 | await ctx.reply('Failed to take a screenshot.'); 56 | } 57 | await ctx.answerCbQuery(); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /commands/stats.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const { exec } = require('child_process'); 3 | 4 | module.exports = (bot) => { 5 | bot.command('stats', async (ctx) => { 6 | try { 7 | // Gather CPU and memory stats 8 | const totalMem = os.totalmem(); 9 | const freeMem = os.freemem(); 10 | const usedMem = totalMem - freeMem; 11 | const memUsagePercent = ((usedMem / totalMem) * 100).toFixed(2); 12 | 13 | // CPU load average (On Windows this will be 0,0,0) 14 | const loadAvg = os.loadavg().map(v => v.toFixed(2)).join(', '); 15 | 16 | const platform = process.platform; 17 | let command; 18 | 19 | if (platform === 'win32') { 20 | command = `powershell -Command "Get-WmiObject Win32_PerfFormattedData_PerfProc_Process | Where-Object { $_.Name -notmatch '_Total|Idle|System' } | Sort-Object PercentProcessorTime -Descending | Select-Object -First 10 Name, PercentProcessorTime, WorkingSet"`; 21 | } else { 22 | // On Linux/macOS: top 10 CPU-consuming processes using ps 23 | // head -n 11: 1 line of header + 10 lines of processes 24 | command = 'ps -eo pid,comm,pcpu,pmem --sort=-pcpu | head -n 11'; 25 | } 26 | 27 | exec(command, (error, stdout, stderr) => { 28 | if (error) { 29 | console.error('Error fetching processes:', error); 30 | ctx.reply('Failed to retrieve process list.'); 31 | return; 32 | } 33 | 34 | let response = `*System Stats*\n`; 35 | response += `*CPU Load Avg:* ${loadAvg}\n`; 36 | response += `*Memory Usage:* ${memUsagePercent}% (Used ${(usedMem / (1024 * 1024)).toFixed(2)} MB / Total ${(totalMem / (1024 * 1024)).toFixed(2)} MB)\n\n`; 37 | response += `*Top 10 Processes:*\n`; 38 | 39 | if (platform === 'win32') { 40 | 41 | const lines = stdout.trim().split('\n'); 42 | const header = lines.shift(); 43 | response += '```\n' + header + '\n'; 44 | 45 | for (const line of lines) { 46 | const parts = line.trim().split(/\s+/); 47 | // Expecting something like: [Name, PercentProcessorTime, WorkingSet] 48 | if (parts.length >= 3) { 49 | const name = parts[0]; 50 | const cpu = parts[1]; 51 | const workingSetBytes = parseInt(parts[2], 10); 52 | const workingSetMB = (workingSetBytes / (1024 * 1024)).toFixed(2); 53 | response += `${name.padEnd(20)} ${cpu.padEnd(10)} ${workingSetMB}MB\n`; 54 | } else { 55 | // If we can't parse properly, just print the line as is. 56 | response += line + '\n'; 57 | } 58 | } 59 | 60 | response += '```\n'; 61 | } else { 62 | // On Linux/macOS, we trust ps output to be already formatted nicely 63 | response += '```\n' + stdout.trim() + '\n```\n'; 64 | } 65 | 66 | ctx.replyWithMarkdown(response); 67 | }); 68 | } catch (err) { 69 | console.error('Error retrieving stats:', err); 70 | ctx.reply('An error occurred while retrieving system stats.'); 71 | } 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PC Robot Telegram Bot 2 | 3 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 4 | 5 | A Telegram bot that can control your PC through various commands such as playing/pausing media, taking screenshots, scheduling shutdowns, requesting and approving user access, fetching system stats, uploading files from the server, and more. 6 | 7 | **GitHub Repo:** [https://github.com/monokaijs/pc-robot-telegram-bot](https://github.com/monokaijs/pc-robot-telegram-bot) 8 | 9 | **Author:** [MonokaiJs](https://delimister.com) ([Telegram](https://t.me/delimister) | [Facebook](https://www.facebook.com/delimister)) 10 | 11 | ## Features 12 | 13 | - **Access Control:** 14 | The first user to interact with the bot becomes the admin. Other users must send `/request_access` to request permission, which the admin can approve. 15 | 16 | - **Media Control:** 17 | Control media playback (play/pause, next, previous, volume up/down, mute) remotely. 18 | 19 | - **Screenshots:** 20 | Capture screenshots of any monitor connected to the system and receive the images directly in Telegram. 21 | 22 | - **File Upload:** 23 | Upload files from the server's filesystem to the user via the bot. 24 | 25 | - **System Management:** 26 | Schedule or cancel system shutdowns, view system stats (CPU, RAM, top processes), and retrieve the server’s public IP. 27 | 28 | - **Command Hints:** 29 | When typing `/` in Telegram, the bot shows a list of available commands as hints. 30 | 31 | ## Requirements 32 | 33 | - **Node.js:** v14 or higher recommended. 34 | [Download Node.js](https://nodejs.org/en/download/) 35 | 36 | - **NPM:** v6 or higher. 37 | 38 | - **Telegram Bot Token:** From [BotFather](https://t.me/BotFather). 39 | 40 | - **Platform-Specific Tools:** 41 | - *Windows:* `DisplaySwitch.exe` for display switching, `taskkill` for ending processes. 42 | - *Linux/macOS:* `xrandr`, `ps`, `shutdown`, `pkill` commands if needed. 43 | 44 | ## Setup Instructions 45 | 46 | 1. **Clone the Repo:** 47 | ```bash 48 | git clone https://github.com/monokaijs/pc-robot-telegram-bot.git 49 | cd pc-robot-telegram-bot 50 | ``` 51 | 52 | 2. **Install Dependencies:** 53 | ```bash 54 | npm install 55 | ``` 56 | 57 | 3. **Create `.env` File:** 58 | ```bash 59 | cp .env.example .env 60 | ``` 61 | Edit `.env` and put your Telegram bot token: 62 | ``` 63 | TELEGRAM_TOKEN=1234567890:ABC-YourTelegramBotTokenHere 64 | ``` 65 | 66 | 4. **Run the Bot:** 67 | ```bash 68 | node bot.js 69 | ``` 70 | 71 | 5. **First Interaction:** 72 | The first user to send a message to the bot will become the admin. The admin can then approve other users who request access. 73 | 74 | ## File Structure 75 | 76 | - **bot.js:** Main entry point that initializes the bot, sets commands, loads middleware, and starts the bot. 77 | - **commands/**: Contains individual command files (e.g., `request_access.js`, `upload.js`, `stats.js`, etc.). 78 | - **middleware/**: Contains middleware like access checks and first-user-becomes-admin logic. 79 | - **callback_handlers/**: Contains handlers for inline callback queries (e.g., approving access requests). 80 | - **utils/**: Utility files for reading/writing user data and managing access control. 81 | 82 | ## Example Commands 83 | 84 | - `/request_access`: Request access if you’re not an admin or allowed user. 85 | - `/control`: Show media controls and screenshot options. 86 | - `/screenshot`: Capture and receive a screenshot. 87 | - `/upload `: Send a file located on the server. 88 | - `/cancel_shutdown`: Cancel any scheduled shutdown. 89 | - `/stats`: Display CPU load, memory usage, and top processes. 90 | - `/getip`: Get the server’s public IP. 91 | - `/secret`: A protected command only available to allowed users and admin. 92 | 93 | ## Running the Bot Automatically with Forever 94 | 95 | To ensure your Telegram bot runs continuously and starts automatically when your system boots, you can use `forever` (for Unix-based systems) or `forever-win` (for Windows). 96 | 97 | ### 1. Install Forever 98 | 99 | - **For Unix-based Systems (Linux/macOS):** 100 | ```bash 101 | npm install -g forever 102 | ``` 103 | 104 | - **For Windows:** 105 | ```bash 106 | npm install -g forever-win 107 | ``` 108 | 109 | ### 2. Start the Bot with Forever 110 | 111 | - **For Unix-based Systems:** 112 | ```bash 113 | forever start bot.js 114 | ``` 115 | 116 | - **For Windows:** 117 | ```bash 118 | forever-win start bot.js 119 | ``` 120 | 121 | ### 3. Set Up Automatic Startup 122 | 123 | - **For Unix-based Systems (Linux/macOS):** 124 | - **Using systemd:** 125 | 1. **Create a service file:** `/etc/systemd/system/pc-robot.service` 126 | 2. **Add the following content:** 127 | ```ini 128 | [Unit] 129 | Description=PC Robot Telegram Bot 130 | After=network.target 131 | 132 | [Service] 133 | ExecStart=/usr/local/bin/forever start /path/to/pc-robot-telegram-bot/bot.js 134 | Restart=always 135 | User=your_username 136 | Group=your_group 137 | Environment=PATH=/usr/bin:/usr/local/bin 138 | Environment=NODE_ENV=production 139 | WorkingDirectory=/path/to/pc-robot-telegram-bot 140 | 141 | [Install] 142 | WantedBy=multi-user.target 143 | ``` 144 | 3. **Reload systemd and enable the service:** 145 | ```bash 146 | sudo systemctl daemon-reload 147 | sudo systemctl enable pc-robot.service 148 | sudo systemctl start pc-robot.service 149 | ``` 150 | 151 | - **For Windows:** 152 | - **Using Task Scheduler:** 153 | 1. **Open Task Scheduler.** 154 | 2. **Create a new task** with the following settings: 155 | - **Trigger:** At system startup 156 | - **Action:** Start a program 157 | - **Program/script:** `forever-win` 158 | - **Add arguments:** `start bot.js` 159 | - **Start in:** `C:\path\to\pc-robot-telegram-bot` 160 | 3. **Save the task.** The bot will now start automatically when Windows boots. 161 | 162 | ### 4. Manage Forever Processes 163 | 164 | - **List running processes:** 165 | ```bash 166 | forever list 167 | ``` 168 | 169 | - **Stop the bot:** 170 | - **Unix-based Systems:** 171 | ```bash 172 | forever stop bot.js 173 | ``` 174 | - **Windows:** 175 | ```bash 176 | forever-win stop bot.js 177 | ``` 178 | 179 | - **Restart the bot:** 180 | - **Unix-based Systems:** 181 | ```bash 182 | forever restart bot.js 183 | ``` 184 | - **Windows:** 185 | ```bash 186 | forever-win restart bot.js 187 | ``` 188 | 189 | **Note:** 190 | - Forever ensures that your bot restarts automatically if it crashes or the system reboots. 191 | - Regularly check the logs to monitor the bot's performance and troubleshoot any issues. 192 | 193 | ## Error Handling & Stability 194 | 195 | - The bot uses `bot.catch()` to log errors and continue running, preventing crashes from unexpected issues. 196 | - Ensure proper permissions and paths exist for files and commands to avoid runtime errors. 197 | 198 | ```javascript 199 | bot.catch((err, ctx) => { 200 | console.error(`Bot Error: ${err}`, ctx.updateType); 201 | }); 202 | ``` 203 | 204 | ## Contributing 205 | 206 | 1. **Fork the repository.** 207 | 2. **Create your feature branch:** 208 | ```bash 209 | git checkout -b feature/new-feature 210 | ``` 211 | 3. **Commit your changes:** 212 | ```bash 213 | git commit -m 'Add new feature' 214 | ``` 215 | 4. **Push to the branch:** 216 | ```bash 217 | git push origin feature/new-feature 218 | ``` 219 | 5. **Open a Pull Request.** 220 | 221 | ## License 222 | 223 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 224 | --------------------------------------------------------------------------------