Esmeralda/commands/incidents.py
Dan 33f0ce3a45 Added a sample.env
Moved incidents code to its own code
Fixed errors that occur with that move.
2025-01-29 22:01:59 -05:00

295 lines
13 KiB
Python

import discord
from discord import app_commands
from typing import Optional, List
from datetime import datetime, timedelta
import sqlite3
import logging
import uuid
from utils.database import Database
from discord.app_commands import MissingPermissions
db = Database()
class IncidentModal(discord.ui.Modal):
def __init__(self):
super().__init__(title="Log Incident")
self.reason = discord.ui.TextInput(
label="Reason for logging",
style=discord.TextStyle.long,
required=True
)
self.message_count = discord.ui.TextInput(
label="Recent messages to capture (1-50)",
placeholder="Leave blank to use timeframe",
default="",
required=False
)
self.start_time = discord.ui.TextInput(
label="Start time (YYYY-MM-DD HH:MM)",
placeholder="Optional - Example: 2024-05-10 14:30",
required=False
)
self.end_time = discord.ui.TextInput(
label="End time (YYYY-MM-DD HH:MM)",
placeholder="Optional - Example: 2024-05-10 15:00",
required=False
)
self.add_item(self.reason)
self.add_item(self.message_count)
self.add_item(self.start_time)
self.add_item(self.end_time)
async def on_submit(self, interaction: discord.Interaction):
try:
messages = []
capture_mode = "count"
capture_param = ""
start_time = None
end_time = None
if self.start_time.value or self.end_time.value:
if not all([self.start_time.value, self.end_time.value]):
raise ValueError("Both start and end times required for timeframe")
start_time = datetime.strptime(self.start_time.value, "%Y-%m-%d %H:%M")
end_time = datetime.strptime(self.end_time.value, "%Y-%m-%d %H:%M")
if start_time >= end_time:
raise ValueError("End time must be after start time")
if (end_time - start_time).total_seconds() > 86400:
raise ValueError("Maximum timeframe duration is 24 hours")
async for msg in interaction.channel.history(
limit=None,
after=start_time,
before=end_time
):
messages.append(msg)
messages = messages[::-1]
capture_mode = "timeframe"
capture_param = f"{start_time.strftime('%Y-%m-%d %H:%M')} to {end_time.strftime('%Y-%m-%d %H:%M')}"
else:
if not self.message_count.value:
raise ValueError("Please provide either message count or timeframe")
count = int(self.message_count.value)
if not 1 <= count <= 50:
raise ValueError("Message count must be between 1-50")
messages = [
msg async for msg in interaction.channel.history(limit=count)
][::-1]
capture_mode = "count"
capture_param = str(count)
formatted_messages = [{
"id": msg.id,
"author_id": msg.author.id,
"content": msg.content,
"timestamp": msg.created_at
} for msg in messages]
incident_id = f"incident_{uuid.uuid4().hex[:8]}"
success = db.add_incident(
incident_id=incident_id,
reason=self.reason.value,
moderator_id=interaction.user.id,
messages=formatted_messages,
capture_mode=capture_mode,
capture_param=capture_param,
start_time=start_time,
end_time=end_time
)
if not success:
raise Exception("Database storage failed - check server logs")
embed = discord.Embed(
title="✅ Incident Logged",
description=f"**ID:** `{incident_id}`\n**Mode:** {capture_mode.title()}",
color=0x00ff00
)
embed.add_field(name="Reason", value=self.reason.value[:500], inline=False)
if messages:
preview = f"{messages[0].content[:100]}..." if len(messages[0].content) > 100 else messages[0].content
embed.add_field(name="First Message", value=preview, inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
except ValueError as e:
await interaction.response.send_message(f"❌ Validation Error: {str(e)}", ephemeral=True)
except Exception as e:
logging.error(f"Incident submission error: {str(e)}", exc_info=True)
await interaction.response.send_message("⚠️ Failed to log incident - please check input format", ephemeral=True)
class FollowupModal(discord.ui.Modal):
def __init__(self, incident_id: str):
super().__init__(title=f"Follow-up: {incident_id}")
self.incident_id = incident_id
self.notes = discord.ui.TextInput(
label="Additional notes/actions",
style=discord.TextStyle.long,
required=True
)
self.add_item(self.notes)
async def on_submit(self, interaction: discord.Interaction):
try:
success = db.add_followup(
incident_id=self.incident_id,
moderator_id=interaction.user.id,
notes=self.notes.value
)
if success:
await interaction.response.send_message(f"✅ Follow-up added to **{self.incident_id}**", ephemeral=True)
else:
await interaction.response.send_message("❌ Failed to save follow-up", ephemeral=True)
except Exception as e:
logging.error(f"Followup error: {str(e)}")
await interaction.response.send_message("⚠️ Failed to add follow-up", ephemeral=True)
def setup(client):
incidents_group = app_commands.Group(name="incidents", description="Manage server incidents")
# Add all commands to the group
@incidents_group.command(name="log", description="Log a new incident")
@app_commands.checks.has_permissions(manage_messages=True)
async def incident_log(interaction: discord.Interaction):
await interaction.response.send_modal(IncidentModal())
@incidents_group.command(name="review", description="Review a logged incident")
@app_commands.describe(incident_id="The incident ID to review")
@app_commands.checks.has_permissions(manage_messages=True)
async def review_incident(interaction: discord.Interaction, incident_id: str):
try:
incident = db.get_incident(incident_id)
if not incident:
await interaction.response.send_message("❌ Incident not found", ephemeral=True)
return
messages = "\n\n".join(
f"**<t:{int(msg['timestamp'].timestamp())}:F>** <@{msg['author_id']}>:\n{msg['content']}"
for msg in incident['messages']
)
followups = db.get_followups(incident_id)
followup_text = "\n\n".join(
f"**<t:{int(f['timestamp'].timestamp())}:f>** <@{f['moderator_id']}>:\n{f['notes'][:200]}"
for f in followups
) if followups else "No follow-up reports yet"
moderator = await interaction.guild.fetch_member(incident['details']['moderator_id'])
embed = discord.Embed(
title=f"Incident {incident_id}",
description=(
f"**Reason:** {incident['details']['reason']}\n"
f"**Logged by:** {moderator.mention}\n"
f"**When:** <t:{int(incident['details']['timestamp'].timestamp())}:F>"
),
color=0xff0000
)
embed.add_field(name="Messages", value=messages[:1020] + "..." if len(messages) > 1024 else messages, inline=False)
embed.add_field(name=f"Follow-ups ({len(followups)})", value=followup_text[:1020] + "..." if len(followup_text) > 1024 else followup_text, inline=False)
await interaction.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
logging.error(f"Incident review error: {str(e)}", exc_info=True)
await interaction.response.send_message("❌ Failed to retrieve incident", ephemeral=True)
@incidents_group.command(name="followup", description="Add follow-up to an incident")
@app_commands.describe(incident_id="The incident ID to follow up on", notes="Quick note (optional)")
@app_commands.checks.has_permissions(manage_messages=True)
async def add_followup(interaction: discord.Interaction, incident_id: str, notes: Optional[str] = None):
try:
if not db.get_incident(incident_id):
await interaction.response.send_message("❌ Incident not found", ephemeral=True)
return
if notes:
success = db.add_followup(incident_id, interaction.user.id, notes)
if success:
await interaction.response.send_message(f"✅ Added quick follow-up to **{incident_id}**", ephemeral=True)
else:
await interaction.response.send_message("❌ Failed to add follow-up", ephemeral=True)
else:
await interaction.response.send_modal(FollowupModal(incident_id))
except Exception as e:
logging.error(f"Followup error: {e}")
await interaction.response.send_message("⚠️ Failed to add follow-up", ephemeral=True)
@incidents_group.command(name="audit", description="View unauthorized access attempts (Admin only)")
@app_commands.describe(days="Lookback period in days (max 30)")
@app_commands.checks.has_permissions(administrator=True)
async def view_audit_log(interaction: discord.Interaction, days: int = 7):
try:
if days > 30 or days < 1:
raise ValueError("Lookback period must be 1-30 days")
cutoff = datetime.now() - timedelta(days=days)
with db._get_connection() as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM unauthorized_access WHERE timestamp > ? ORDER BY timestamp DESC", (cutoff.isoformat(),))
results = [dict(row) for row in cursor.fetchall()]
if not results:
await interaction.response.send_message("✅ No unauthorized access attempts found", ephemeral=True)
return
log_text = "\n".join(
f"<t:{int(datetime.fromisoformat(row['timestamp']).timestamp())}:f> | "
f"<@{row['user_id']}> tried `{row['command_used']}`"
for row in results
)
embed = discord.Embed(
title=f"Unauthorized Access Logs ({days} days)",
description=log_text[:4000],
color=0xff0000
)
await interaction.response.send_message(embed=embed, ephemeral=True)
except Exception as e:
await interaction.response.send_message(f"❌ Error: {str(e)}", ephemeral=True)
@review_incident.autocomplete("incident_id")
@add_followup.autocomplete("incident_id")
async def incident_autocomplete(interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]:
incidents = db.get_recent_incidents(25)
choices = []
for inc in incidents:
try:
mod = await interaction.guild.fetch_member(inc['moderator_id'])
name = f"{inc['id']} (by {mod.display_name})"
except:
name = inc['id']
if current.lower() in name.lower():
choices.append(app_commands.Choice(name=name, value=inc['id']))
return choices[:25]
@client.tree.error
async def handle_incident_errors(interaction: discord.Interaction, error):
if isinstance(error, MissingPermissions):
db.log_unauthorized_access(
user_id=interaction.user.id,
command_used=interaction.command.name if interaction.command else "unknown",
details=f"Attempted params: {interaction.data}"
)
await interaction.response.send_message("⛔ You don't have permission to use this command.", ephemeral=True)
else:
logging.error(f"Command error: {error}", exc_info=True)
await interaction.response.send_message("⚠️ An unexpected error occurred. This has been logged.", ephemeral=True)
client.tree.add_command(incidents_group)