diff --git a/Dockerfile b/Dockerfile index c47794a..4f4a404 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,36 @@ FROM ubuntu:24.10 LABEL maintainer="tomer.klein@gmail.com" -RUN apt update -yqq && \ - apt install -yqq python3-pip && \ - apt install -yqq libffi-dev && \ - apt install -yqq libssl-dev && \ - apt install -yqq curl && \ - apt install -yqq speedtest-cli && \ - apt install -yqq wget +# Install required system dependencies +RUN apt update -yqq && \ + apt install -yqq python3 \ + python3-pip \ + curl \ + wget \ + speedtest-cli \ + --no-install-recommends && \ + apt clean && \ + rm -rf /var/lib/apt/lists/* + +# Set environment variables ENV API_KEY "" +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 -COPY requirements.txt /tmp - - -RUN pip3 install --upgrade pip --no-cache-dir && \ - pip3 install --upgrade setuptools --no-cache-dir - -RUN pip3 install -r /tmp/requirements.txt - -RUN wget https://raw.githubusercontent.com/sivel/speedtest-cli/v2.1.3/speedtest.py -O /usr/lib/python3/dist-packages/speedtest.py - -RUN mkdir /opt/dockerbot +# Create working directory +WORKDIR /opt/dockerbot -COPY dockerbot.py /opt/dockerbot +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip3 install --no-cache-dir --upgrade pip && \ + pip3 install --no-cache-dir -r requirements.txt +# Install speedtest-cli script +RUN wget https://raw.githubusercontent.com/sivel/speedtest-cli/v2.1.3/speedtest.py -O /usr/local/lib/python3.12/site-packages/speedtest.py +# Copy application code +COPY dockerbot.py . -ENTRYPOINT ["/usr/bin/python3", "/opt/dockerbot/dockerbot.py"] +# Run the application +CMD ["python3", "dockerbot.py"] diff --git a/dockerbot.py b/dockerbot.py old mode 100644 new mode 100755 index 24e9e99..7f1aac5 --- a/dockerbot.py +++ b/dockerbot.py @@ -2,135 +2,273 @@ import re import random import datetime -import telepot +import logging from subprocess import call import subprocess import os import sys import docker -from telepot.loop import MessageLoop +import telegram +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters +# Configure logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) +# Get allowed IDs from environment variable +ALLOWED_IDS = [uid.strip() for uid in os.getenv('ALLOWED_IDS', '').split(',') if uid.strip()] if os.getenv('ALLOWED_IDS') else [] def getCommandHelp(line): if "#[" in line: - start = line.find("#[") + len("]#") + start = line.find("#[") + 2 end = line.find("]#") return line[start:end] return "" - -#Auto Commmand List +# Auto Command List def search_string_in_file(file_name, string_to_search): - """Search for the given string in file and return lines containing that string, - along with line numbers""" list_of_results = [] - # Open the file in read only mode with open(file_name, 'r') as read_obj: - # Read all lines in the file one by one for line in read_obj: - # For each line, check if line contains the string if string_to_search in line: if ("/?" not in line and not "o/" in line and not "," in line): command = line - number = command.rfind("/") - command = command[number:] - number = command.rfind("'") - command = command[:number] - command = command + " " + getCommandHelp(line) +"\n" - # If yes, then add the line number & line as a tuple in the list - list_of_results.append(command) - # Return list of tuples containing line numbers and lines where string is found + number = command.rfind("def ") + if number != -1: + command = command[number+4:] + space_pos = command.find("(") + if space_pos != -1: + command_name = command[:space_pos] + help_text = getCommandHelp(line) + if help_text: + command = f"/{command_name} {help_text}\n" + list_of_results.append(command) return list_of_results -def handle(msg): - chat_id = msg['chat']['id'] - command = msg['text'] - - if str(chat_id) not in os.getenv('ALLOWED_IDS'): - bot.sendPhoto(chat_id,"https://github.com/t0mer/dockerbot/raw/master/No-Trespassing.gif") - return "" - - - - print ('Got command: %s')%command - if command == '/time': #[ Get Local Time ]# - bot.sendMessage(chat_id, str(datetime.datetime.now())) - elif command == '/speed': #[ Run Speedtest ]# - x = subprocess.check_output(['speedtest-cli','--share']) - photo = re.search("(?Phttps?://[^\s]+)", x).group("url") - bot.sendPhoto(chat_id,photo) - elif command == '/ip': #[ Get Real IP ]# - x = subprocess.check_output(['curl','ipinfo.io/ip']) - bot.sendMessage(chat_id,x) - elif command == '/disk': #[ Get Disk Space ]# - x = subprocess.check_output(['df', '-h']) - bot.sendMessage(chat_id,x) - elif command == '/mem': #[ Get Memory ]# - x = subprocess.check_output(['cat','/proc/meminfo']) - bot.sendMessage(chat_id,x) - elif command == '/stat': #[ Get bot Status ]# - bot.sendMessage(chat_id,'Number five is alive!') - elif command == '/list_containers': - try: - client = client = docker.from_env() - containers = client.containers.list(all=True) - for f in containers: - bot.sendMessage(chat_id,str(f.name) + " : " + str(f.status) + "\n( /" +f.name +"_restart \n /" +f.name +"_stop \n /" +f.name +"_start )") - except Exception as e: - x = str(e) - bot.sendMessage(chat_id,x) - elif '_restart' in command: - try: - commands = command.split('_') - commands[0] = commands[0].replace('/','') - client = client = docker.from_env() - containers = client.containers.list(all=True, filters={'name':commands[0]}) - bot.sendMessage(chat_id,'Restarting ' + commands[0]) - containers[0].restart() - time.sleep(2) - bot.sendMessage(chat_id, commands[0] + ' is: ' + containers[0].status) - except Exception as e: - x = str(e) - bot.sendMessage(chat_id,x) - elif '_stop' in command: - try: - commands = command.split('_') - commands[0] = commands[0].replace('/','') - client = client = docker.from_env() - containers = client.containers.list(all=True, filters={'name':commands[0]}) - bot.sendMessage(chat_id,'Stopping ' + commands[0]) - containers[0].stop() - time.sleep(2) - bot.sendMessage(chat_id, commands[0] + ' is: ' + containers[0].status) - except Exception as e: - x = str(e) - bot.sendMessage(chat_id,x) - elif '_start' in command: - try: - commands = command.split('_') - commands[0] = commands[0].replace('/','') - client = client = docker.from_env() - containers = client.containers.list(all=True, filters={'name':commands[0]}) - bot.sendMessage(chat_id,'Starting ' + commands[0]) - containers[0].start() - time.sleep(2) - bot.sendMessage(chat_id, commands[0] + ' is: ' + containers[0].status) - except Exception as e: - x = str(e) - bot.sendMessage(chat_id,x) - elif command == '/?' or command=="/start": - array = search_string_in_file('/opt/dockerbot/dockerbot.py', "/") - s = "Command List:\n" - for val in array: - if ")" not in val: - s+=str(val) - x = s - bot.sendMessage(chat_id,x) - -bot = telepot.Bot(os.getenv('API_KEY')) -MessageLoop(bot, handle).run_as_thread() -print('I am listening ...') - -while 1: - time.sleep(10) +# Check if user is authorized +def is_authorized(update, context): + user_id = str(update.message.from_user.id) + if user_id not in ALLOWED_IDS: + context.bot.send_photo( + chat_id=update.message.chat_id, + photo="https://github.com/t0mer/dockerbot/raw/master/No-Trespassing.gif" + ) + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return False + return True + +# Command handlers +def start(update, context): + if not is_authorized(update, context): + return + command_list = search_string_in_file(os.path.abspath(__file__), "def ") + message = "Command List:\n" + for cmd in command_list: + message += cmd + context.bot.send_message(chat_id=update.message.chat_id, text=message) + +def help_command(update, context): + start(update, context) + +def time_command(update, context): #[ Get Local Time ]# + if not is_authorized(update, context): + return + context.bot.send_message( + chat_id=update.message.chat_id, + text=str(datetime.datetime.now()) + ) + +def speed_command(update, context): #[ Run Speedtest ]# + if not is_authorized(update, context): + return + context.bot.send_message( + chat_id=update.message.chat_id, + text="Running speed test... Please wait." + ) + try: + result = subprocess.check_output(['speedtest-cli', '--share']) + result_str = result.decode('utf-8') + photo_match = re.search(r"(?Phttps?://[^\s]+)", result_str) + if photo_match: + photo_url = photo_match.group("url") + context.bot.send_photo( + chat_id=update.message.chat_id, + photo=photo_url + ) + else: + context.bot.send_message( + chat_id=update.message.chat_id, + text=result_str + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error running speed test: {str(e)}" + ) + +def ip_command(update, context): #[ Get Real IP ]# + if not is_authorized(update, context): + return + try: + result = subprocess.check_output(['curl', 'ipinfo.io/ip']) + context.bot.send_message( + chat_id=update.message.chat_id, + text=result.decode('utf-8') + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error getting IP: {str(e)}" + ) + +def disk_command(update, context): #[ Get Disk Space ]# + if not is_authorized(update, context): + return + try: + result = subprocess.check_output(['df', '-h']) + context.bot.send_message( + chat_id=update.message.chat_id, + text=result.decode('utf-8') + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error getting disk space: {str(e)}" + ) + +def mem_command(update, context): #[ Get Memory ]# + if not is_authorized(update, context): + return + try: + result = subprocess.check_output(['cat', '/proc/meminfo']) + context.bot.send_message( + chat_id=update.message.chat_id, + text=result.decode('utf-8') + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error getting memory info: {str(e)}" + ) + +def stat_command(update, context): #[ Get bot Status ]# + if not is_authorized(update, context): + return + context.bot.send_message( + chat_id=update.message.chat_id, + text='Number five is alive!' + ) + +def list_containers_command(update, context): #[ List all Docker containers ]# + if not is_authorized(update, context): + return + try: + client = docker.from_env() + containers = client.containers.list(all=True) + if not containers: + context.bot.send_message( + chat_id=update.message.chat_id, + text="No containers found." + ) + return + for container in containers: + message = (f"{container.name}: {container.status}\n" + f"( /{container.name}_restart \n" + f" /{container.name}_stop \n" + f" /{container.name}_start )") + context.bot.send_message( + chat_id=update.message.chat_id, + text=message + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error listing containers: {str(e)}" + ) + +def container_command(update, context): + if not is_authorized(update, context): + return + command = update.message.text + operation = None + if command.endswith("_restart"): + operation = "restart" + elif command.endswith("_stop"): + operation = "stop" + elif command.endswith("_start"): + operation = "start" + else: + return + container_name = command.lstrip('/').rsplit(f'_{operation}', 1)[0] + try: + client = docker.from_env() + containers = client.containers.list(all=True, filters={'name': f'^/{container_name}$'}) + if not containers: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Container '{container_name}' not found." + ) + return + container = containers[0] + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"{operation.capitalize()}ing {container_name}..." + ) + if operation == "restart": + container.restart() + elif operation == "stop": + container.stop() + elif operation == "start": + container.start() + time.sleep(2) + container.reload() + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"{container_name} is: {container.status}" + ) + except Exception as e: + context.bot.send_message( + chat_id=update.message.chat_id, + text=f"Error {operation}ing container: {str(e)}" + ) + +# Error handler +def error_handler(update, context): + logger.error(f"Update {update} caused error {context.error}") + if update and update.message: + context.bot.send_message( + chat_id=update.message.chat_id, + text="An error occurred while processing your request." + ) + +def main(): + api_key = os.getenv('API_KEY') + if not api_key: + sys.exit("API_KEY environment variable is not set") + updater = Updater(api_key) + dp = updater.dispatcher + + dp.add_handler(CommandHandler("start", start)) + dp.add_handler(CommandHandler("help", help_command)) + dp.add_handler(CommandHandler("time", time_command)) + dp.add_handler(CommandHandler("speed", speed_command)) + dp.add_handler(CommandHandler("ip", ip_command)) + dp.add_handler(CommandHandler("disk", disk_command)) + dp.add_handler(CommandHandler("mem", mem_command)) + dp.add_handler(CommandHandler("stat", stat_command)) + dp.add_handler(CommandHandler("list_containers", list_containers_command)) + + dp.add_handler(MessageHandler(Filters.regex(r"^/[a-zA-Z0-9_-]+_(restart|stop|start)$"), container_command)) + + dp.add_error_handler(error_handler) + + logger.info("Starting bot...") + updater.start_polling() + logger.info("Bot started successfully!") + updater.idle() + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index 0219ad1..771a53f --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ docker -telepot +python-telegram-bot==13.15 aiohttp>=3.9.0 # not directly required, pinned by Snyk to avoid a vulnerability urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability -requests>=2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file +requests>=2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability