wdiscordbotserver/cogs/adminsys.py
pancakes-proxy 334612cc0b fire
2025-05-20 21:35:53 -07:00

999 lines
46 KiB
Python

import discord
from discord.ext import commands
from discord import app_commands
import subprocess
import asyncio
import os
import tempfile
import json
import sys
import shutil
from typing import Optional, List, Tuple
class AdminSysCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
# Support multiple authorized user IDs
self.authorized_user_ids = {452666956353503252, 1146391317295935570, 1141746562922459136} # Add more user IDs as needed
self.log_file = "admin_commands.log"
self.max_message_length = 1990 # Discord's message length limit (leaving some room for formatting)
self.max_medium_content = 50000 # Size threshold for medium content (split into chunks)
self.max_large_content = 10000000 # Size threshold for large content (send as file)
print(f"AdminSysCog initialized. Authorized user IDs: {self.authorized_user_ids}")
async def is_authorized_user(self, interaction: discord.Interaction) -> bool:
"""Check if the user is authorized to use admin commands."""
if interaction.user.id not in self.authorized_user_ids:
await interaction.response.send_message(
"You are not authorized to use this command.",
ephemeral=True
)
return False
return True
async def log_command(self, interaction: discord.Interaction, command_name: str, args: str = "") -> None:
"""Log command usage for security and auditing purposes."""
timestamp = discord.utils.utcnow().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] User: {interaction.user} (ID: {interaction.user.id}) | Command: {command_name} | Args: {args}\n"
try:
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(log_entry)
except Exception as e:
print(f"Error logging admin command: {e}")
async def run_command(self, command: str, use_sudo: bool = False) -> Tuple[str, str, int]:
"""
Run a shell command and return its output.
Args:
command: The command to execute
use_sudo: Whether to p refix the command with sudo
Returns:
Tuple of (stdout, stderr, return_code)
"""
if use_sudo:
command = f"sudo {command}"
try:
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
return (
stdout.decode("utf-8", errors="replace").strip(),
stderr.decode("utf-8", errors="replace").strip(),
process.returncode
)
except Exception as e:
return "", f"Error executing command: {str(e)}", 1
async def handle_large_content(self, content: str, filename: str = "output.txt") -> discord.File:
"""
Handle large content by saving it to a temporary file and returning a Discord File object.
Args:
content: The content to save
filename: The filename to use for the attachment
Returns:
A Discord File object containing the content
"""
try:
with tempfile.NamedTemporaryFile(mode="w+", suffix=".txt", delete=False, encoding="utf-8") as temp_file:
temp_file_path = temp_file.name
temp_file.write(content)
# Create a Discord File object
file = discord.File(temp_file_path, filename=filename)
# Schedule the temporary file for deletion after sending
async def delete_temp_file():
await asyncio.sleep(5) # Wait a bit to ensure the file is sent
try:
os.unlink(temp_file_path)
except Exception:
pass
# Start the deletion task without awaiting it
asyncio.create_task(delete_temp_file())
return file
except Exception as e:
print(f"Error creating temporary file: {e}")
# If we can't create a temp file, try to create it in the current directory
try:
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
return discord.File(filename)
except Exception as e2:
print(f"Error creating fallback file: {e2}")
# If all else fails, raise an error
raise RuntimeError(f"Failed to handle large content: {e}")
async def send_response_with_smaller_chunks(self, interaction: discord.Interaction, content: str, ephemeral: bool = False) -> None:
"""
Alternative response method with smaller chunks when the standard method fails due to Discord's message size limits.
Args:
interaction: The interaction to respond to
content: The content to send
ephemeral: Whether the response should be ephemeral
"""
send_method = interaction.followup.send
# Use a much smaller chunk size to be safe
very_safe_chunk_size = 1500 # Well under Discord's 2000 character limit
chunks = [content[i:i+very_safe_chunk_size] for i in range(0, len(content), very_safe_chunk_size)]
try:
# Send first message with part indicator
await send_method(
f"Output split into {len(chunks)} parts.\nPart 1/{len(chunks)}:```\n{chunks[0]}\n```",
ephemeral=ephemeral
)
# Send remaining chunks
for i, chunk in enumerate(chunks[1:], start=2):
try:
await send_method(
f"Part {i}/{len(chunks)}:```\n{chunk}\n```",
ephemeral=ephemeral
)
except discord.HTTPException as e:
if "400 Bad Request" in str(e) and "Must be 2000 or fewer in length" in str(e):
# If we still hit the limit with very small chunks, split this chunk even further
quarter_size = len(chunk) // 4
for j in range(4):
start_idx = j * quarter_size
end_idx = start_idx + quarter_size if j < 3 else len(chunk)
sub_chunk = chunk[start_idx:end_idx]
if sub_chunk: # Only send if there's content
try:
await send_method(
f"Part {i}/{len(chunks)} (split {j+1}/4):```\n{sub_chunk}\n```",
ephemeral=ephemeral
)
except Exception as e3:
print(f"Error sending quarter chunk: {e3}")
# If even this fails, switch to file method for remaining content
remaining = content[i*very_safe_chunk_size:]
file = await self.handle_large_content(remaining, f"output_remaining.txt")
await send_method(
f"Error sending parts as text. Remaining output attached as file:",
file=file,
ephemeral=ephemeral
)
return
else:
# For other errors, switch to file method
print(f"Error sending chunk {i}: {e}")
remaining = content[(i-1)*very_safe_chunk_size:]
file = await self.handle_large_content(remaining, f"output_parts_{i}_to_{len(chunks)}.txt")
await send_method(
f"Error sending parts as text. Remaining parts {i}-{len(chunks)} attached as file:",
file=file,
ephemeral=ephemeral
)
break
except Exception as e:
# If all else fails, try to send as a file
print(f"Critical error in send_response_with_smaller_chunks: {e}")
try:
file = await self.handle_large_content(content, "output_fallback.txt")
await send_method(
"Error sending output as text. Full content attached as file:",
file=file,
ephemeral=ephemeral
)
except Exception as e2:
print(f"Failed to send file fallback: {e2}")
# Last resort - send minimal error message
try:
await send_method(
"Critical error handling output. Check logs for details.",
ephemeral=ephemeral
)
except:
print("Complete failure in message sending system")
async def send_response(self, interaction: discord.Interaction, content: str, ephemeral: bool = False) -> None:
"""
Send a response, handling large outputs by splitting or sending as a file.
Args:
interaction: The interaction to respond to
content: The content to send
ephemeral: Whether the response should be ephemeral
"""
if not content:
content = "Command executed with no output."
# Make sure we're using followup if the interaction is already deferred
send_method = interaction.followup.send
try:
# Small content: send directly
if len(content) <= self.max_message_length:
await send_method(f"```\n{content}\n```", ephemeral=ephemeral)
return
# Medium content: split into chunks
if len(content) <= self.max_medium_content:
# Reduce chunk size to ensure we stay well under Discord's limit
# when adding the part indicators and code blocks
safe_chunk_size = self.max_message_length - 50 # Leave extra room for formatting
chunks = [content[i:i+safe_chunk_size] for i in range(0, len(content), safe_chunk_size)]
# Send first message with part indicator
try:
await send_method(
f"Output split into {len(chunks)} parts.\nPart 1/{len(chunks)}:```\n{chunks[0]}\n```",
ephemeral=ephemeral
)
except discord.HTTPException as e:
if "400 Bad Request" in str(e) and "Must be 2000 or fewer in length" in str(e):
# If even with our safety margin we hit the limit, reduce chunk size further
print(f"Hit Discord message limit. Reducing chunk size and retrying.")
return await self.send_response_with_smaller_chunks(interaction, content, ephemeral)
else:
raise
# Send remaining chunks
for i, chunk in enumerate(chunks[1:], start=2):
try:
message = f"Part {i}/{len(chunks)}:```\n{chunk}\n```"
await send_method(message, ephemeral=ephemeral)
except discord.HTTPException as e:
if "400 Bad Request" in str(e) and "Must be 2000 or fewer in length" in str(e):
# If we hit the limit, try to send a smaller chunk
print(f"Hit Discord message limit on chunk {i}. Splitting further.")
# Split this chunk in half and send separately
half_size = len(chunk) // 2
try:
await send_method(
f"Part {i}/{len(chunks)} (split 1/2):```\n{chunk[:half_size]}\n```",
ephemeral=ephemeral
)
await send_method(
f"Part {i}/{len(chunks)} (split 2/2):```\n{chunk[half_size:]}\n```",
ephemeral=ephemeral
)
except Exception as e2:
print(f"Error sending split chunk: {e2}")
# If splitting fails, fall back to file method
remaining = "\n".join(chunks[i-1:])
file = await self.handle_large_content(remaining, f"output_parts_{i}_to_{len(chunks)}.txt")
await send_method(
f"Error sending parts as text. Remaining parts {i}-{len(chunks)} attached as file:",
file=file,
ephemeral=ephemeral
)
break
else:
# For other errors, switch to file method
print(f"Error sending chunk {i}: {e}")
remaining = "\n".join(chunks[i-1:])
file = await self.handle_large_content(remaining, f"output_parts_{i}_to_{len(chunks)}.txt")
await send_method(
f"Error sending parts as text. Remaining parts {i}-{len(chunks)} attached as file:",
file=file,
ephemeral=ephemeral
)
break
return
# Large content: send as file
if len(content) <= self.max_large_content:
file = await self.handle_large_content(content, "output.txt")
await send_method(
"Output is too large to display as text. See attached file:",
file=file,
ephemeral=ephemeral
)
return
# Extremely large content: truncate and send as file with warning
truncated_content = content[:self.max_large_content] + "\n\n... CONTENT TRUNCATED (too large) ..."
file = await self.handle_large_content(truncated_content, "truncated_output.txt")
await send_method(
"⚠️ **WARNING**: Output is extremely large and has been truncated!",
file=file,
ephemeral=ephemeral
)
except Exception as e:
# Last resort error handling
error_msg = f"Error handling response: {str(e)}\n\nFirst 1000 characters of output:\n{content[:1000]}"
try:
await send_method(f"```\n{error_msg}\n```", ephemeral=ephemeral)
except:
# If even that fails, try a minimal message
try:
await send_method("Error handling command output. See logs for details.", ephemeral=ephemeral)
except:
# If everything fails, log the error
print(f"Critical error sending response: {e}")
print(f"Content length: {len(content)}")
# Command Groups
pkg_group = app_commands.Group(name="pkg", description="Package management commands")
file_group = app_commands.Group(name="file", description="File operation commands")
sys_group = app_commands.Group(name="sys", description="System administration commands")
service_group = app_commands.Group(name="service", description="Service management commands")
net_group = app_commands.Group(name="net", description="Network-related commands")
# Package Management Commands
@pkg_group.command(name="pip_install", description="Install a Python package using pip")
@app_commands.describe(package="The package name to install")
async def pip_install(self, interaction: discord.Interaction, package: str):
"""Install a Python package using pip."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "pip_install", package)
stdout, stderr, return_code = await self.run_command(f"pip install {package}")
if return_code == 0:
await self.send_response(interaction, f"Successfully installed {package}\n\n{stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to install {package}\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="pip_uninstall", description="Uninstall a Python package using pip")
@app_commands.describe(package="The package name to uninstall")
async def pip_uninstall(self, interaction: discord.Interaction, package: str):
"""Uninstall a Python package using pip."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "pip_uninstall", package)
stdout, stderr, return_code = await self.run_command(f"pip uninstall -y {package}")
if return_code == 0:
await self.send_response(interaction, f"Successfully uninstalled {package}\n\n{stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to uninstall {package}\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="pip_list", description="List installed Python packages")
async def pip_list(self, interaction: discord.Interaction):
"""List installed Python packages."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "pip_list")
stdout, stderr, return_code = await self.run_command("pip list")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to list packages\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="apt_install", description="Install a package using apt")
@app_commands.describe(package="The package name to install")
async def apt_install(self, interaction: discord.Interaction, package: str):
"""Install a package using apt."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "apt_install", package)
stdout, stderr, return_code = await self.run_command(f"apt-get install -y {package}", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, f"Successfully installed {package}\n\n{stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to install {package}\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="apt_remove", description="Remove a package using apt")
@app_commands.describe(package="The package name to remove")
async def apt_remove(self, interaction: discord.Interaction, package: str):
"""Remove a package using apt."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "apt_remove", package)
stdout, stderr, return_code = await self.run_command(f"apt-get remove -y {package}", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, f"Successfully removed {package}\n\n{stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to remove {package}\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="apt_update", description="Update package lists using apt")
async def apt_update(self, interaction: discord.Interaction):
"""Update package lists using apt."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "apt_update")
stdout, stderr, return_code = await self.run_command("apt-get update", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to update package lists\n\n{stderr}", ephemeral=True)
@pkg_group.command(name="apt_upgrade", description="Upgrade packages using apt")
async def apt_upgrade(self, interaction: discord.Interaction):
"""Upgrade packages using apt."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "apt_upgrade")
stdout, stderr, return_code = await self.run_command("apt-get upgrade -y", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to upgrade packages\n\n{stderr}", ephemeral=True)
# File Operation Commands
@file_group.command(name="read", description="Read a file")
@app_commands.describe(
path="The path to the file to read",
as_attachment="Whether to send the file as an attachment instead of text"
)
async def read_file(self, interaction: discord.Interaction, path: str, as_attachment: bool = False):
"""Read a file and display its contents."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "read_file", f"{path} (as_attachment: {as_attachment})")
try:
# Check if file exists
if not os.path.exists(path):
await interaction.followup.send(f"Error: File {path} does not exist.", ephemeral=True)
return
file_size = os.path.getsize(path)
# If file is too large or user requested attachment, send as attachment
if as_attachment or file_size > self.max_medium_content:
# Check if file is too large for Discord (8MB for normal, 50MB for nitro)
if file_size > 8 * 1024 * 1024:
await interaction.followup.send(
f"⚠️ Warning: File {path} is very large ({file_size} bytes). "
f"Discord may reject files larger than 8MB (or 50MB with Nitro).",
ephemeral=True
)
try:
await interaction.followup.send(
f"File {path} ({file_size} bytes):",
file=discord.File(path),
ephemeral=True
)
except discord.HTTPException as e:
if "Request entity too large" in str(e):
await interaction.followup.send(
f"Error: File {path} is too large to upload to Discord ({file_size} bytes). "
f"Maximum file size is 8MB (or 50MB with Nitro).",
ephemeral=True
)
else:
raise
return
# For text files, read and send content
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
# Add file info to the content
header = f"Contents of {path} ({file_size} bytes):\n\n"
content = header + content
# Use our global message handling system
await self.send_response(interaction, content, ephemeral=True)
except UnicodeDecodeError:
# If file can't be decoded as text, send as attachment
await interaction.followup.send(
f"File {path} appears to be binary. Sending as attachment:",
file=discord.File(path),
ephemeral=True
)
except Exception as e:
await interaction.followup.send(f"Error reading file {path}: {str(e)}", ephemeral=True)
@file_group.command(name="write", description="Write content to a file")
@app_commands.describe(
path="The path to the file to write",
content="The content to write to the file"
)
async def write_file(self, interaction: discord.Interaction, path: str, content: str):
"""Write content to a file."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "write_file", f"{path} (content length: {len(content)})")
try:
with open(path, "w", encoding="utf-8") as f:
f.write(content)
await self.send_response(interaction, f"Successfully wrote to {path}", ephemeral=True)
except Exception as e:
await self.send_response(interaction, f"Error writing to file {path}: {str(e)}", ephemeral=True)
@file_group.command(name="append", description="Append content to a file")
@app_commands.describe(
path="The path to the file to append to",
content="The content to append to the file"
)
async def append_file(self, interaction: discord.Interaction, path: str, content: str):
"""Append content to a file."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "append_file", f"{path} (content length: {len(content)})")
try:
with open(path, "a", encoding="utf-8") as f:
f.write(content)
await self.send_response(interaction, f"Successfully appended to {path}", ephemeral=True)
except Exception as e:
await self.send_response(interaction, f"Error appending to file {path}: {str(e)}", ephemeral=True)
@file_group.command(name="list", description="List files in a directory")
@app_commands.describe(path="The directory path to list")
async def list_files(self, interaction: discord.Interaction, path: str = "."):
"""List files in a directory."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "list_files", path)
try:
files = os.listdir(path)
file_info = []
for file in files:
full_path = os.path.join(path, file)
try:
stats = os.stat(full_path)
size = stats.st_size
is_dir = os.path.isdir(full_path)
file_info.append(f"{'📁' if is_dir else '📄'} {file} ({size} bytes)")
except Exception:
file_info.append(f"? {file} (error getting info)")
if file_info:
await self.send_response(interaction, f"Contents of {path}:\n\n" + "\n".join(file_info), ephemeral=True)
else:
await self.send_response(interaction, f"Directory {path} is empty", ephemeral=True)
except Exception as e:
await self.send_response(interaction, f"Error listing directory {path}: {str(e)}", ephemeral=True)
# System Commands
@sys_group.command(name="exec", description="Execute a shell command")
@app_commands.describe(
command="The command to execute",
use_sudo="Whether to use sudo"
)
async def execute_command(self, interaction: discord.Interaction, command: str, use_sudo: bool = False):
"""Execute a shell command."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "execute_command", f"{command} (sudo: {use_sudo})")
stdout, stderr, return_code = await self.run_command(command, use_sudo)
output = f"Command: {command}\nReturn Code: {return_code}\n\n"
if stdout:
output += f"STDOUT:\n{stdout}\n\n"
if stderr:
output += f"STDERR:\n{stderr}"
await self.send_response(interaction, output, ephemeral=True)
@sys_group.command(name="info", description="Display system information")
async def system_info(self, interaction: discord.Interaction):
"""Display system information."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "system_info")
# Get various system information
commands = [
("OS Information", "cat /etc/os-release"),
("Kernel Version", "uname -a"),
("CPU Information", "lscpu"),
("Memory Usage", "free -h"),
("Disk Usage", "df -h"),
("Uptime", "uptime")
]
results = []
for title, cmd in commands:
stdout, stderr, return_code = await self.run_command(cmd)
if return_code == 0:
results.append(f"=== {title} ===\n{stdout}")
else:
results.append(f"=== {title} ===\nError: {stderr}")
await self.send_response(interaction, "\n\n".join(results), ephemeral=True)
@sys_group.command(name="processes", description="List running processes")
@app_commands.describe(
filter="Optional filter string to search for specific processes"
)
async def list_processes(self, interaction: discord.Interaction, filter: str = ""):
"""List running processes, optionally filtered."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "list_processes", filter)
command = "ps aux"
if filter:
command += f" | grep -i {filter}"
stdout, stderr, return_code = await self.run_command(command)
if return_code == 0 and stdout:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"No processes found or error occurred: {stderr}", ephemeral=True)
@sys_group.command(name="kill", description="Kill a process by PID")
@app_commands.describe(
pid="Process ID to kill",
signal="Signal to send (default: 15/SIGTERM)"
)
async def kill_process(self, interaction: discord.Interaction, pid: int, signal: int = 15):
"""Kill a process by its PID."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "kill_process", f"PID: {pid}, Signal: {signal}")
_, stderr, return_code = await self.run_command(f"kill -{signal} {pid}", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, f"Successfully sent signal {signal} to process {pid}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to kill process {pid}: {stderr}", ephemeral=True)
@sys_group.command(name="disk", description="Show disk usage information")
async def disk_usage(self, interaction: discord.Interaction):
"""Show detailed disk usage information."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "disk_usage")
commands = [
("Disk Usage", "df -h"),
("Disk I/O Statistics", "iostat -x 1 2 | tail -n +4"),
("Largest Directories", "du -h --max-depth=1 / 2>/dev/null | sort -hr | head -n 10")
]
results = []
for title, cmd in commands:
stdout, stderr, return_code = await self.run_command(cmd)
if return_code == 0 and stdout:
results.append(f"=== {title} ===\n{stdout}")
else:
results.append(f"=== {title} ===\nError or no output: {stderr}")
await self.send_response(interaction, "\n\n".join(results), ephemeral=True)
@sys_group.command(name="env", description="View or set environment variables")
@app_commands.describe(
variable="Environment variable name (leave empty to list all)",
value="New value (leave empty to just view the variable)"
)
async def environment(self, interaction: discord.Interaction, variable: str = "", value: str = ""):
"""View or set environment variables."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
if not variable:
# List all environment variables
await self.log_command(interaction, "environment", "list all")
stdout, stderr, return_code = await self.run_command("env | sort")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to list environment variables: {stderr}", ephemeral=True)
elif not value:
# View a specific environment variable
await self.log_command(interaction, "environment", f"view {variable}")
stdout, stderr, return_code = await self.run_command(f"echo ${variable}")
if return_code == 0:
await self.send_response(interaction, f"{variable}={stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to get environment variable: {stderr}", ephemeral=True)
else:
# Set an environment variable
await self.log_command(interaction, "environment", f"set {variable}={value}")
# Write to .env file if it exists
env_file = ".env"
if os.path.exists(env_file):
try:
# Read current .env file
with open(env_file, "r") as f:
lines = f.readlines()
# Check if variable already exists
var_exists = False
for i, line in enumerate(lines):
if line.startswith(f"{variable}="):
lines[i] = f"{variable}={value}\n"
var_exists = True
break
# Add variable if it doesn't exist
if not var_exists:
lines.append(f"{variable}={value}\n")
# Write back to .env file
with open(env_file, "w") as f:
f.writelines(lines)
await self.send_response(interaction,
f"Environment variable {variable} set to {value} in {env_file}. "
f"Note: This will take effect after bot restart.",
ephemeral=True
)
except Exception as e:
await self.send_response(interaction, f"Error updating .env file: {str(e)}", ephemeral=True)
else:
await self.send_response(interaction,
f"No .env file found. Cannot persist environment variable.",
ephemeral=True
)
# Service Management Commands
@service_group.command(name="status", description="Check the status of a service")
@app_commands.describe(service="The name of the service to check")
async def service_status(self, interaction: discord.Interaction, service: str):
"""Check the status of a system service."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "service_status", service)
stdout, stderr, return_code = await self.run_command(f"systemctl status {service}", use_sudo=True)
if return_code == 0 or return_code == 3: # 3 is returned when service is stopped but exists
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Error checking service status: {stderr}", ephemeral=True)
@service_group.command(name="start", description="Start a service")
@app_commands.describe(service="The name of the service to start")
async def service_start(self, interaction: discord.Interaction, service: str):
"""Start a system service."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "service_start", service)
_, stderr, return_code = await self.run_command(f"systemctl start {service}", use_sudo=True)
if return_code == 0:
# Get the status after starting
status_stdout, _, _ = await self.run_command(f"systemctl status {service}", use_sudo=True)
await self.send_response(interaction, f"Service {service} started successfully.\n\n{status_stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to start service {service}: {stderr}", ephemeral=True)
@service_group.command(name="stop", description="Stop a service")
@app_commands.describe(service="The name of the service to stop")
async def service_stop(self, interaction: discord.Interaction, service: str):
"""Stop a system service."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "service_stop", service)
_, stderr, return_code = await self.run_command(f"systemctl stop {service}", use_sudo=True)
if return_code == 0:
await self.send_response(interaction, f"Service {service} stopped successfully.", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to stop service {service}: {stderr}", ephemeral=True)
@service_group.command(name="restart", description="Restart a service")
@app_commands.describe(service="The name of the service to restart")
async def service_restart(self, interaction: discord.Interaction, service: str):
"""Restart a system service."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "service_restart", service)
_, stderr, return_code = await self.run_command(f"systemctl restart {service}", use_sudo=True)
if return_code == 0:
# Get the status after restarting
status_stdout, _, _ = await self.run_command(f"systemctl status {service}", use_sudo=True)
await self.send_response(interaction, f"Service {service} restarted successfully.\n\n{status_stdout}", ephemeral=True)
else:
await self.send_response(interaction, f"Failed to restart service {service}: {stderr}", ephemeral=True)
@service_group.command(name="list", description="List all services")
@app_commands.describe(
filter="Optional filter string to search for specific services",
show_all="Show all services, including inactive ones"
)
async def service_list(self, interaction: discord.Interaction, filter: str = "", show_all: bool = False):
"""List system services, optionally filtered."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "service_list", f"filter: {filter}, show_all: {show_all}")
command = "systemctl list-units --type=service"
if show_all:
command += " --all"
if filter:
command += f" | grep -i {filter}"
stdout, stderr, return_code = await self.run_command(command, use_sudo=True)
if return_code == 0 and stdout:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"No services found or error occurred: {stderr}", ephemeral=True)
# Network Commands
@net_group.command(name="ping", description="Ping a host")
@app_commands.describe(
host="The host to ping",
count="Number of packets to send (default: 4)"
)
async def ping_host(self, interaction: discord.Interaction, host: str, count: int = 4):
"""Ping a host to check connectivity."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "ping_host", f"{host} (count: {count})")
stdout, stderr, return_code = await self.run_command(f"ping -c {count} {host}")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to ping {host}: {stderr}", ephemeral=True)
@net_group.command(name="ifconfig", description="Show network interface configuration")
@app_commands.describe(interface="Optional specific interface to show")
async def ifconfig(self, interaction: discord.Interaction, interface: str = ""):
"""Show network interface configuration."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "ifconfig", interface)
command = "ifconfig"
if interface:
command += f" {interface}"
stdout, stderr, return_code = await self.run_command(command)
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
# Try ip addr as fallback
stdout, stderr, return_code = await self.run_command("ip addr")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to get network interface info: {stderr}", ephemeral=True)
@net_group.command(name="netstat", description="Show network statistics")
@app_commands.describe(
option="Options for netstat (e.g., 'tuln' for TCP, UDP, listening, numeric)"
)
async def netstat(self, interaction: discord.Interaction, option: str = "tuln"):
"""Show network statistics."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "netstat", option)
stdout, stderr, return_code = await self.run_command(f"netstat -{option}")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
# Try ss as fallback
stdout, stderr, return_code = await self.run_command("ss -tuln")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to get network statistics: {stderr}", ephemeral=True)
@net_group.command(name="traceroute", description="Trace the route to a host")
@app_commands.describe(host="The host to trace the route to")
async def traceroute(self, interaction: discord.Interaction, host: str):
"""Trace the route to a host."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "traceroute", host)
stdout, stderr, return_code = await self.run_command(f"traceroute {host}")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to trace route to {host}: {stderr}", ephemeral=True)
@net_group.command(name="dns", description="Perform DNS lookup")
@app_commands.describe(
host="The host to lookup",
type="Record type (default: A)"
)
async def dns_lookup(self, interaction: discord.Interaction, host: str, type: str = "A"):
"""Perform DNS lookup for a host."""
if not await self.is_authorized_user(interaction):
return
await interaction.response.defer(ephemeral=True)
await self.log_command(interaction, "dns_lookup", f"{host} (type: {type})")
stdout, stderr, return_code = await self.run_command(f"dig {host} {type}")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
# Try nslookup as fallback
stdout, stderr, return_code = await self.run_command(f"nslookup -type={type} {host}")
if return_code == 0:
await self.send_response(interaction, stdout, ephemeral=True)
else:
await self.send_response(interaction, f"Failed to perform DNS lookup for {host}: {stderr}", ephemeral=True)
async def setup(bot: commands.Bot):
await bot.add_cog(AdminSysCog(bot))
print("AdminSysCog has been loaded.")