Upload files to "/"

This commit is contained in:
2026-03-28 20:34:09 +00:00
parent a110ef780c
commit aa7be68aff

420
main.py
View File

@@ -2,22 +2,26 @@ import discord
import docker import docker
import os import os
import json import json
import asyncio
from datetime import datetime, timezone
from discord.ext import commands, tasks from discord.ext import commands, tasks
from discord import app_commands
from dotenv import load_dotenv from dotenv import load_dotenv
from aiohttp import web
load_dotenv() load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
DOCKER_HOST = os.getenv("DOCKER_HOST") DOCKER_HOST = os.getenv("DOCKER_HOST")
GUILD_ID = int(os.getenv("GUILD_ID")) # Your server ID GUILD_ID = int(os.getenv("GUILD_ID"))
CATEGORY_NAME = os.getenv("CATEGORY_NAME") # Category name for the channels CATEGORY_NAME = os.getenv("CATEGORY_NAME", "Docker Status")
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents) bot = commands.Bot(command_prefix="!", intents=intents)
# Stores groups: { "groupname": { "channel_id": 123, "containers": ["nginx", "mysql"], "message_id": 456 } }
GROUPS_FILE = "groups.json" GROUPS_FILE = "groups.json"
SETTINGS_FILE = "settings.json"
def load_groups(): def load_groups():
if os.path.exists(GROUPS_FILE): if os.path.exists(GROUPS_FILE):
@@ -29,77 +33,216 @@ def save_groups(groups):
with open(GROUPS_FILE, "w") as f: with open(GROUPS_FILE, "w") as f:
json.dump(groups, f, indent=2) json.dump(groups, f, indent=2)
def load_settings():
if os.path.exists(SETTINGS_FILE):
with open(SETTINGS_FILE, "r") as f:
return json.load(f)
return {
"alert_mode": None,
"alert_channel_id": None,
"setup_done": False
}
def save_settings(settings):
with open(SETTINGS_FILE, "w") as f:
json.dump(settings, f, indent=2)
def status_symbol(status): def status_symbol(status):
symbols = { return {
"running": "🟢", "running": "🟢",
"exited": "🔴", "exited": "🔴",
"paused": "🟡", "paused": "🟡",
"created": "", "created": "",
} }.get(status, "")
return symbols.get(status, "")
# --- Commands --- def get_docker_client():
return docker.DockerClient(base_url=DOCKER_HOST, timeout=5)
@bot.command() def get_all_containers():
async def container_list(ctx): client = get_docker_client()
"""Shows all available containers on the host""" return client.containers.list(all=True)
def format_uptime(started_at: str) -> str:
try: try:
client = docker.DockerClient(base_url=DOCKER_HOST, timeout=5) started = datetime.fromisoformat(started_at[:26].replace("Z", "+00:00"))
container_list = client.containers.list(all=True) now = datetime.now(timezone.utc)
delta = now - started
days = delta.days
hours, remainder = divmod(delta.seconds, 3600)
minutes, _ = divmod(remainder, 60)
if days > 0:
return f"{days}d {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
except Exception:
return "unknown"
message = "**🐳 Available Containers:**\n```\n" def build_status_embed(group_name: str, containers: list, all_containers: dict, container_objects: list) -> discord.Embed:
for c in container_list: embed = discord.Embed(
message += f"{status_symbol(c.status)} {c.name}\n" title=f"🐳 {group_name.upper()}",
message += "```\nUse `!group_create <name> <container1> <container2> ...` to create a group." color=discord.Color.blurple(),
timestamp=datetime.now(timezone.utc)
)
container_map = {c.name: c for c in container_objects}
for name in containers:
if name in all_containers:
status = all_containers[name]
uptime = ""
if status == "running" and name in container_map:
try:
started_at = container_map[name].attrs["State"]["StartedAt"]
uptime = f"\nUptime: `{format_uptime(started_at)}`"
except Exception:
pass
embed.add_field(
name=f"{status_symbol(status)} {name}",
value=f"`{status}`{uptime}",
inline=False
)
else:
embed.add_field(
name=f"{name}",
value="`not found`",
inline=False
)
embed.set_footer(text="Last updated")
return embed
await ctx.send(message) async def health_handler(request):
return web.Response(text="OK")
async def start_health_server():
app = web.Application()
app.router.add_get("/health", health_handler)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", 8080)
await site.start()
print("Health check running on :8080/health")
async def container_autocomplete(interaction: discord.Interaction, current: str):
try:
containers = get_all_containers()
names = [c.name for c in containers if current.lower() in c.name.lower()]
return [app_commands.Choice(name=n, value=n) for n in names[:25]]
except Exception:
return []
async def group_autocomplete(interaction: discord.Interaction, current: str):
groups = load_groups()
return [
app_commands.Choice(name=n, value=n)
for n in groups if current.lower() in n.lower()
][:25]
@bot.tree.command(name="setup", description="Setup the bot (alert preferences etc.)")
@app_commands.describe(
alert_mode="Where should crash alerts be sent?",
alert_channel="Channel for alerts (only needed if alert_mode is 'channel')"
)
@app_commands.choices(alert_mode=[
app_commands.Choice(name="In each group channel", value="group"),
app_commands.Choice(name="In a separate alert channel", value="channel"),
])
async def setup(interaction: discord.Interaction, alert_mode: app_commands.Choice[str], alert_channel: discord.TextChannel = None):
settings = load_settings()
if alert_mode.value == "channel" and alert_channel is None:
await interaction.response.send_message(
"❌ Please also select an alert channel when using 'separate alert channel' mode.",
ephemeral=True
)
return
settings["alert_mode"] = alert_mode.value
settings["alert_channel_id"] = alert_channel.id if alert_channel else None
settings["setup_done"] = True
save_settings(settings)
embed = discord.Embed(title="✅ Setup Complete", color=discord.Color.green())
embed.add_field(name="Alert Mode", value=alert_mode.name, inline=False)
if alert_channel:
embed.add_field(name="Alert Channel", value=alert_channel.mention, inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
@bot.tree.command(name="container_list", description="Lists all containers on the Docker host")
async def container_list(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
try:
containers = get_all_containers()
embed = discord.Embed(title="🐳 Available Containers", color=discord.Color.blurple())
for c in containers:
try:
started_at = c.attrs["State"]["StartedAt"]
uptime = f"\nUptime: `{format_uptime(started_at)}`" if c.status == "running" else ""
except Exception:
uptime = ""
embed.add_field(
name=f"{status_symbol(c.status)} {c.name}",
value=f"`{c.status}`{uptime}",
inline=False
)
embed.set_footer(text=f"{len(containers)} containers found")
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e: except Exception as e:
await ctx.send(f"❌ Error: {e}") await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
@bot.command()
async def group_create(ctx, group_name: str, *container_names): @bot.tree.command(name="group_create", description="Creates a container group with a dedicated channel")
"""Creates a group and a channel for it @app_commands.describe(
Example: !group_create webservices nginx apache""" group_name="Name of the group",
containers="Container names separated by spaces (e.g. nginx mysql redis)"
)
async def group_create(interaction: discord.Interaction, group_name: str, containers: str):
await interaction.response.defer(ephemeral=True)
container_names = containers.split()
if not container_names: if not container_names:
await ctx.send("❌ Please specify at least one container!\nExample: `!group_create webservices nginx apache`") await interaction.followup.send("❌ Please specify at least one container!", ephemeral=True)
return return
guild = bot.get_guild(GUILD_ID) guild = bot.get_guild(GUILD_ID)
groups = load_groups() groups = load_groups()
# Create category if it doesn't exist
category = discord.utils.get(guild.categories, name=CATEGORY_NAME) category = discord.utils.get(guild.categories, name=CATEGORY_NAME)
if not category: if not category:
category = await guild.create_category(CATEGORY_NAME) category = await guild.create_category(CATEGORY_NAME)
# Create channel
channel_name = f"🐳{group_name.lower()}" channel_name = f"🐳{group_name.lower()}"
channel = discord.utils.get(category.channels, name=channel_name) channel = discord.utils.get(category.channels, name=channel_name)
if not channel: if not channel:
# Only bot is allowed to write
overwrites = { overwrites = {
guild.default_role: discord.PermissionOverwrite(send_messages=False), guild.default_role: discord.PermissionOverwrite(send_messages=False),
guild.me: discord.PermissionOverwrite(send_messages=True) guild.me: discord.PermissionOverwrite(send_messages=True)
} }
channel = await category.create_text_channel(channel_name, overwrites=overwrites) channel = await category.create_text_channel(channel_name, overwrites=overwrites)
# Save group
groups[group_name] = { groups[group_name] = {
"channel_id": channel.id, "channel_id": channel.id,
"containers": list(container_names), "containers": container_names,
"message_id": None "message_id": None
} }
save_groups(groups) save_groups(groups)
await ctx.send(f"✅ Group **{group_name}** created with channel {channel.mention}\nContainers: `{', '.join(container_names)}`") embed = discord.Embed(title="✅ Group Created", color=discord.Color.green())
embed.add_field(name="Group", value=group_name, inline=True)
embed.add_field(name="Channel", value=channel.mention, inline=True)
embed.add_field(name="Containers", value="`" + ", ".join(container_names) + "`", inline=False)
await interaction.followup.send(embed=embed, ephemeral=True)
@bot.command()
async def group_remove(ctx, group_name: str): @bot.tree.command(name="group_remove", description="Deletes a group and its channel")
"""Deletes a group and its channel""" @app_commands.describe(group_name="Name of the group to delete")
@app_commands.autocomplete(group_name=group_autocomplete)
async def group_remove(interaction: discord.Interaction, group_name: str):
await interaction.response.defer(ephemeral=True)
groups = load_groups() groups = load_groups()
if group_name not in groups: if group_name not in groups:
await ctx.send(f"❌ Group `{group_name}` not found.") await interaction.followup.send(f"❌ Group `{group_name}` not found.", ephemeral=True)
return return
guild = bot.get_guild(GUILD_ID) guild = bot.get_guild(GUILD_ID)
@@ -109,103 +252,202 @@ async def group_remove(ctx, group_name: str):
del groups[group_name] del groups[group_name]
save_groups(groups) save_groups(groups)
await ctx.send(f"✅ Group **{group_name}** deleted.")
@bot.command() embed = discord.Embed(title="✅ Group Deleted", color=discord.Color.red())
async def group_list(ctx): embed.add_field(name="Group", value=group_name)
"""Shows all groups""" await interaction.followup.send(embed=embed, ephemeral=True)
@bot.tree.command(name="group_list", description="Shows all existing groups")
async def group_list(interaction: discord.Interaction):
groups = load_groups() groups = load_groups()
if not groups: if not groups:
await ctx.send("No groups found. Use `!group_create` to create one.") await interaction.response.send_message("No groups found. Use `/group_create` to create one.", ephemeral=True)
return return
message = "**📋 Groups:**\n" embed = discord.Embed(title="📋 Groups", color=discord.Color.blurple())
for name, data in groups.items(): for name, data in groups.items():
message += f"\n**{name}:** `{', '.join(data['containers'])}`" guild = bot.get_guild(GUILD_ID)
await ctx.send(message) channel = guild.get_channel(data["channel_id"])
embed.add_field(
@bot.command() name=f"📁 {name}",
async def info(ctx): value=f"Channel: {channel.mention if channel else '`deleted`'}\nContainers: `{', '.join(data['containers'])}`",
"""Shows all available commands and their usage""" inline=False
message = (
"**🤖 Docker Monitor Bot - Commands**\n\n"
"**📋 Container**\n"
"`!container_list`\n"
"→ Shows all available containers on the host\n\n"
"**📁 Groups**\n"
"`!group_create <name> <container1> <container2> ...`\n"
"→ Creates a group with a dedicated channel for the given containers\n"
"→ Example: `!group_create webservices nginx apache`\n\n"
"`!group_remove <name>`\n"
"→ Deletes a group and its channel\n"
"→ Example: `!group_remove webservices`\n\n"
"`!group_list`\n"
"→ Shows all existing groups and their containers\n\n"
"** Other**\n"
"`!info`\n"
"→ Shows this help message\n\n"
"**🔄 Monitoring**\n"
"Container statuses are automatically updated every **30 seconds** in their group channels.\n"
"🟢 running 🔴 exited 🟡 paused ⚪ created ❓ unknown"
) )
await ctx.send(message) await interaction.response.send_message(embed=embed, ephemeral=True)
# --- Monitoring ---
@bot.tree.command(name="container_start", description="Start a container")
@app_commands.describe(container_name="Name of the container to start")
@app_commands.autocomplete(container_name=container_autocomplete)
async def container_start(interaction: discord.Interaction, container_name: str):
await interaction.response.defer(ephemeral=True)
try:
client = get_docker_client()
container = client.containers.get(container_name)
container.start()
embed = discord.Embed(title="▶️ Container Started", color=discord.Color.green())
embed.add_field(name="Container", value=f"`{container_name}`")
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
@bot.tree.command(name="container_stop", description="Stop a container")
@app_commands.describe(container_name="Name of the container to stop")
@app_commands.autocomplete(container_name=container_autocomplete)
async def container_stop(interaction: discord.Interaction, container_name: str):
await interaction.response.defer(ephemeral=True)
try:
client = get_docker_client()
container = client.containers.get(container_name)
container.stop()
embed = discord.Embed(title="⏹️ Container Stopped", color=discord.Color.red())
embed.add_field(name="Container", value=f"`{container_name}`")
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
@bot.tree.command(name="container_logs", description="Show the last 20 log lines of a container")
@app_commands.describe(container_name="Name of the container")
@app_commands.autocomplete(container_name=container_autocomplete)
async def container_logs(interaction: discord.Interaction, container_name: str):
await interaction.response.defer(ephemeral=True)
try:
client = get_docker_client()
container = client.containers.get(container_name)
logs = container.logs(tail=20).decode("utf-8", errors="replace")
if not logs.strip():
logs = "No logs available."
if len(logs) > 1900:
logs = "..." + logs[-1900:]
embed = discord.Embed(
title=f"📋 Logs: {container_name}",
description=f"```\n{logs}\n```",
color=discord.Color.blurple(),
timestamp=datetime.now(timezone.utc)
)
await interaction.followup.send(embed=embed, ephemeral=True)
except Exception as e:
await interaction.followup.send(f"❌ Error: {e}", ephemeral=True)
@bot.tree.command(name="info", description="Shows all available commands")
async def info(interaction: discord.Interaction):
embed = discord.Embed(title="🤖 Docker Monitor Bot", color=discord.Color.blurple())
embed.add_field(name="/setup", value="Configure alert preferences", inline=False)
embed.add_field(name="/container_list", value="List all containers on the host", inline=False)
embed.add_field(name="/container_start <n>", value="Start a container", inline=False)
embed.add_field(name="/container_stop <n>", value="Stop a container", inline=False)
embed.add_field(name="/container_logs <n>", value="Show last 20 log lines", inline=False)
embed.add_field(name="/group_create <n> <containers>", value="Create a group with a dedicated channel", inline=False)
embed.add_field(name="/group_remove <n>", value="Delete a group and its channel", inline=False)
embed.add_field(name="/group_list", value="Show all existing groups", inline=False)
embed.add_field(name="🔄 Monitoring", value="Status + Uptime updates every **30 seconds**\n🟢 running 🔴 exited 🟡 paused ⚪ created ❓ unknown", inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
previous_statuses = {}
@tasks.loop(seconds=30) @tasks.loop(seconds=30)
async def monitor_containers(): async def monitor_containers():
global previous_statuses
groups = load_groups() groups = load_groups()
settings = load_settings()
if not groups: if not groups:
return return
try: try:
docker_client = docker.DockerClient(base_url=DOCKER_HOST, timeout=5) container_objects = get_all_containers()
all_containers = {c.name: c.status for c in docker_client.containers.list(all=True)} all_containers = {c.name: c.status for c in container_objects}
except Exception: except Exception:
return return
guild = bot.get_guild(GUILD_ID) guild = bot.get_guild(GUILD_ID)
for container_name, status in all_containers.items():
prev = previous_statuses.get(container_name)
if prev == "running" and status == "exited":
await send_crash_alert(guild, settings, container_name, groups)
previous_statuses = dict(all_containers)
running_count = sum(1 for s in all_containers.values() if s == "running")
await bot.change_presence(activity=discord.Activity(
type=discord.ActivityType.watching,
name=f"{running_count} containers"
))
for group_name, data in groups.items(): for group_name, data in groups.items():
channel = guild.get_channel(data["channel_id"]) channel = guild.get_channel(data["channel_id"])
if not channel: if not channel:
continue continue
# Build status text embed = build_status_embed(group_name, data["containers"], all_containers, container_objects)
text = f"**🐳 {group_name.upper()}**\n```\n"
for container_name in data["containers"]:
if container_name in all_containers:
status = all_containers[container_name]
text += f"{status_symbol(status)} {container_name:<25} {status}\n"
else:
text += f"{container_name:<25} not found\n"
text += "```"
try: try:
if data["message_id"]: if data["message_id"]:
# Edit existing message
message = await channel.fetch_message(data["message_id"]) message = await channel.fetch_message(data["message_id"])
await message.edit(content=text) await message.edit(embed=embed)
else: else:
# Send first message message = await channel.send(embed=embed)
message = await channel.send(text)
data["message_id"] = message.id data["message_id"] = message.id
save_groups(groups) save_groups(groups)
except discord.NotFound: except discord.NotFound:
# Message was deleted, resend message = await channel.send(embed=embed)
message = await channel.send(text)
data["message_id"] = message.id data["message_id"] = message.id
save_groups(groups) save_groups(groups)
@bot.event
async def on_command_error(ctx, error): async def send_crash_alert(guild, settings, container_name: str, groups: dict):
if isinstance(error, commands.CommandNotFound): embed = discord.Embed(
return # Ignore unknown commands silently title="🚨 Container Crashed!",
raise error # All other errors still show in logs description=f"Container **{container_name}** has stopped unexpectedly.",
color=discord.Color.red(),
timestamp=datetime.now(timezone.utc)
)
embed.set_footer(text="Docker Monitor Bot")
alert_mode = settings.get("alert_mode")
if alert_mode == "channel":
channel_id = settings.get("alert_channel_id")
if channel_id:
channel = guild.get_channel(channel_id)
if channel:
await channel.send(embed=embed)
elif alert_mode == "group":
for group_name, data in groups.items():
if container_name in data["containers"]:
channel = guild.get_channel(data["channel_id"])
if channel:
await channel.send(embed=embed)
else:
for data in groups.values():
channel = guild.get_channel(data["channel_id"])
if channel:
await channel.send(embed=embed)
break
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f"Bot is online as {bot.user}") print(f"Bot is online as {bot.user}")
try:
guild = discord.Object(id=GUILD_ID)
bot.tree.copy_global_to(guild=guild)
await bot.tree.sync(guild=guild)
print("Slash commands synced.")
except Exception as e:
print(f"Failed to sync commands: {e}")
await start_health_server()
monitor_containers.start() monitor_containers.start()
bot.run(BOT_TOKEN) bot.run(BOT_TOKEN)