From 8711a88684f106870ec9ceb900597726cc2aa3aa Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:12:29 +0000 Subject: [PATCH 1/9] fix: replace telepot with python-telegram-bot==13.15 in requirements --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 requirements.txt 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 From 18168ca094e9911b0b7009c49fa3ad4317b3ddfb Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:09 +0000 Subject: [PATCH 2/9] fix: migrate handlers to python-telegram-bot v13 API (update, context) and replace RegexHandler --- dockerbot.py | 352 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 243 insertions(+), 109 deletions(-) mode change 100644 => 100755 dockerbot.py diff --git a/dockerbot.py b/dockerbot.py old mode 100644 new mode 100755 index 24e9e99..f1992e8 --- a/dockerbot.py +++ b/dockerbot.py @@ -2,135 +2,269 @@ 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 = os.getenv('ALLOWED_IDS', '').split(',') 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) + 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('/opt/dockerbot/dockerbot.py', "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 "_restart" in command: + operation = "restart" + elif "_stop" in command: + operation = "stop" + elif "_start" in command: + operation = "start" + else: + return + container_name = command.split('_')[0].replace('/', '') + try: + client = docker.from_env() + containers = client.containers.list(all=True, filters={'name': 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: + context.bot.send_message( + chat_id=update.message.chat_id, + text="An error occurred while processing your request." + ) + +def main(): + updater = Updater(os.getenv('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() From 6349921953f10c6096924c1b09eb3cb54ac2ba8e Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:18 +0000 Subject: [PATCH 3/9] fix: strip whitespace from ALLOWED_IDS entries to handle space-padded values --- dockerbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerbot.py b/dockerbot.py index f1992e8..79e31f2 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) # Get allowed IDs from environment variable -ALLOWED_IDS = os.getenv('ALLOWED_IDS', '').split(',') if os.getenv('ALLOWED_IDS') else [] +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: From 511fd7b819812572f324fd97b1063c27555a90f2 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:28 +0000 Subject: [PATCH 4/9] fix: validate API_KEY at startup and exit with clear message if unset --- dockerbot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dockerbot.py b/dockerbot.py index 79e31f2..ef120f9 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -244,7 +244,10 @@ def error_handler(update, context): ) def main(): - updater = Updater(os.getenv('API_KEY')) + 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)) From 2e258784b1a633c1f9b3c7f047d2f248fe54c365 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:37 +0000 Subject: [PATCH 5/9] fix: parse container name with rsplit to handle underscore-containing names correctly --- dockerbot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dockerbot.py b/dockerbot.py index ef120f9..78adafe 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -193,15 +193,15 @@ def container_command(update, context): return command = update.message.text operation = None - if "_restart" in command: + if command.endswith("_restart"): operation = "restart" - elif "_stop" in command: + elif command.endswith("_stop"): operation = "stop" - elif "_start" in command: + elif command.endswith("_start"): operation = "start" else: return - container_name = command.split('_')[0].replace('/', '') + container_name = command.lstrip('/').rsplit(f'_{operation}', 1)[0] try: client = docker.from_env() containers = client.containers.list(all=True, filters={'name': container_name}) From 0f2196baa62915eee1f456b77170fb8cad9d46e9 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:45 +0000 Subject: [PATCH 6/9] fix: use anchored regex filter for exact container name match to prevent substring collisions --- dockerbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerbot.py b/dockerbot.py index 78adafe..c40f237 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -204,7 +204,7 @@ def container_command(update, context): container_name = command.lstrip('/').rsplit(f'_{operation}', 1)[0] try: client = docker.from_env() - containers = client.containers.list(all=True, filters={'name': container_name}) + containers = client.containers.list(all=True, filters={'name': f'^/{container_name}$'}) if not containers: context.bot.send_message( chat_id=update.message.chat_id, From aa876606190e701e0e35a60833c1c306af64fbd0 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:13:54 +0000 Subject: [PATCH 7/9] fix: guard update.message against None in error_handler for non-message updates --- dockerbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockerbot.py b/dockerbot.py index c40f237..1acd11f 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -237,7 +237,7 @@ def container_command(update, context): # Error handler def error_handler(update, context): logger.error(f"Update {update} caused error {context.error}") - if update: + if update and update.message: context.bot.send_message( chat_id=update.message.chat_id, text="An error occurred while processing your request." From 3a5daf1ee80a67d12907ae740d3adefd16a21cdd Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:14:08 +0000 Subject: [PATCH 8/9] fix: use __file__ for source path in /start; only list functions with help markers --- dockerbot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dockerbot.py b/dockerbot.py index 1acd11f..7f1aac5 100755 --- a/dockerbot.py +++ b/dockerbot.py @@ -43,8 +43,9 @@ def search_string_in_file(file_name, string_to_search): if space_pos != -1: command_name = command[:space_pos] help_text = getCommandHelp(line) - command = f"/{command_name} {help_text}\n" - list_of_results.append(command) + if help_text: + command = f"/{command_name} {help_text}\n" + list_of_results.append(command) return list_of_results # Check if user is authorized @@ -63,7 +64,7 @@ def is_authorized(update, context): def start(update, context): if not is_authorized(update, context): return - command_list = search_string_in_file('/opt/dockerbot/dockerbot.py', "def ") + command_list = search_string_in_file(os.path.abspath(__file__), "def ") message = "Command List:\n" for cmd in command_list: message += cmd From cc4944a22fd01b5d97f98c37d94c785fc64cda61 Mon Sep 17 00:00:00 2001 From: Tomer Klein Date: Mon, 1 Jun 2026 23:14:24 +0000 Subject: [PATCH 9/9] fix: install python3 and python3-pip on ubuntu:24.10; use python3 in CMD --- Dockerfile | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) 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"]