Compare commits
	
		
			9 Commits
		
	
	
		
			main
			...
			Dev-Modula
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					43baa58834 | ||
| 
						 | 
					33f0ce3a45 | ||
| 
						 | 
					87fa9c6aee | ||
| 
						 | 
					02155f3e0f | ||
| 
						 | 
					979e7c74d5 | ||
| 
						 | 
					31913db64f | ||
| 
						 | 
					6f2b13f055 | ||
| 
						 | 
					c4962f2d09 | ||
| 
						 | 
					03f6337e27 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -172,3 +172,4 @@ cython_debug/
 | 
			
		||||
 | 
			
		||||
# PyPI configuration file
 | 
			
		||||
.pypirc
 | 
			
		||||
/data
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
							
								
								
									
										166
									
								
								commands/moments.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								commands/moments.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
import discord
 | 
			
		||||
from discord import app_commands
 | 
			
		||||
from discord.ext import tasks
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from datetime import datetime, timedelta, time, timezone
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from utils.database import Database
 | 
			
		||||
 | 
			
		||||
db = Database()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FunnyMoments:
 | 
			
		||||
    def __init__(self, client):
 | 
			
		||||
        self.client = client
 | 
			
		||||
        self.retention_days = 30
 | 
			
		||||
        self.highlight_channel = None  # Will be set in on_ready
 | 
			
		||||
 | 
			
		||||
        # Start scheduled tasks
 | 
			
		||||
        self.weekly_highlight.start()
 | 
			
		||||
        self.daily_purge.start()
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
            # Only run on Mondays (0 = Monday)
 | 
			
		||||
            if datetime.now(timezone.utc).weekday() != 0:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            start_date = datetime.now(timezone.utc) - timedelta(days=7)
 | 
			
		||||
            moments = db.get_funny_moments_since(start_date)
 | 
			
		||||
 | 
			
		||||
            if not moments or not self.highlight_channel:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            embed = discord.Embed(
 | 
			
		||||
                title="😂 Weekly Funny Moments Highlight",
 | 
			
		||||
                color=0x00ff00
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            for idx, moment in enumerate(moments[:5], 1):
 | 
			
		||||
                embed.add_field(
 | 
			
		||||
                    name=f"Moment #{idx}",
 | 
			
		||||
                    value=f"[Jump to Message]({moment['message_link']})",
 | 
			
		||||
                    inline=False
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            await self.highlight_channel.send(embed=embed)
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Weekly highlight error: {str(e)}")
 | 
			
		||||
 | 
			
		||||
    @tasks.loop(hours=24)
 | 
			
		||||
    async def daily_purge(self):
 | 
			
		||||
        try:
 | 
			
		||||
            cutoff = datetime.utcnow() - timedelta(days=self.retention_days)
 | 
			
		||||
            deleted_count = db.purge_old_funny_moments(cutoff)
 | 
			
		||||
            logging.info(f"Purged {deleted_count} old funny moments")
 | 
			
		||||
        except Exception as 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(
 | 
			
		||||
                f"✅ Purged {deleted_count} moments older than {days} days",
 | 
			
		||||
                ephemeral=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            await interaction.response.send_message(
 | 
			
		||||
                f"❌ Error: {str(e)}",
 | 
			
		||||
                ephemeral=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    # Create FunnyMoments instance
 | 
			
		||||
    funny_moments = FunnyMoments(client)
 | 
			
		||||
 | 
			
		||||
    # Context menu command
 | 
			
		||||
    @client.tree.context_menu(name="Mark as Funny Moment")
 | 
			
		||||
    async def mark_funny(interaction: discord.Interaction, message: discord.Message):
 | 
			
		||||
        try:
 | 
			
		||||
            await message.add_reaction("😂")
 | 
			
		||||
            await message.add_reaction("🎉")
 | 
			
		||||
 | 
			
		||||
            message_link = f"https://discord.com/channels/{interaction.guild_id}/{message.channel.id}/{message.id}"
 | 
			
		||||
            record_id = db.add_funny_moment(
 | 
			
		||||
                message_link=message_link,
 | 
			
		||||
                author_id=message.author.id
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            embed = discord.Embed(
 | 
			
		||||
                title="😂 Funny Moment Saved",
 | 
			
		||||
                description=f"[Jump to Message]({message_link})",
 | 
			
		||||
                color=0x00ff00
 | 
			
		||||
            ).set_author(
 | 
			
		||||
                name=message.author.display_name,
 | 
			
		||||
                icon_url=message.author.avatar.url
 | 
			
		||||
            ).add_field(
 | 
			
		||||
                name="Message Preview",
 | 
			
		||||
                value=message.content[:100] + "..." if len(message.content) > 100 else message.content,
 | 
			
		||||
                inline=False
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            await interaction.response.send_message(embed=embed, ephemeral=True)
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Context menu error: {e}")
 | 
			
		||||
            await interaction.response.send_message(
 | 
			
		||||
                "❌ Couldn't mark this message. Is it too old?",
 | 
			
		||||
                ephemeral=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    # Purge command
 | 
			
		||||
    @app_commands.command(name="purge_funny")
 | 
			
		||||
    @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.utcnow() - timedelta(days=days)
 | 
			
		||||
            deleted_count = db.purge_old_funny_moments(cutoff)
 | 
			
		||||
 | 
			
		||||
            await interaction.response.send_message(
 | 
			
		||||
                f"✅ Purged {deleted_count} moments older than {days} days",
 | 
			
		||||
                ephemeral=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            await interaction.response.send_message(
 | 
			
		||||
                f"❌ Error: {str(e)}",
 | 
			
		||||
                ephemeral=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    client.tree.add_command(moments_group)
 | 
			
		||||
							
								
								
									
										31
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								main.py
									
									
									
									
									
								
							@@ -1,4 +1,3 @@
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
import discord
 | 
			
		||||
from discord import app_commands
 | 
			
		||||
@@ -6,9 +5,11 @@ from dotenv import load_dotenv
 | 
			
		||||
from pydantic_settings import BaseSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Configuration
 | 
			
		||||
class Settings(BaseSettings):
 | 
			
		||||
    DISCORD_TOKEN: str
 | 
			
		||||
    DISCORD_GUILD: int
 | 
			
		||||
    HIGHLIGHT_CHANNEL_ID: int
 | 
			
		||||
 | 
			
		||||
    class Config:
 | 
			
		||||
        env_file = ".env"
 | 
			
		||||
@@ -17,7 +18,8 @@ class Settings(BaseSettings):
 | 
			
		||||
 | 
			
		||||
config = Settings()
 | 
			
		||||
 | 
			
		||||
# Initialize logging
 | 
			
		||||
 | 
			
		||||
# Logging setup
 | 
			
		||||
logging.basicConfig(
 | 
			
		||||
    level=logging.INFO,
 | 
			
		||||
    format="%(asctime)s | %(levelname)s | %(message)s",
 | 
			
		||||
@@ -25,29 +27,40 @@ logging.basicConfig(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmeraldClient(discord.Client):
 | 
			
		||||
class EsmeraldaClient(discord.Client):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        intents = discord.Intents.default()
 | 
			
		||||
        intents.message_content = True
 | 
			
		||||
        intents.messages = True
 | 
			
		||||
        intents.reactions = True
 | 
			
		||||
        super().__init__(intents=intents)
 | 
			
		||||
        self.tree = app_commands.CommandTree(self)
 | 
			
		||||
 | 
			
		||||
    async def setup_hook(self):
 | 
			
		||||
        """Sync commands with test guild"""
 | 
			
		||||
        # Load command modules
 | 
			
		||||
        from commands.moments import setup as moments_setup
 | 
			
		||||
        from commands.incidents import setup as incidents_setup
 | 
			
		||||
 | 
			
		||||
        # Initialize command groups
 | 
			
		||||
        moments_setup(self)
 | 
			
		||||
        incidents_setup(self)
 | 
			
		||||
 | 
			
		||||
        # Sync commands
 | 
			
		||||
        guild = discord.Object(id=config.DISCORD_GUILD)
 | 
			
		||||
        self.tree.copy_global_to(guild=guild)
 | 
			
		||||
        await self.tree.sync(guild=guild)
 | 
			
		||||
        logging.info("Commands synced to guild")
 | 
			
		||||
        logging.info("Commands synced")
 | 
			
		||||
 | 
			
		||||
    async def on_ready(self):
 | 
			
		||||
        logging.info(f"Logged in as {self.user} (ID: {self.user.id})")
 | 
			
		||||
        logging.info(f"Logged in as {self.user}")
 | 
			
		||||
        await self.change_presence(activity=discord.Activity(
 | 
			
		||||
            type=discord.ActivityType.listening,
 | 
			
		||||
            name="your requests"
 | 
			
		||||
            name="/moments"
 | 
			
		||||
        ))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    load_dotenv()
 | 
			
		||||
    client = EmeraldClient()
 | 
			
		||||
    client.run(config.DISCORD_TOKEN)
 | 
			
		||||
    client = EsmeraldaClient()
 | 
			
		||||
 | 
			
		||||
    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"
 | 
			
		||||
							
								
								
									
										235
									
								
								utils/database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								utils/database.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
			
		||||
import sqlite3
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import List, Dict, Optional
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Database:
 | 
			
		||||
    def __init__(self, db_path: str = "data/moments.db"):
 | 
			
		||||
        self.db_path = db_path
 | 
			
		||||
        self._init_db()
 | 
			
		||||
 | 
			
		||||
    def _init_db(self):
 | 
			
		||||
        """Initialize database tables"""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            # Drop tables if they exist (for development)
 | 
			
		||||
            conn.execute("DROP TABLE IF EXISTS incidents")
 | 
			
		||||
            conn.execute("DROP TABLE IF EXISTS incident_messages")
 | 
			
		||||
 | 
			
		||||
            conn.execute("""
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS unauthorized_access (
 | 
			
		||||
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
                    user_id INTEGER NOT NULL,
 | 
			
		||||
                    command_used TEXT NOT NULL,
 | 
			
		||||
                    timestamp DATETIME NOT NULL,
 | 
			
		||||
                    details TEXT
 | 
			
		||||
                )
 | 
			
		||||
            """)
 | 
			
		||||
 | 
			
		||||
            conn.execute("""
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS funny_moments (
 | 
			
		||||
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
                    message_link TEXT NOT NULL,
 | 
			
		||||
                    description TEXT,
 | 
			
		||||
                    author_id INTEGER NOT NULL,
 | 
			
		||||
                    timestamp DATETIME NOT NULL
 | 
			
		||||
                )
 | 
			
		||||
            """)
 | 
			
		||||
 | 
			
		||||
            conn.execute("""
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS incidents (
 | 
			
		||||
                    id TEXT PRIMARY KEY,
 | 
			
		||||
                    reason TEXT NOT NULL,
 | 
			
		||||
                    moderator_id INTEGER NOT NULL,
 | 
			
		||||
                    timestamp DATETIME NOT NULL,
 | 
			
		||||
                    capture_mode TEXT NOT NULL,
 | 
			
		||||
                    capture_param TEXT,
 | 
			
		||||
                    start_time DATETIME,
 | 
			
		||||
                    end_time DATETIME
 | 
			
		||||
                )
 | 
			
		||||
            """)
 | 
			
		||||
 | 
			
		||||
            conn.execute("""
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS incident_messages (
 | 
			
		||||
                    incident_id TEXT,
 | 
			
		||||
                    message_id INTEGER,
 | 
			
		||||
                    author_id INTEGER,
 | 
			
		||||
                    content TEXT,
 | 
			
		||||
                    timestamp DATETIME,
 | 
			
		||||
                    PRIMARY KEY (incident_id, message_id),
 | 
			
		||||
                    FOREIGN KEY (incident_id) REFERENCES incidents(id)
 | 
			
		||||
                )
 | 
			
		||||
            """)
 | 
			
		||||
 | 
			
		||||
            conn.execute("""
 | 
			
		||||
                CREATE TABLE IF NOT EXISTS incident_followups (
 | 
			
		||||
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
                    incident_id TEXT NOT NULL,
 | 
			
		||||
                    moderator_id INTEGER NOT NULL,
 | 
			
		||||
                    notes TEXT NOT NULL,
 | 
			
		||||
                    timestamp DATETIME NOT NULL,
 | 
			
		||||
                    FOREIGN KEY (incident_id) REFERENCES incidents(id)
 | 
			
		||||
                )
 | 
			
		||||
            """)
 | 
			
		||||
            conn.commit()
 | 
			
		||||
 | 
			
		||||
    def _get_connection(self):
 | 
			
		||||
        return sqlite3.connect(self.db_path)
 | 
			
		||||
 | 
			
		||||
    def add_incident(self, incident_id: str, reason: str, moderator_id: int, 
 | 
			
		||||
                    messages: List[Dict], capture_mode: str, capture_param: str,
 | 
			
		||||
                    start_time: datetime = None, end_time: datetime = None) -> bool:
 | 
			
		||||
        """Store an incident with related messages"""
 | 
			
		||||
        try:
 | 
			
		||||
            with self._get_connection() as conn:
 | 
			
		||||
                # Add incident record
 | 
			
		||||
                conn.execute("""
 | 
			
		||||
                    INSERT INTO incidents 
 | 
			
		||||
                    (id, reason, moderator_id, timestamp, capture_mode, capture_param, start_time, end_time)
 | 
			
		||||
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
                """, (
 | 
			
		||||
                    incident_id,
 | 
			
		||||
                    reason,
 | 
			
		||||
                    moderator_id,
 | 
			
		||||
                    datetime.now(),
 | 
			
		||||
                    capture_mode,
 | 
			
		||||
                    capture_param,
 | 
			
		||||
                    start_time,
 | 
			
		||||
                    end_time
 | 
			
		||||
                ))
 | 
			
		||||
 | 
			
		||||
                # Add incident messages
 | 
			
		||||
                for msg in messages:
 | 
			
		||||
                    conn.execute("""
 | 
			
		||||
                        INSERT INTO incident_messages
 | 
			
		||||
                        (incident_id, message_id, author_id, content, timestamp)
 | 
			
		||||
                        VALUES (?, ?, ?, ?, ?)
 | 
			
		||||
                    """, (
 | 
			
		||||
                        incident_id,
 | 
			
		||||
                        msg['id'],
 | 
			
		||||
                        msg['author_id'],
 | 
			
		||||
                        msg['content'],
 | 
			
		||||
                        msg['timestamp']
 | 
			
		||||
                    ))
 | 
			
		||||
 | 
			
		||||
                conn.commit()
 | 
			
		||||
                return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Failed to save incident: {str(e)}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def add_funny_moment(self, message_link: str, author_id: int, description: str = None) -> int:
 | 
			
		||||
        """Store a funny moment in database"""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute("""
 | 
			
		||||
                INSERT INTO funny_moments 
 | 
			
		||||
                (message_link, description, author_id, timestamp)
 | 
			
		||||
                VALUES (?, ?, ?, ?)
 | 
			
		||||
            """, (message_link, description, author_id, datetime.now()))
 | 
			
		||||
            conn.commit()
 | 
			
		||||
            return cursor.lastrowid
 | 
			
		||||
 | 
			
		||||
    def get_incident(self, incident_id: str) -> Optional[Dict]:
 | 
			
		||||
        """Retrieve an incident with its messages"""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            conn.row_factory = sqlite3.Row
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
 | 
			
		||||
            # Get incident details
 | 
			
		||||
            cursor.execute("SELECT * FROM incidents WHERE id = ?", (incident_id,))
 | 
			
		||||
            incident = cursor.fetchone()
 | 
			
		||||
            if not incident:
 | 
			
		||||
                return None
 | 
			
		||||
 | 
			
		||||
            # Convert timestamp string to datetime object
 | 
			
		||||
            incident_details = dict(incident)
 | 
			
		||||
            incident_details['timestamp'] = datetime.fromisoformat(incident_details['timestamp'])
 | 
			
		||||
 | 
			
		||||
            # Get related messages
 | 
			
		||||
            cursor.execute("SELECT * FROM incident_messages WHERE incident_id = ?", (incident_id,))
 | 
			
		||||
            messages = [
 | 
			
		||||
                {**dict(msg), 'timestamp': datetime.fromisoformat(msg['timestamp'])} 
 | 
			
		||||
                for msg in cursor.fetchall()
 | 
			
		||||
            ]
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                "details": incident_details,
 | 
			
		||||
                "messages": messages
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
    def get_recent_incidents(self, limit: int = 25):
 | 
			
		||||
        """Get all recent incidents (not limited to current moderator)"""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            conn.row_factory = sqlite3.Row
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute("""
 | 
			
		||||
                SELECT id, moderator_id FROM incidents
 | 
			
		||||
                ORDER BY timestamp DESC
 | 
			
		||||
                LIMIT ?
 | 
			
		||||
            """, (limit,))
 | 
			
		||||
            return [dict(row) for row in cursor.fetchall()]
 | 
			
		||||
 | 
			
		||||
    def add_followup(self, incident_id: str, moderator_id: int, notes: str) -> bool:
 | 
			
		||||
        """Add a follow-up report to an incident"""
 | 
			
		||||
        try:
 | 
			
		||||
            with self._get_connection() as conn:
 | 
			
		||||
                conn.execute("""
 | 
			
		||||
                    INSERT INTO incident_followups 
 | 
			
		||||
                    (incident_id, moderator_id, notes, timestamp)
 | 
			
		||||
                    VALUES (?, ?, ?, ?)
 | 
			
		||||
                """, (incident_id, moderator_id, notes, datetime.now()))
 | 
			
		||||
                conn.commit()
 | 
			
		||||
                return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Failed to add followup: {str(e)}")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def get_followups(self, incident_id: str) -> List[Dict]:
 | 
			
		||||
        """Retrieve follow-ups with proper timestamps"""
 | 
			
		||||
        with self._get_connection() as conn:
 | 
			
		||||
            conn.row_factory = sqlite3.Row
 | 
			
		||||
            cursor = conn.cursor()
 | 
			
		||||
            cursor.execute("SELECT * FROM incident_followups WHERE incident_id = ?", (incident_id,))
 | 
			
		||||
            return [
 | 
			
		||||
                {**dict(row), 'timestamp': datetime.fromisoformat(row['timestamp'])}
 | 
			
		||||
                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 = ""):
 | 
			
		||||
        """Log unauthorized command attempts"""
 | 
			
		||||
        try:
 | 
			
		||||
            with self._get_connection() as conn:
 | 
			
		||||
                conn.execute("""
 | 
			
		||||
                    INSERT INTO unauthorized_access 
 | 
			
		||||
                    (user_id, command_used, timestamp, details)
 | 
			
		||||
                    VALUES (?, ?, ?, ?)
 | 
			
		||||
                """, (user_id, command_used, datetime.now(), details))
 | 
			
		||||
                conn.commit()
 | 
			
		||||
                return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Failed to log unauthorized access: {e}")
 | 
			
		||||
            return False
 | 
			
		||||
		Reference in New Issue
	
	Block a user