├── LICENSE ├── README.md ├── bot ├── bot.py ├── bot_setup.py ├── db.py └── utils.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tostapunk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | logo 3 | 4 | # PC Control bot 5 | 6 | Through this bot you can execute actions on your PC directly from Telegram! 7 | 8 | ## Getting Started 9 | 10 | ### Prerequisites 11 | 12 | - Python 3.6+ 13 | - A [BotFather](https://t.me/BotFather) token 14 | 15 | GNU/Linux users: to use UI features (setup with UI and memo) you need to install the python-tk package 16 | 17 | ### Install the requirements 18 | Execute ```python -m pip install -r requirements.txt``` 19 | 20 | ## Setup the bot 21 | ### UI 22 | Launch the setup with ```python bot/bot_setup.py``` 23 | 24 | Add your BotFather token and start it! 25 | 26 | ![setup](https://user-images.githubusercontent.com/25140297/103703845-95b99680-4fa8-11eb-9b09-b660760de701.png) 27 | 28 | ### Command line 29 | You can also setup the bot from the command line by using ```python bot/bot_setup.py``` followed by a valid option.\ 30 | To see all the available options use ```python bot/bot_setup.py -h``` 31 | 32 | ## Set the permissions 33 | 34 | **The first user registered into the database will have admin permissions by default.** \ 35 | You can add or remove a user from the admin group by using the UI or the command line.\ 36 | **Note:** you need to use a Telegram username (write it without '@') 37 | 38 | UI example: 39 | 40 | ![privs](https://user-images.githubusercontent.com/25140297/103581006-76086c80-4edb-11eb-99a4-4e13777e7794.png) 41 | 42 | ## Available commands 43 | 44 | | Command | Description | Note 45 | | --- | --- | --- | 46 | | /shutdown | Shutdown your PC | 47 | | /reboot | Reboot your PC | 48 | | /logout | Log out from your current account | Currently not working on Linux | 49 | | /hibernate | Hibernate your PC | 50 | | /lock | Lock your PC | Currently not working on Linux | 51 | | /cancel | Annul the previous command | It work with the first two commands_t | 52 | | /check | Check the PC status | 53 | | /launch | Launch a program | Example: /launch notepad | 54 | | /link | Open a web link | Example: /link http://google.com (don't use "www") | 55 | | /memo | Show a memo on your pc | Tkinter needed | 56 | | /task | Check if a process is currently running or kill it| Example: /task chrome | 57 | | /screen | Take a screenshot and receive it | 58 | | /menu | Shows the inline menu | 59 | | /kb or /keyboard | Brings the normal keyboard up | 60 | 61 | You can set a delay time for the execution of the first four commands by using _t + time in minutes after a command.\ 62 | Example: ```/shutdown_t 2``` 63 | 64 | ## Contributors 65 | Thanks to [Jasoc](https://github.com/jasoc) for the awesome [logo](https://i.imgur.com/V6B5ZEf.png)! 66 | -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import ctypes 5 | import getpass 6 | import logging 7 | import os 8 | import platform 9 | import socket 10 | import subprocess 11 | import sys 12 | import threading 13 | try: 14 | import tkinter as tk 15 | from tkinter import ttk 16 | except ImportError: 17 | pass 18 | from typing import Optional 19 | from shlex import quote 20 | 21 | import distro 22 | import psutil 23 | import pyscreenshot 24 | from telegram import ParseMode, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, Bot 25 | from telegram import error as tg_error 26 | from telegram.ext import CommandHandler, Filters, MessageHandler, Updater, CallbackContext 27 | from telegram.utils import helpers 28 | 29 | import db 30 | import utils 31 | 32 | if sys.version_info < (3, 6, 0): 33 | raise Exception("This bot works only with Python 3.6+") 34 | 35 | if db.exists() is False: 36 | raise Exception("You need to start bot_setup first") 37 | 38 | # Enable logging 39 | logging.basicConfig( 40 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 41 | level=logging.DEBUG) 42 | logger = logging.getLogger(__name__) 43 | 44 | 45 | def startupinfo() -> Optional[int]: 46 | if db.console_get() == "hide": 47 | if platform.system() == "Windows": 48 | value = subprocess.STARTUPINFO() 49 | value.dwFlags |= subprocess.STARTF_USESHOWWINDOW 50 | else: 51 | value = None 52 | else: 53 | value = None 54 | return value 55 | 56 | 57 | def start(update: Update, context: CallbackContext) -> None: 58 | db.update_user(update.message.from_user, context.bot) 59 | text = """Welcome to [PC\-Control bot](https://goo.gl/9TjCHR), 60 | you can get the bot profile picture [here](http://i.imgur.com/294uZ8G.png) 61 | 62 | Use /help to see all the commands\! 63 | 64 | 65 | Made by [Tostapunk](https://github.com/Tostapunk)""" 66 | 67 | context.bot.sendMessage( 68 | chat_id=update.message.chat.id, 69 | text=text, 70 | parse_mode=ParseMode.MARKDOWN_V2, 71 | disable_web_page_preview="true") 72 | keyboard_up(update, context) 73 | 74 | 75 | @db.admin_check 76 | def bot_help(update: Update, context: CallbackContext) -> None: 77 | db.update_user(update.message.from_user, context.bot) 78 | text = "*Available commands:*" 79 | text += helpers.escape_markdown(""" 80 | /shutdown - To shutdown your PC 81 | /reboot - To reboot your PC 82 | /logout - To log out from your current account 83 | /hibernate - To hibernate your PC 84 | /lock - To lock your PC 85 | /cancel - To annul the previous command 86 | /check - To check the PC status 87 | /launch - To launch a program | Example: /launch notepad 88 | /link - To open a link | Example: /link http://google.com (don't use \"www\") 89 | /memo - To show a memo on your pc 90 | /task - To check if a process is currently running or to kill it | Example: /task chrome 91 | /screen - To take a screenshot and receive it 92 | /kb or /keyboard - Brings the normal keyboard up 93 | 94 | You can set a delay time for the execution of the first four commands by using _t + time in minutes after a command. 95 | Example: /shutdown_t 2""", 2) 96 | context.bot.sendMessage( 97 | chat_id=update.message.chat.id, 98 | text=text, 99 | parse_mode=ParseMode.MARKDOWN_V2, 100 | disable_web_page_preview="true") 101 | 102 | 103 | def keyboard_up(update: Update, context: CallbackContext) -> None: 104 | db.update_user(update.message.from_user, context.bot) 105 | text = "Keyboard is up" 106 | keyboard = [['Shutdown', 'Reboot'], 107 | ['Logout', 'Hibernate'], 108 | ['Lock', 'Screenshot'], 109 | ['PC status'], 110 | ['Cancel'], 111 | ['Close Keyboard']] 112 | reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) 113 | update.message.reply_text(reply_markup=reply_markup, text=text) 114 | 115 | 116 | def message_handler(update: Update, context: CallbackContext) -> None: 117 | if update.message.text == "Shutdown": 118 | shutdown(update, context) 119 | elif update.message.text == "Reboot": 120 | reboot(update, context) 121 | elif update.message.text == "Logout": 122 | logout(update, context) 123 | elif update.message.text == "Hibernate": 124 | hibernate(update, context) 125 | elif update.message.text == "Lock": 126 | lock(update, context) 127 | elif update.message.text == "PC status": 128 | check(update, context) 129 | elif update.message.text == "Screenshot": 130 | screenshot(update, context) 131 | elif update.message.text == "Cancel": 132 | cancel(update, context) 133 | elif update.message.text == "Close Keyboard": 134 | text = "Keyboard is down." 135 | reply_markup = ReplyKeyboardRemove() 136 | update.message.reply_text(text=text, reply_markup=reply_markup) 137 | elif update.message.text == f"Kill {update.message.text[5:]}": 138 | task_kill(update, context) 139 | elif update.message.text == "Exit": 140 | keyboard_up(update, context) 141 | 142 | 143 | @db.admin_check 144 | def shutdown(update: Update, context: CallbackContext) -> None: 145 | db.update_user(update.message.from_user, context.bot) 146 | if platform.system() == "Windows": 147 | subprocess.run('shutdown /s', startupinfo=startupinfo()) 148 | text = "Shutted down." 149 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 150 | else: 151 | subprocess.run('shutdown -h now', startupinfo=startupinfo(), shell=True) 152 | text = "Shutted down." 153 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 154 | 155 | 156 | @db.admin_check 157 | def shutdown_time(update: Update, context: CallbackContext) -> None: 158 | db.update_user(update.message.from_user, context.bot) 159 | if context.args: 160 | if platform.system() == "Windows": 161 | subprocess.run(f"shutdown /s /t {str(int(context.args[0])*60)}", 162 | startupinfo=startupinfo()) 163 | text = "Shutting down..." 164 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 165 | else: 166 | subprocess.run(f"shutdown -P +{quote(context.args[0])}", 167 | startupinfo=startupinfo(), shell=True) 168 | text = "Shutting down..." 169 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 170 | else: 171 | text = """No time inserted 172 | ``` Usage: /shutdown_t + time in minutes```""" 173 | context.bot.sendMessage(chat_id=update.message.chat.id, 174 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 175 | 176 | 177 | @db.admin_check 178 | def reboot(update: Update, context: CallbackContext) -> None: 179 | db.update_user(update.message.from_user, context.bot) 180 | if platform.system() == "Windows": 181 | subprocess.run('shutdown /r', startupinfo=startupinfo()) 182 | text = "Rebooted." 183 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 184 | else: 185 | subprocess.run('reboot', startupinfo=startupinfo()) 186 | text = "Rebooted." 187 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 188 | 189 | 190 | @db.admin_check 191 | def reboot_time(update: Update, context: CallbackContext) -> None: 192 | db.update_user(update.message.from_user, context.bot) 193 | if context.args: 194 | if platform.system() == "Windows": 195 | subprocess.run(f"shutdown /r /t {str(int(context.args[0])*60)}", 196 | startupinfo=startupinfo()) 197 | text = "Rebooting..." 198 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 199 | else: 200 | subprocess.run(f"shutdown -r +{quote(context.args[0])}", 201 | startupinfo=startupinfo(), shell=True) 202 | text = "Rebooting..." 203 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 204 | else: 205 | text = """No time inserted 206 | ``` Usage: /reboot_t + time in minutes```""" 207 | context.bot.sendMessage(chat_id=update.message.chat.id, 208 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 209 | 210 | 211 | @db.admin_check 212 | def logout(update: Update, context: CallbackContext) -> None: 213 | db.update_user(update.message.from_user, context.bot) 214 | if platform.system() == "Windows": 215 | subprocess.run('shutdown /l', startupinfo=startupinfo()) 216 | text = "Logged out." 217 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 218 | else: 219 | text = "Currently not working on Linux." 220 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 221 | 222 | 223 | @db.admin_check 224 | def logout_time(update: Update, context: CallbackContext) -> None: 225 | db.update_user(update.message.from_user, context.bot) 226 | if platform.system() != "Windows": 227 | text = "Currently not working on Linux." 228 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 229 | elif not context.args: 230 | text = """No time inserted ```Usage: /logout_t + time in minutes```""" 231 | context.bot.sendMessage(chat_id=update.message.chat.id, 232 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 233 | else: 234 | time = int(context.args[0]) 235 | if thread_name := utils.ThreadTimer().start(logout, time, update, context): 236 | text = f"{thread_name} function is already running, cancel it and retry" 237 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 238 | 239 | 240 | @db.admin_check 241 | def hibernate(update: Update, context: CallbackContext) -> None: 242 | db.update_user(update.message.from_user, context.bot) 243 | if platform.system() == "Windows": 244 | subprocess.run('shutdown /h', startupinfo=startupinfo()) 245 | text = "Hibernated." 246 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 247 | else: 248 | subprocess.run('systemctl suspend', startupinfo=startupinfo(), shell=True) 249 | text = "Hibernated." 250 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 251 | 252 | 253 | @db.admin_check 254 | def hibernate_time(update: Update, context: CallbackContext) -> None: 255 | db.update_user(update.message.from_user, context.bot) 256 | if not context.args: 257 | text = """No time inserted ```Usage: /hibernate_t + time in minutes```""" 258 | context.bot.sendMessage(chat_id=update.message.chat.id, 259 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 260 | else: 261 | time = int(context.args[0]) 262 | if thread_name := utils.ThreadTimer().start(hibernate, time, update, context): 263 | text = f"{thread_name} function is already running, cancel it and retry" 264 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 265 | 266 | 267 | @db.admin_check 268 | def lock(update: Update, context: CallbackContext) -> None: 269 | db.update_user(update.message.from_user, context.bot) 270 | if platform.system() == "Windows": 271 | ctypes.windll.user32.LockWorkStation() 272 | text = "PC locked." 273 | else: 274 | text = "Currently not working on Linux." 275 | context.bot.sendMessage(chat_id=update.message.from_user.id, text=text) 276 | 277 | 278 | @db.admin_check 279 | def cancel(update: Update, context: CallbackContext) -> None: 280 | db.update_user(update.message.from_user, context.bot) 281 | if (thread_name := utils.ThreadTimer().stop()): 282 | text = f"{thread_name} cancelled" 283 | else: 284 | text = "Annulled." 285 | if platform.system() == "Windows": 286 | subprocess.run('shutdown /a', startupinfo=startupinfo()) 287 | else: 288 | subprocess.run('shutdown -c', startupinfo=startupinfo(), shell=True) 289 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 290 | 291 | 292 | @db.admin_check 293 | def check(update: Update, context: CallbackContext) -> None: 294 | db.update_user(update.message.from_user, context.bot) 295 | text = "" 296 | text += "Your PC is online.\n\n" 297 | text += f"PC name: {socket.gethostname()}" 298 | text += f"\nLogged user: {getpass.getuser()}" 299 | if platform.system() == "Windows": 300 | text += f"\nOS: Windows {platform.win32_ver()[0]}" 301 | else: 302 | text += f"\nOS: {distro.id()}" 303 | text += f"\nCPU: {str(psutil.cpu_percent())}%" 304 | text += f"\nMemory: {str(int(psutil.virtual_memory().percent))}%" 305 | battery = psutil.sensors_battery() 306 | if battery: 307 | text += f"\nBattery: {str(format(battery.percent, '.0f'))}%" 308 | if battery.power_plugged is True: 309 | text += " | Charging" 310 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 311 | 312 | 313 | @db.admin_check 314 | def launch(update: Update, context: CallbackContext) -> None: 315 | db.update_user(update.message.from_user, context.bot) 316 | if context.args: 317 | if platform.system() == "Windows": 318 | ret = subprocess.run(f"start {quote(context.args[0])}", 319 | startupinfo=startupinfo(), shell=True).returncode 320 | text = f"Launching {context.args[0]}..." if ret == 0 else f"Cannot launch {context.args[0]}" 321 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 322 | else: 323 | ret = subprocess.run(f"{str(quote(context.args[0]))} &", 324 | startupinfo=startupinfo(), shell=True).returncode 325 | text = f"Launching {context.args[0]}..." if ret == 0 else f"Cannot launch {context.args[0]}" 326 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 327 | else: 328 | text = """No program name inserted 329 | ``` Usage: /launch + program name```""" 330 | context.bot.sendMessage(chat_id=update.message.chat.id, 331 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 332 | 333 | 334 | @db.admin_check 335 | def link(update: Update, context: CallbackContext) -> None: 336 | db.update_user(update.message.from_user, context.bot) 337 | if context.args: 338 | if platform.system() == "Windows": 339 | ret = subprocess.run(f"start {quote(context.args[0])}", 340 | startupinfo=startupinfo(), shell=True).returncode 341 | text = f"Opening {context.args[0]}..." if ret == 0 else f"Cannot open {context.args[0]}" 342 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 343 | else: 344 | ret = subprocess.run(f"xdg-open {quote(context.args[0])}", 345 | startupinfo=startupinfo(), shell=True).returncode 346 | text = f"Opening {context.args[0]}..." if ret == 0 else f"Cannot open {context.args[0]}" 347 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 348 | else: 349 | text = "No link inserted\n``` Usage: /link + web link```" 350 | context.bot.sendMessage(chat_id=update.message.chat.id, 351 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 352 | 353 | 354 | @db.admin_check 355 | def memo_thread(update: Update, context: CallbackContext) -> None: 356 | db.update_user(update.message.from_user, context.bot) 357 | module = "tkinter" 358 | if module not in sys.modules: 359 | context.bot.sendMessage(chat_id=update.message.chat.id, text=f"Error: you need to install {module} to use this function") 360 | 361 | args = update.message.text[6:] 362 | if len(args) != 0: 363 | def memo() -> None: 364 | popup = tk.Tk() 365 | popup.wm_title("Memo") 366 | label = ttk.Label( 367 | popup, 368 | text=f"{args} \ 369 | \nsent by {update.message.from_user.name} through PC-Control", 370 | font=( 371 | "Helvetica", 372 | 10)) 373 | label.pack(side="top", fill="x", pady=10) 374 | B1 = ttk.Button(popup, text="Okay", command=popup.destroy) 375 | B1.pack() 376 | popup.mainloop() 377 | 378 | t = threading.Thread(target=memo) 379 | t.start() 380 | else: 381 | text = "No text inserted" 382 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 383 | 384 | 385 | @db.admin_check 386 | def task(update: Update, context: CallbackContext) -> None: 387 | db.update_user(update.message.from_user, context.bot) 388 | if context.args: 389 | kill_kb = [[f"Kill {context.args[0]}"], 390 | ["Exit"]] 391 | reply_markup = ReplyKeyboardMarkup( 392 | kill_kb, resize_keyboard=True) 393 | if platform.system() == "Windows": 394 | try: 395 | out = os.popen(f"tasklist | findstr {context.args[0]}").read() 396 | context.bot.sendMessage(chat_id=update.message.chat.id, 397 | text=out, reply_markup=reply_markup) 398 | except tg_error.BadRequest: 399 | context.bot.sendMessage(chat_id=update.message.chat.id, text="The program is not running") 400 | else: 401 | try: 402 | out = os.popen(f"ps -A | grep {context.args[0]}").read() 403 | context.bot.sendMessage(chat_id=update.message.chat.id, text=out, reply_markup=reply_markup) 404 | except tg_error.BadRequest: 405 | context.bot.sendMessage(chat_id=update.message.chat.id, text="The program is not running") 406 | else: 407 | text = "No task inserted\n``` Usage: /task + process name```" 408 | context.bot.sendMessage(chat_id=update.message.chat.id, 409 | text=text, parse_mode=ParseMode.MARKDOWN_V2) 410 | 411 | 412 | @db.admin_check 413 | def task_kill(update: Update, context: CallbackContext) -> None: 414 | db.update_user(update.message.from_user, context.bot) 415 | args = update.message.text[5:] 416 | if platform.system() == "Windows": 417 | try: 418 | ret = subprocess.run(f"tskill {quote(args)}", startupinfo=startupinfo()).returncode 419 | text =f"I've killed {args}" if ret == 0 else f"Cannot kill {args}" 420 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 421 | keyboard_up(update, context) 422 | except Exception as e: 423 | context.bot.sendMessage(chat_id=update.message.chat.id, text=f"Error: {str(e)}") 424 | else: 425 | try: 426 | ret = subprocess.run(f'pkill -f "^"{quote(args)}', startupinfo=startupinfo(), shell=True).returncode 427 | text =f"I've killed {args}" if ret == 0 else f"Cannot kill {args}" 428 | context.bot.sendMessage(chat_id=update.message.chat.id, text=text) 429 | keyboard_up(update, context) 430 | except Exception as e: 431 | context.bot.sendMessage(chat_id=update.message.chat.id, text=f"Error: {str(e)}") 432 | 433 | 434 | @db.admin_check 435 | def screenshot(update: Update, context: CallbackContext) -> None: 436 | db.update_user(update.message.from_user, context.bot) 437 | path = os.path.join(os.path.dirname(utils.current_path()), "tmp/screenshot.png") 438 | img = pyscreenshot.grab() 439 | img.save(path) 440 | context.bot.send_document(chat_id=update.message.chat.id, document=open(path, "rb")) 441 | os.remove(path) 442 | 443 | 444 | def is_up_notification(bot: Bot) -> None: 445 | admins = db.get_admins_id() 446 | for admin_id in admins: 447 | bot.sendMessage(chat_id=admin_id, text="Bot up and running") 448 | 449 | 450 | def error(update: Update, context: CallbackContext) -> None: 451 | logger.warning(f"Update {update} caused error {error}") 452 | 453 | 454 | def main() -> None: 455 | # Create the EventHandler and pass it your bot's token. 456 | if not db.token_get("BotFather_token"): 457 | raise ValueError("You need to add a BotFather token first") 458 | 459 | updater = Updater(db.token_get("BotFather_token")) 460 | # Get the dispatcher to register handlers 461 | dp = updater.dispatcher 462 | 463 | # Send a message when the bot is up and running 464 | is_up_notification(updater.bot) 465 | 466 | # Start 467 | dp.add_handler(CommandHandler("start", start)) 468 | 469 | # Help 470 | dp.add_handler(CommandHandler("help", bot_help)) 471 | 472 | # Send keyboard up 473 | dp.add_handler(CommandHandler("keyboard", keyboard_up)) 474 | dp.add_handler(CommandHandler("kb", keyboard_up)) 475 | 476 | # Shutdown 477 | dp.add_handler(CommandHandler("shutdown", shutdown)) 478 | 479 | # Shutdown time 480 | dp.add_handler(CommandHandler( 481 | "shutdown_t", shutdown_time, pass_args=True)) 482 | 483 | # Reboot 484 | dp.add_handler(CommandHandler("reboot", reboot)) 485 | 486 | # Reboot time 487 | dp.add_handler(CommandHandler( 488 | "reboot_t", reboot_time, pass_args=True)) 489 | 490 | # Log out 491 | dp.add_handler(CommandHandler("logout", logout)) 492 | 493 | # Log out time 494 | dp.add_handler(CommandHandler("logout_t", logout_time)) 495 | 496 | # Hibernate 497 | dp.add_handler(CommandHandler("hibernate", hibernate)) 498 | 499 | # Hibernate time 500 | dp.add_handler(CommandHandler("hibernate_t", hibernate_time)) 501 | 502 | # Lock 503 | dp.add_handler(CommandHandler("lock", lock)) 504 | 505 | # Annul the previous command 506 | dp.add_handler(CommandHandler("cancel", cancel)) 507 | 508 | # Check the PC status 509 | dp.add_handler(CommandHandler("check", check)) 510 | 511 | # Launch a program 512 | dp.add_handler(CommandHandler("launch", launch, pass_args=True)) 513 | 514 | # Open a link with the default browser 515 | dp.add_handler(CommandHandler("link", link, pass_args=True)) 516 | 517 | # Show a popup with the memo 518 | dp.add_handler(CommandHandler("memo", memo_thread, pass_args=True)) 519 | 520 | # Check if a program is currently active 521 | dp.add_handler(CommandHandler("task", task, pass_args=True)) 522 | 523 | # Kill the selected process 524 | dp.add_handler(CommandHandler("task_kill", task_kill)) 525 | 526 | # Send a full screen screenshot 527 | dp.add_handler(CommandHandler("screen", screenshot)) 528 | 529 | # Keyboard Button Reply 530 | dp.add_handler(MessageHandler(Filters.text | 531 | Filters.status_update, message_handler)) 532 | 533 | # Log all errors 534 | dp.add_error_handler(error) 535 | 536 | # Start the Bot 537 | updater.start_polling() 538 | 539 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 540 | # SIGTERM or SIGABRT. This should be used most of the time, since 541 | # start_polling() is non-blocking and will stop the bot gracefully. 542 | updater.idle() 543 | 544 | 545 | if __name__ == "__main__": 546 | main() 547 | -------------------------------------------------------------------------------- /bot/bot_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import os 6 | import pathlib 7 | import platform 8 | import subprocess 9 | import sys 10 | try: 11 | import tkinter as tk 12 | from tkinter import Entry, Label, Menu, LEFT 13 | except ImportError: 14 | if len(sys.argv) == 1: 15 | print("To use the UI mode you need to install tkinter. To use the command line mode type python bot/bot_setup.py -h\n") 16 | from typing import Optional, Any 17 | 18 | import db 19 | import utils 20 | 21 | if platform.system() == "Windows": 22 | import winreg 23 | 24 | if sys.version_info < (3, 6, 0): 25 | raise Exception("This bot works only with Python 3.6+") 26 | 27 | 28 | def startupinfo() -> Optional[int]: 29 | if db.console_get() == "hide": 30 | if platform.system() == "Windows": 31 | value = subprocess.STARTUPINFO() 32 | value.dwFlags |= subprocess.STARTF_USESHOWWINDOW 33 | else: 34 | value = None 35 | else: 36 | value = None 37 | return value 38 | 39 | 40 | def db_and_co() -> None: 41 | pathlib.Path(os.path.join(os.path.dirname(utils.current_path()), "data")).mkdir(parents=True, exist_ok=True) 42 | pathlib.Path(os.path.join(os.path.dirname(utils.current_path()), "tmp")).mkdir(parents=True, exist_ok=True) 43 | db.create() 44 | 45 | 46 | def tokens_check(btn: Any=None) -> None: 47 | if not db.token_get("BotFather_token"): 48 | if btn: 49 | btn.configure(text="Confirm") 50 | else: 51 | if btn: 52 | btn.configure(text="Change token") 53 | 54 | 55 | def botfather_token_set(token: str, entry: Any=None, btn: Any=None, lbl: Any=None) -> None: 56 | if token: 57 | db.token_set("BotFather_token", token) 58 | text = "Token saved!" 59 | if entry and btn and lbl: 60 | entry.destroy() 61 | btn.destroy() 62 | lbl.configure(text=text, font="Times 11", fg="green", justify=LEFT) 63 | else: 64 | print(text+"\n") 65 | else: 66 | text="Your entry is empty" 67 | if lbl: 68 | lbl.configure(text=text, font="Times 11", fg="red", justify=LEFT) 69 | else: 70 | print(text+"\n") 71 | 72 | 73 | def bot_start(root: Any=None) -> None: 74 | if root: 75 | root.withdraw() 76 | if startupinfo() is not None or platform.system() == "Windows": 77 | if db.startup_get() == "true": 78 | subprocess.run(f"{sys.executable} {os.path.join(utils.current_path(), 'bot.pyw')}", creationflags=0x08000000, shell=True) 79 | else: 80 | subprocess.run(f"{sys.executable} {os.path.join(utils.current_path(), 'bot.py')}", creationflags=0x08000000, shell=True) 81 | else: 82 | if db.startup_get() == "true": 83 | subprocess.run(f"{sys.executable} {os.path.join(utils.current_path(), 'bot.pyw')}", shell=True) 84 | else: 85 | subprocess.run(f"{sys.executable} {os.path.join(utils.current_path(), 'bot.py')}", shell=True) 86 | 87 | 88 | def privs_window(root: Any) -> None: 89 | privs = tk.Toplevel(root) 90 | privs.wm_title("Permissions") 91 | usr_l = Label(privs, text="Username", font="Times 11 bold", justify=LEFT) 92 | usr_l.pack() 93 | usr_e = Entry(privs, bd=5) 94 | usr_e.pack() 95 | add_b = tk.Button(privs, text="Add permissions", 96 | command=lambda: add_privs(usr_e.get(), usr_e, add_b, rm_b, usr_done)) 97 | add_b.pack() 98 | rm_b = tk.Button(privs, text="Remove permissions", 99 | command=lambda: remove_privs(usr_e.get(), usr_e, add_b, rm_b, usr_done)) 100 | rm_b.pack() 101 | usr_done = Label(privs, text="") 102 | usr_done.pack() 103 | 104 | 105 | def add_privs(user: str, usr_e: Any=None, add_b: Any=None, rm_b: Any=None, usr_done_l: Any=None) -> None: 106 | if db.user_exists(user): 107 | db.user_role(user, admin=True) 108 | text = f"Permissions for {user} changed" 109 | if usr_e and add_b and rm_b and usr_done_l: 110 | usr_e.destroy() 111 | add_b.destroy() 112 | rm_b.destroy() 113 | usr_done_l.configure(text=text, font="Times 11", fg="green", justify=LEFT) 114 | else: 115 | print(text) 116 | else: 117 | text = f"{user} isn't in your database" 118 | if usr_done_l: 119 | usr_done_l.configure(text=text, font="Times 11", fg="red", justify=LEFT) 120 | else: 121 | print(text) 122 | 123 | 124 | def remove_privs(user: str, usr_e: Any=None, add_b: Any=None, rm_b: Any=None, usr_done_l: Any=None) -> None: 125 | if db.user_exists(user): 126 | db.user_role(user, admin=False) 127 | text = f"Permissions for {user} changed" 128 | if usr_e and add_b and rm_b and usr_done_l: 129 | usr_e.destroy() 130 | add_b.destroy() 131 | rm_b.destroy() 132 | usr_done_l.configure(text=text, font="Times 11", fg="green", justify=LEFT) 133 | else: 134 | print(text) 135 | else: 136 | text = f"{user} isn't in your database" 137 | if usr_done_l: 138 | usr_done_l.configure(text=text, font="Times 11", fg="red", justify=LEFT) 139 | else: 140 | print(text) 141 | 142 | 143 | def restart_popup(root: Any) -> None: 144 | privs = tk.Toplevel(root) 145 | privs.wm_title("Restart") 146 | lp = Label(privs, text="Please restart bot_setup to apply the change", 147 | font="Times 11", justify=LEFT) 148 | lp.pack() 149 | add_b = tk.Button(privs, text="Restart", command=lambda: restart()) 150 | add_b.pack() 151 | 152 | def restart() -> None: 153 | python = sys.executable 154 | os.execl(python, python, *sys.argv) 155 | 156 | 157 | def console_show(ui: bool=False, root: Any=None) -> None: 158 | db.console_set("show") 159 | if ui and root: 160 | restart_popup(root) 161 | 162 | 163 | def console_hide(ui: bool=False, root: Any=None) -> None: 164 | db.console_set("hide") 165 | if ui and root: 166 | restart_popup(root) 167 | 168 | 169 | def startup_enable(root: Any=None) -> None: 170 | if db.startup_get() == "false": 171 | py_path = os.path.join(utils.current_path(), "bot.py") 172 | pyw_path = os.path.join(utils.current_path(), "bot.pyw") 173 | if platform.system() == "Windows": 174 | if os.path.isfile(py_path) is True: 175 | os.rename(py_path, pyw_path) 176 | key = winreg.OpenKey( 177 | winreg.HKEY_CURRENT_USER, 178 | 'Software\Microsoft\Windows\CurrentVersion\Run', 0, 179 | winreg.KEY_SET_VALUE) 180 | winreg.SetValueEx(key, 'PC-Control', 0, winreg.REG_SZ, '"' + pyw_path +'"') 181 | key.Close() 182 | db.startup_set("true") 183 | else: 184 | if os.path.isfile(py_path) is True: 185 | os.rename(py_path, pyw_path) 186 | try: 187 | xdg_autostart_user_config_path = os.path.join(str(pathlib.Path.home()), ".config/autostart/") 188 | os.makedirs(xdg_autostart_user_config_path, exist_ok=True) 189 | text = "[Desktop Entry]\n" 190 | text += "Type=Application\n" 191 | text += f"Path={utils.current_path()}/\n" 192 | text += f"Exec={sys.executable} bot.pyw\n" 193 | text += "Name=PC-Control bot\n" 194 | text += "Comment=PC-Control bot startup\n" 195 | text += "\n" 196 | with open(os.path.join(xdg_autostart_user_config_path + "PC-Control.desktop"), 'x') as file: 197 | file.write(text) 198 | db.startup_set("true") 199 | except IOError as e: 200 | text = f"Error: {str(e)}" 201 | if root: 202 | error = tk.Toplevel(root) 203 | error.wm_title("Error") 204 | warn_l = Label(error, text=text, font="Times 11 bold", justify=LEFT) 205 | warn_l.pack() 206 | ok_b = tk.Button(error, text="Okay", command=lambda: error.destroy()) 207 | ok_b.pack() 208 | else: 209 | print(text+"\n") 210 | else: 211 | text = "Already enabled" 212 | if root: 213 | error = tk.Toplevel(root) 214 | error.wm_title("Error") 215 | warn_l = Label(error, text=text, font="Times 11 bold", justify=LEFT) 216 | warn_l.pack() 217 | ok_b = tk.Button(error, text="Okay", 218 | command=lambda: error.destroy()) 219 | ok_b.pack() 220 | else: 221 | print(text+"\n") 222 | 223 | 224 | def startup_disable(root: Any=None) -> None: 225 | if db.startup_get() == "true": 226 | py_path = os.path.join(utils.current_path(), "bot.py") 227 | pyw_path = os.path.join(utils.current_path(), "bot.pyw") 228 | if platform.system() == "Windows": 229 | key = winreg.OpenKey( 230 | winreg.HKEY_CURRENT_USER, 231 | 'Software\Microsoft\Windows\CurrentVersion\Run', 0, 232 | winreg.KEY_SET_VALUE) 233 | winreg.DeleteValue(key, 'PC-Control') 234 | key.Close() 235 | os.rename(pyw_path, py_path) 236 | db.startup_set("false") 237 | else: 238 | try: 239 | os.rename(pyw_path, py_path) 240 | os.remove(os.path.join(str(pathlib.Path.home()),".config/autostart/PC-Control.desktop")) 241 | db.startup_set("false") 242 | except OSError as e: 243 | os.rename(py_path, pyw_path) 244 | text = f"Error: {str(e)}" 245 | if root: 246 | error = tk.Toplevel(root) 247 | error.wm_title("Error") 248 | warn_l = Label(error, text=text, font="Times 11 bold", justify=LEFT) 249 | warn_l.pack() 250 | ok_b = tk.Button(error, text="Okay", command=lambda: error.destroy()) 251 | ok_b.pack() 252 | else: 253 | print(text+"\n") 254 | else: 255 | text = "Already disabled" 256 | if root: 257 | error = tk.Toplevel(root) 258 | error.wm_title("Error") 259 | warn_l = Label(error, text=text, font="Times 11 bold", justify=LEFT) 260 | warn_l.pack() 261 | ok_b = tk.Button(error, text="Okay", 262 | command=lambda: error.destroy()) 263 | ok_b.pack() 264 | else: 265 | print(text+"\n") 266 | 267 | 268 | def ui() -> None: 269 | root = tk.Tk() 270 | root.wm_title("Setup") 271 | root.geometry("200x200") 272 | root.resizable(width=False, height=False) 273 | 274 | bft_l = Label(root, text="BotFather token", font="Times 11 bold", justify=LEFT) 275 | bft_l.pack() 276 | bft_e = Entry(root, bd=5) 277 | bft_e.pack() 278 | bft_b = tk.Button( 279 | root, text="", command=lambda: botfather_token_set(bft_e.get(), bft_e, bft_b, bft_done_l)) 280 | bft_b.pack(pady=5) 281 | bft_done_l = Label(root, text="") 282 | bft_done_l.pack() 283 | 284 | start_btn = tk.Button(root, text="Start it!", command=lambda: bot_start(root)) 285 | start_btn.pack(pady=5) 286 | 287 | privs_btn = tk.Button(root, text="Change user permissions", command= lambda: privs_window(root)) 288 | privs_btn.pack(pady=5) 289 | 290 | db_and_co() 291 | menubar = Menu(root) 292 | root.config(menu=menubar) 293 | filemenu = Menu(menubar, tearoff=0) 294 | menubar.add_cascade(label="Options", menu=filemenu) 295 | 296 | console_menu = Menu(root, tearoff=0) 297 | console_menu.add_command(label="Show", command=lambda: console_show(True, root)) 298 | console_menu.add_command(label="Hide", command=lambda: console_hide(True, root)) 299 | filemenu.add_cascade(label="Console", menu=console_menu, underline=0) 300 | 301 | startup_menu = Menu(root, tearoff=0) 302 | startup_menu.add_command(label="Enable", command=lambda: startup_enable(root)) 303 | startup_menu.add_command(label="Disable", command=lambda: startup_disable(root)) 304 | filemenu.add_cascade(label="Startup", menu=startup_menu, underline=0) 305 | 306 | db_and_co() 307 | tokens_check(bft_b) 308 | root.mainloop() 309 | 310 | 311 | def parse_args() -> bool: 312 | parser = argparse.ArgumentParser(description="PC-Control setup utility", epilog="If called without options it will launch in UI mode") 313 | group = parser.add_mutually_exclusive_group() 314 | group.add_argument("--token", help="BotFather token", metavar="BotFather_token") 315 | group.add_argument("--admin_add", help="add user to admin group", metavar="username") 316 | group.add_argument("--admin_remove", help="remove user from admin group", metavar="username") 317 | group.add_argument("--output_show", help="show command line output", action="store_true") 318 | group.add_argument("--output_hide", help="hide command line output", action="store_true") 319 | group.add_argument("--startup_enable", help="enable startup (run auomatically at startup)", action="store_true") 320 | group.add_argument("--startup_disable", help="disable startup", action="store_true") 321 | group.add_argument("--start", help="start the bot", action="store_true") 322 | 323 | args = parser.parse_args() 324 | 325 | if args.token: 326 | botfather_token_set(args.token) 327 | elif args.start: 328 | bot_start() 329 | elif args.admin_add: 330 | add_privs(args.admin_add) 331 | elif args.admin_remove: 332 | remove_privs(args.admin_remove) 333 | elif args.output_show: 334 | console_show() 335 | elif args.output_hide: 336 | console_hide() 337 | elif args.startup_enable: 338 | startup_enable() 339 | elif args.startup_disable: 340 | startup_disable() 341 | else: 342 | return False 343 | return True 344 | 345 | 346 | def main() -> None: 347 | db_and_co() 348 | if parse_args() == False: 349 | ui() 350 | 351 | 352 | if __name__ == "__main__": 353 | main() 354 | -------------------------------------------------------------------------------- /bot/db.py: -------------------------------------------------------------------------------- 1 | import array 2 | import platform 3 | from datetime import datetime 4 | from functools import wraps 5 | from pathlib import Path 6 | import os 7 | from typing import Callable, Optional 8 | 9 | import pytz 10 | from telegram import ParseMode, Update, User, Bot 11 | from telegram.ext import CallbackContext 12 | from telegram.utils import helpers 13 | from tzlocal import get_localzone 14 | 15 | import utils 16 | 17 | from sqlalchemy.ext.declarative import declarative_base 18 | from sqlalchemy.orm import sessionmaker 19 | from sqlalchemy import create_engine, Column, Integer, Text 20 | 21 | 22 | def database() -> str: 23 | return os.path.join(os.path.dirname(utils.current_path()), "data/pccontrol.sqlite") 24 | 25 | 26 | engine = create_engine("sqlite:///" + database(), echo=False) 27 | Base = declarative_base() 28 | 29 | Base.metadata.bind = engine 30 | DBsession = sessionmaker(bind=engine) 31 | 32 | 33 | class Config(Base): 34 | __tablename__ = "config" 35 | id = Column(Integer, primary_key=True, unique=True) 36 | name = Column(Text, nullable=False, unique=True) 37 | value = Column(Text, nullable=False) 38 | 39 | 40 | class Users(Base): 41 | __tablename__ = "users" 42 | id = Column(Integer, primary_key=True, unique=True) 43 | name_first = Column(Text) 44 | name_last = Column(Text) 45 | username = Column(Text) 46 | privs = Column(Text) 47 | last_use = Column(Text) 48 | time_used = Column(Integer) 49 | 50 | 51 | def exists() -> bool: 52 | return Path(database()).exists() 53 | 54 | 55 | def create() -> None: 56 | if exists() is False: 57 | Base.metadata.create_all(engine) 58 | console_set("hide") 59 | startup_set("false") 60 | 61 | 62 | def get_admins_id() -> array.array: 63 | admins_arr = array.array('i') 64 | session = DBsession() 65 | admins = session.query(Users).filter(Users.privs == "-2").all() 66 | for admin in admins: 67 | admins_arr.append(admin.id) 68 | session.commit() 69 | return admins_arr 70 | 71 | 72 | def update_user(from_user: User, bot: Bot) -> None: # Update the user list (db) 73 | session = DBsession() 74 | user = session.query(Users).filter(Users.id == from_user.id).one_or_none() 75 | used = 0 76 | if user: 77 | if user.time_used: 78 | used = user.time_used 79 | if user: 80 | user.name_first = from_user.first_name 81 | user.name_last = from_user.last_name 82 | user.username = from_user.username 83 | user.last_use = datetime.now(pytz.timezone(str(get_localzone()))).strftime('%Y-%m-%d %H:%M') 84 | user.time_used = used + 1 85 | else: 86 | if session.query(Users).count() == 0: 87 | new_user = Users(name_first=from_user.first_name, 88 | name_last=from_user.last_name, 89 | username=from_user.username, 90 | last_use=datetime.now(pytz.timezone(str(get_localzone()))).strftime('%Y-%m-%d %H:%M'), 91 | time_used=used + 1, 92 | id=from_user.id, 93 | privs="-2") 94 | else: 95 | new_user = Users(name_first=from_user.first_name, 96 | name_last=from_user.last_name, 97 | username=from_user.username, 98 | last_use=datetime.now(pytz.timezone(str(get_localzone()))).strftime('%Y-%m-%d %H:%M'), 99 | time_used=used + 1, 100 | id=from_user.id) 101 | session.add(new_user) 102 | admins = get_admins_id() 103 | for admin_id in admins: 104 | if admin_id != from_user.id: 105 | text = "*New user registered into the database* \n\n" 106 | text += f"Name: {helpers.escape_markdown(from_user.first_name, 2)}" 107 | if from_user.last_name: 108 | text += f"\nLast name: {helpers.escape_markdown(from_user.last_name, 2)}" 109 | if from_user.username: 110 | text += f"\nUsername: @{helpers.escape_markdown(from_user.username, 2)}" 111 | bot.sendMessage( 112 | chat_id=admin_id, text=text, parse_mode=ParseMode.MARKDOWN_V2) 113 | session.commit() 114 | 115 | 116 | def admin_check(func: Callable) -> Callable[[Update, CallbackContext], Optional[Callable]]: 117 | @wraps(func) 118 | def is_admin(update: Update, context: CallbackContext) -> Optional[Callable]: 119 | session = DBsession() 120 | privs = session.query(Users).filter(Users.id == update.message.from_user.id).one_or_none().privs 121 | if privs == "-2": 122 | return func(update, context) 123 | else: 124 | update.effective_message.reply_text("Unauthorized") 125 | return is_admin 126 | 127 | 128 | def token_set(token_type: str, token_value: str) -> None: 129 | session = DBsession() 130 | if token_get(token_type): 131 | session.query(Config).filter(Config.name == token_type).one().value = token_value 132 | else: 133 | token = Config(name=token_type, value=token_value) 134 | session.add(token) 135 | session.commit() 136 | 137 | 138 | def token_get(token_type: str) -> Optional[str]: 139 | session = DBsession() 140 | entry = session.query(Config).filter(Config.name == token_type).one_or_none() 141 | if entry: 142 | return entry.value 143 | 144 | 145 | def user_exists(user: Optional[str] = None) -> bool: 146 | session = DBsession() 147 | if session.query(Users).filter(Users.username == user).one_or_none(): 148 | return True 149 | else: 150 | return False 151 | 152 | 153 | def user_role(user: str, admin: bool) -> None: 154 | session = DBsession() 155 | db_user = session.query(Users).filter(Users.username == user).one() 156 | if admin: 157 | db_user.privs = "-2" 158 | else: 159 | db_user.privs = "" 160 | session.commit() 161 | 162 | 163 | def console_get() -> Optional[str]: 164 | session = DBsession() 165 | entry = session.query(Config).filter(Config.name == "console").one_or_none() 166 | if entry: 167 | return entry.value 168 | 169 | 170 | def console_set(value: str) -> None: 171 | session = DBsession() 172 | if console_get(): 173 | session.query(Config).filter(Config.name == "console").one().value = value 174 | else: 175 | console_value = Config(name="console", value=value) 176 | session.add(console_value) 177 | session.commit() 178 | 179 | 180 | def startup_set(value: str) -> None: 181 | session = DBsession() 182 | if startup_get(): 183 | session.query(Config).filter(Config.name == "startup").one().value = value 184 | else: 185 | startup_value = Config(name="startup", value=value) 186 | session.add(startup_value) 187 | session.commit() 188 | 189 | 190 | def startup_get() -> Optional[str]: 191 | session = DBsession() 192 | entry = session.query(Config).filter(Config.name == "startup").one_or_none() 193 | if entry: 194 | return entry.value 195 | -------------------------------------------------------------------------------- /bot/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import threading 3 | from typing import Callable, Optional, Any 4 | 5 | 6 | class ThreadTimer: 7 | thread = None 8 | 9 | def start(self, fun: Callable, time: int, *args: Any) -> Optional[str]: 10 | if self.thread and self.thread.is_alive(): 11 | return self.thread.name 12 | ThreadTimer.thread = threading.Timer(time*60, fun, [*args]) 13 | ThreadTimer.thread.name = f"{fun.__name__}_t" 14 | ThreadTimer.thread.start() 15 | 16 | def stop(self) -> Optional[str]: 17 | if self.thread and self.thread.is_alive(): 18 | self.thread.cancel() 19 | return self.thread.name 20 | 21 | 22 | def current_path() -> str: 23 | return os.path.abspath(os.path.dirname(__file__)) 24 | 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distro==1.6.0 2 | Pillow==10.3.0 3 | psutil==5.9.0 4 | pyscreenshot==3.0 5 | python-telegram-bot==13.10 6 | pytz==2021.3 7 | SQLAlchemy==1.4.31 8 | tzlocal==4.1 9 | --------------------------------------------------------------------------------