Added a sample.env
Moved incidents code to its own code Fixed errors that occur with that move.
This commit is contained in:
		
							
								
								
									
										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) | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								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" | ||||||
| @@ -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) | ||||||
| @@ -59,13 +63,4 @@ if __name__ == "__main__": | |||||||
|     load_dotenv() |     load_dotenv() | ||||||
|     client = EmeraldClient() |     client = EmeraldClient() | ||||||
|  |  | ||||||
|     # 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