Compare commits
	
		
			2 Commits
		
	
	
		
			dev-moment
			...
			Dev-Modula
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					43baa58834 | ||
| 
						 | 
					33f0ce3a45 | 
							
								
								
									
										294
									
								
								commands/incidents.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								commands/incidents.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
				
			|||||||
 | 
					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)
 | 
				
			||||||
@@ -1,181 +1,110 @@
 | 
				
			|||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord import app_commands
 | 
					from discord import app_commands
 | 
				
			||||||
from typing import Optional, List
 | 
					from discord.ext import tasks
 | 
				
			||||||
from datetime import datetime, timedelta
 | 
					from typing import Optional
 | 
				
			||||||
import sqlite3
 | 
					from datetime import datetime, timedelta, time, timezone
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
from utils.database import Database
 | 
					from utils.database import Database
 | 
				
			||||||
from discord.app_commands import MissingPermissions
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
db = Database()
 | 
					db = Database()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IncidentModal(discord.ui.Modal):
 | 
					class FunnyMoments:
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self, client):
 | 
				
			||||||
        super().__init__(title="Log Incident")
 | 
					        self.client = client
 | 
				
			||||||
        self.reason = discord.ui.TextInput(
 | 
					        self.retention_days = 30
 | 
				
			||||||
            label="Reason for logging",
 | 
					        self.highlight_channel = None  # Will be set in on_ready
 | 
				
			||||||
            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)
 | 
					        # Start scheduled tasks
 | 
				
			||||||
        self.add_item(self.message_count)
 | 
					        self.weekly_highlight.start()
 | 
				
			||||||
        self.add_item(self.start_time)
 | 
					        self.daily_purge.start()
 | 
				
			||||||
        self.add_item(self.end_time)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def on_submit(self, interaction: discord.Interaction):
 | 
					    async def on_ready(self):
 | 
				
			||||||
 | 
					        """Initialize channel after bot is ready"""
 | 
				
			||||||
 | 
					        channel_id = int(os.getenv("HIGHLIGHT_CHANNEL_ID"))
 | 
				
			||||||
 | 
					        self.highlight_channel = self.client.get_channel(channel_id)
 | 
				
			||||||
 | 
					        if not self.highlight_channel:
 | 
				
			||||||
 | 
					            logging.error("Invalid highlight channel ID in .env")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @tasks.loop(time=time(hour=9, minute=0, tzinfo=timezone.utc))
 | 
				
			||||||
 | 
					    async def weekly_highlight(self):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            messages = []
 | 
					            # Only run on Mondays (0 = Monday)
 | 
				
			||||||
            capture_mode = "count"
 | 
					            if datetime.now(timezone.utc).weekday() != 0:
 | 
				
			||||||
            capture_param = ""
 | 
					                return
 | 
				
			||||||
            start_time = None
 | 
					 | 
				
			||||||
            end_time = None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Determine capture mode
 | 
					            start_date = datetime.now(timezone.utc) - timedelta(days=7)
 | 
				
			||||||
            if self.start_time.value or self.end_time.value:
 | 
					            moments = db.get_funny_moments_since(start_date)
 | 
				
			||||||
                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")
 | 
					            if not moments or not self.highlight_channel:
 | 
				
			||||||
                end_time = datetime.strptime(self.end_time.value, "%Y-%m-%d %H:%M")
 | 
					                return
 | 
				
			||||||
 | 
					 | 
				
			||||||
                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]  # Oldest first
 | 
					 | 
				
			||||||
                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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Format messages
 | 
					 | 
				
			||||||
            formatted_messages = [{
 | 
					 | 
				
			||||||
                "id": msg.id,
 | 
					 | 
				
			||||||
                "author_id": msg.author.id,
 | 
					 | 
				
			||||||
                "content": msg.content,
 | 
					 | 
				
			||||||
                "timestamp": msg.created_at
 | 
					 | 
				
			||||||
            } for msg in messages]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Generate unique ID
 | 
					 | 
				
			||||||
            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(
 | 
					            embed = discord.Embed(
 | 
				
			||||||
                title="✅ Incident Logged",
 | 
					                title="😂 Weekly Funny Moments Highlight",
 | 
				
			||||||
                description=f"**ID:** `{incident_id}`\n**Mode:** {capture_mode.title()}",
 | 
					 | 
				
			||||||
                color=0x00ff00
 | 
					                color=0x00ff00
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            embed.add_field(name="Reason", value=self.reason.value[:500], inline=False)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if messages:
 | 
					            for idx, moment in enumerate(moments[:5], 1):
 | 
				
			||||||
                preview = f"{messages[0].content[:100]}..." if len(messages[0].content) > 100 else messages[0].content
 | 
					                embed.add_field(
 | 
				
			||||||
                embed.add_field(name="First Message", value=preview, inline=False)
 | 
					                    name=f"Moment #{idx}",
 | 
				
			||||||
 | 
					                    value=f"[Jump to Message]({moment['message_link']})",
 | 
				
			||||||
            await interaction.response.send_message(embed=embed, ephemeral=True)
 | 
					                    inline=False
 | 
				
			||||||
 | 
					 | 
				
			||||||
        except ValueError as e:
 | 
					 | 
				
			||||||
            await interaction.response.send_message(
 | 
					 | 
				
			||||||
                f"❌ Validation Error: {str(e)}",
 | 
					 | 
				
			||||||
                ephemeral=True
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await self.highlight_channel.send(embed=embed)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            logging.error(f"Incident submission error: {str(e)}", exc_info=True)
 | 
					            logging.error(f"Weekly highlight error: {str(e)}")
 | 
				
			||||||
            await interaction.response.send_message(
 | 
					 | 
				
			||||||
                "⚠️ Failed to log incident - please check input format",
 | 
					 | 
				
			||||||
                ephemeral=True
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @tasks.loop(hours=24)
 | 
				
			||||||
class FollowupModal(discord.ui.Modal):
 | 
					    async def daily_purge(self):
 | 
				
			||||||
    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:
 | 
					        try:
 | 
				
			||||||
            success = db.add_followup(
 | 
					            cutoff = datetime.utcnow() - timedelta(days=self.retention_days)
 | 
				
			||||||
                incident_id=self.incident_id,
 | 
					            deleted_count = db.purge_old_funny_moments(cutoff)
 | 
				
			||||||
                moderator_id=interaction.user.id,
 | 
					            logging.info(f"Purged {deleted_count} old funny moments")
 | 
				
			||||||
                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:
 | 
					        except Exception as e:
 | 
				
			||||||
            logging.error(f"Followup error: {str(e)}")
 | 
					            logging.error(f"Purge error: {str(e)}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @weekly_highlight.before_loop
 | 
				
			||||||
 | 
					    @daily_purge.before_loop
 | 
				
			||||||
 | 
					    async def before_tasks(self):
 | 
				
			||||||
 | 
					        await self.client.wait_until_ready()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def setup(client):
 | 
				
			||||||
 | 
					    # Create command group
 | 
				
			||||||
 | 
					    moments_group = app_commands.Group(name="moments", description="Manage funny moments")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add purge command to the group
 | 
				
			||||||
 | 
					    @moments_group.command(
 | 
				
			||||||
 | 
					        name="purge",
 | 
				
			||||||
 | 
					        description="Purge old funny moments"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @app_commands.describe(days="Purge moments older than X days (0 to disable)")
 | 
				
			||||||
 | 
					    @app_commands.checks.has_permissions(manage_messages=True)
 | 
				
			||||||
 | 
					    async def purge_funny(interaction: discord.Interaction, days: int = 30):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if days < 0:
 | 
				
			||||||
 | 
					                raise ValueError("Days must be >= 0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            cutoff = datetime.now(timezone.utc) - timedelta(days=days)
 | 
				
			||||||
 | 
					            deleted_count = db.purge_old_funny_moments(cutoff)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await interaction.response.send_message(
 | 
					            await interaction.response.send_message(
 | 
				
			||||||
                "⚠️ Failed to add follow-up",
 | 
					                f"✅ Purged {deleted_count} moments older than {days} days",
 | 
				
			||||||
                ephemeral=True
 | 
					                ephemeral=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            await interaction.response.send_message(
 | 
				
			||||||
 | 
					                f"❌ Error: {str(e)}",
 | 
				
			||||||
 | 
					                ephemeral=True
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create FunnyMoments instance
 | 
				
			||||||
 | 
					    funny_moments = FunnyMoments(client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def setup(client):
 | 
					 | 
				
			||||||
    # Context menu command
 | 
					    # Context menu command
 | 
				
			||||||
    @client.tree.context_menu(name="Mark as Funny Moment")
 | 
					    @client.tree.context_menu(name="Mark as Funny Moment")
 | 
				
			||||||
    async def mark_funny(interaction: discord.Interaction, message: discord.Message):
 | 
					    async def mark_funny(interaction: discord.Interaction, message: discord.Message):
 | 
				
			||||||
@@ -202,10 +131,7 @@ async def setup(client):
 | 
				
			|||||||
                inline=False
 | 
					                inline=False
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await interaction.response.send_message(
 | 
					            await interaction.response.send_message(embed=embed, ephemeral=True)
 | 
				
			||||||
                embed=embed,
 | 
					 | 
				
			||||||
                ephemeral=True
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            logging.error(f"Context menu error: {e}")
 | 
					            logging.error(f"Context menu error: {e}")
 | 
				
			||||||
@@ -214,213 +140,27 @@ async def setup(client):
 | 
				
			|||||||
                ephemeral=True
 | 
					                ephemeral=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Command group
 | 
					    # Purge command
 | 
				
			||||||
    moments_group = app_commands.Group(
 | 
					    @app_commands.command(name="purge_funny")
 | 
				
			||||||
        name="moments",
 | 
					    @app_commands.describe(days="Purge moments older than X days (0 to disable)")
 | 
				
			||||||
        description="Manage memorable moments"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Incident command
 | 
					 | 
				
			||||||
    @moments_group.command(
 | 
					 | 
				
			||||||
        name="incident",
 | 
					 | 
				
			||||||
        description="Log an incident with recent messages"
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @app_commands.checks.has_permissions(manage_messages=True)
 | 
					    @app_commands.checks.has_permissions(manage_messages=True)
 | 
				
			||||||
    async def incident_log(interaction: discord.Interaction):
 | 
					    async def purge_funny(interaction: discord.Interaction, days: int = 30):
 | 
				
			||||||
        await interaction.response.send_modal(IncidentModal())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Review command
 | 
					 | 
				
			||||||
    @moments_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:
 | 
					        try:
 | 
				
			||||||
            incident = db.get_incident(incident_id)
 | 
					            if days < 0:
 | 
				
			||||||
            if not incident:
 | 
					                raise ValueError("Days must be >= 0")
 | 
				
			||||||
                await interaction.response.send_message("❌ Incident not found", ephemeral=True)
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Format messages with proper timestamps
 | 
					            cutoff = datetime.utcnow() - timedelta(days=days)
 | 
				
			||||||
            messages = "\n\n".join(
 | 
					            deleted_count = db.purge_old_funny_moments(cutoff)
 | 
				
			||||||
                f"**<t:{int(msg['timestamp'].timestamp())}:F>** <@{msg['author_id']}>:\n"
 | 
					
 | 
				
			||||||
                f"{msg['content']}"
 | 
					            await interaction.response.send_message(
 | 
				
			||||||
                for msg in incident['messages']
 | 
					                f"✅ Purged {deleted_count} moments older than {days} days",
 | 
				
			||||||
 | 
					                ephemeral=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Get follow-ups with proper timestamps
 | 
					 | 
				
			||||||
            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"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create embed
 | 
					 | 
				
			||||||
            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:
 | 
					        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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Followup commands
 | 
					 | 
				
			||||||
    @moments_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(
 | 
					            await interaction.response.send_message(
 | 
				
			||||||
                    "❌ Incident not found",
 | 
					                f"❌ Error: {str(e)}",
 | 
				
			||||||
                ephemeral=True
 | 
					                ephemeral=True
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if notes:
 | 
					 | 
				
			||||||
                success = db.add_followup(
 | 
					 | 
				
			||||||
                    incident_id=incident_id,
 | 
					 | 
				
			||||||
                    moderator_id=interaction.user.id,
 | 
					 | 
				
			||||||
                    notes=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
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Autocomplete
 | 
					 | 
				
			||||||
    @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]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Global error handler for commands
 | 
					 | 
				
			||||||
    @client.tree.error
 | 
					 | 
				
			||||||
    async def on_command_error(interaction: discord.Interaction, error):
 | 
					 | 
				
			||||||
        """Handle unauthorized access attempts"""
 | 
					 | 
				
			||||||
        if isinstance(error, MissingPermissions):
 | 
					 | 
				
			||||||
            # Log unauthorized access
 | 
					 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @moments_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(),))  # Store and compare ISO format
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            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)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client.tree.add_command(moments_group)
 | 
					    client.tree.add_command(moments_group)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								main.py
									
									
									
									
									
								
							@@ -1,4 +1,3 @@
 | 
				
			|||||||
import os
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import discord
 | 
					import discord
 | 
				
			||||||
from discord import app_commands
 | 
					from discord import app_commands
 | 
				
			||||||
@@ -10,6 +9,7 @@ from pydantic_settings import BaseSettings
 | 
				
			|||||||
class Settings(BaseSettings):
 | 
					class Settings(BaseSettings):
 | 
				
			||||||
    DISCORD_TOKEN: str
 | 
					    DISCORD_TOKEN: str
 | 
				
			||||||
    DISCORD_GUILD: int
 | 
					    DISCORD_GUILD: int
 | 
				
			||||||
 | 
					    HIGHLIGHT_CHANNEL_ID: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Config:
 | 
					    class Config:
 | 
				
			||||||
        env_file = ".env"
 | 
					        env_file = ".env"
 | 
				
			||||||
@@ -27,7 +27,7 @@ logging.basicConfig(
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EmeraldClient(discord.Client):
 | 
					class EsmeraldaClient(discord.Client):
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        intents = discord.Intents.default()
 | 
					        intents = discord.Intents.default()
 | 
				
			||||||
        intents.message_content = True
 | 
					        intents.message_content = True
 | 
				
			||||||
@@ -37,9 +37,13 @@ class EmeraldClient(discord.Client):
 | 
				
			|||||||
        self.tree = app_commands.CommandTree(self)
 | 
					        self.tree = app_commands.CommandTree(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def setup_hook(self):
 | 
					    async def setup_hook(self):
 | 
				
			||||||
        # Load commands
 | 
					        # Load command modules
 | 
				
			||||||
        from commands.moments import setup as moments_setup
 | 
					        from commands.moments import setup as moments_setup
 | 
				
			||||||
        await moments_setup(self)
 | 
					        from commands.incidents import setup as incidents_setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Initialize command groups
 | 
				
			||||||
 | 
					        moments_setup(self)
 | 
				
			||||||
 | 
					        incidents_setup(self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Sync commands
 | 
					        # Sync commands
 | 
				
			||||||
        guild = discord.Object(id=config.DISCORD_GUILD)
 | 
					        guild = discord.Object(id=config.DISCORD_GUILD)
 | 
				
			||||||
@@ -57,15 +61,6 @@ class EmeraldClient(discord.Client):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    load_dotenv()
 | 
					    load_dotenv()
 | 
				
			||||||
    client = EmeraldClient()
 | 
					    client = EsmeraldaClient()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Global error handler
 | 
					 | 
				
			||||||
    @client.tree.error
 | 
					 | 
				
			||||||
    async def on_error(interaction: discord.Interaction, error):
 | 
					 | 
				
			||||||
        logging.error(f"Error: {error}")
 | 
					 | 
				
			||||||
        await interaction.response.send_message(
 | 
					 | 
				
			||||||
            "⚠️ Something went wrong. Please try again.",
 | 
					 | 
				
			||||||
            ephemeral=True
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    client.run(config.DISCORD_TOKEN)
 | 
					    client.run(config.DISCORD_TOKEN)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								sample.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								sample.env
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					DISCORD_TOKEN="YOUR_TOKEN_HERE"
 | 
				
			||||||
 | 
					DISCORD_GUILD="YOUR_GUILD_ID"
 | 
				
			||||||
 | 
					HIGHLIGHT_CHANNEL_ID="YOUR_CHANNEL_ID"
 | 
				
			||||||
@@ -196,6 +196,29 @@ class Database:
 | 
				
			|||||||
                for row in cursor.fetchall()
 | 
					                for row in cursor.fetchall()
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_funny_moments_since(self, start_date: datetime) -> List[Dict]:
 | 
				
			||||||
 | 
					        """Get funny moments since specified date"""
 | 
				
			||||||
 | 
					        with self._get_connection() as conn:
 | 
				
			||||||
 | 
					            conn.row_factory = sqlite3.Row
 | 
				
			||||||
 | 
					            cursor = conn.cursor()
 | 
				
			||||||
 | 
					            cursor.execute("""
 | 
				
			||||||
 | 
					                SELECT * FROM funny_moments
 | 
				
			||||||
 | 
					                WHERE timestamp > ?
 | 
				
			||||||
 | 
					                ORDER BY timestamp DESC
 | 
				
			||||||
 | 
					            """, (start_date,))
 | 
				
			||||||
 | 
					            return [dict(row) for row in cursor.fetchall()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def purge_old_funny_moments(self, cutoff_date: datetime) -> int:
 | 
				
			||||||
 | 
					        """Delete funny moments older than specified date"""
 | 
				
			||||||
 | 
					        with self._get_connection() as conn:
 | 
				
			||||||
 | 
					            cursor = conn.cursor()
 | 
				
			||||||
 | 
					            cursor.execute("""
 | 
				
			||||||
 | 
					                DELETE FROM funny_moments
 | 
				
			||||||
 | 
					                WHERE timestamp < ?
 | 
				
			||||||
 | 
					            """, (cutoff_date,))
 | 
				
			||||||
 | 
					            conn.commit()
 | 
				
			||||||
 | 
					            return cursor.rowcount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def log_unauthorized_access(self, user_id: int, command_used: str, details: str = ""):
 | 
					    def log_unauthorized_access(self, user_id: int, command_used: str, details: str = ""):
 | 
				
			||||||
        """Log unauthorized command attempts"""
 | 
					        """Log unauthorized command attempts"""
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user