mirror of
https://github.com/Lilac-Rose/Lacie.git
synced 2026-07-18 16:43:23 -05:00
fuck my stupid chungus life
This commit is contained in:
parent
78544dee2c
commit
4aefafc404
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -7,4 +7,5 @@ venv/
|
|||
*/fonts/
|
||||
.txt
|
||||
*/databases/
|
||||
*/old_backups/
|
||||
*/old_backups/
|
||||
logs/
|
||||
1
bot.py
1
bot.py
|
|
@ -7,6 +7,7 @@ import glob
|
|||
import traceback
|
||||
from xp.database import get_db as get_xp_db
|
||||
from moderation.loader import ModerationBase
|
||||
from aiohttp import web
|
||||
|
||||
# Import sparkle DB to ensure it exists
|
||||
from sparkle.database import get_db as get_sparkle_db
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ COLOR_ROLE_NAMES = [
|
|||
"Turquoise", "Green Sea", "Emerald", "Nephritis", "River", "Belize",
|
||||
"Amethyst", "Wisteria", "Linen", "Alizarin", "Pomegranate", "Tangerine",
|
||||
"Rose", "Carrot", "Orange", "Sun Flower", "Pumpkin", "Light Gray",
|
||||
"Dark Air", "White"
|
||||
"Dark Air", "White-ish"
|
||||
]
|
||||
|
||||
FONTS_PATH = os.path.join(os.path.dirname(__file__), "..", "fonts")
|
||||
|
|
|
|||
233
events/status_alerts.py
Normal file
233
events/status_alerts.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""
|
||||
Status Monitor Cog
|
||||
Monitors the status_monitor database and sends DMs when services go down/recover
|
||||
"""
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
class StatusMonitor(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Path to the status monitor database
|
||||
self.db_path = Path("/home/lilacrose/lilacrose.dev2.0/monitor.db")
|
||||
# Your Discord user ID
|
||||
self.admin_user_id = 252130669919076352
|
||||
# Track last known status to detect changes
|
||||
self.last_status = {}
|
||||
# Start the monitoring loop
|
||||
self.check_status.start()
|
||||
|
||||
def cog_unload(self):
|
||||
"""Stop the monitoring loop when cog is unloaded"""
|
||||
self.check_status.cancel()
|
||||
|
||||
@tasks.loop(minutes=1)
|
||||
async def check_status(self):
|
||||
"""Check service status every minute"""
|
||||
try:
|
||||
# Check if database exists
|
||||
if not self.db_path.exists():
|
||||
return
|
||||
|
||||
# Connect to the status monitor database
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
# Get latest status for each service
|
||||
cur.execute("""
|
||||
SELECT service_name, status, error_message, timestamp
|
||||
FROM service_checks
|
||||
WHERE id IN (
|
||||
SELECT MAX(id)
|
||||
FROM service_checks
|
||||
GROUP BY service_name
|
||||
)
|
||||
""")
|
||||
|
||||
current_statuses = {row['service_name']: row for row in cur.fetchall()}
|
||||
conn.close()
|
||||
|
||||
# Get the admin user
|
||||
admin = await self.bot.fetch_user(self.admin_user_id)
|
||||
if not admin:
|
||||
return
|
||||
|
||||
# Check each service for status changes
|
||||
for service_name, row in current_statuses.items():
|
||||
current_status = row['status']
|
||||
error_msg = row['error_message']
|
||||
|
||||
# Get the service's display name
|
||||
service_display_names = {
|
||||
"website": "TERMINAL//FKLR-F23",
|
||||
"game_tracker": "GAME_TRACKER//FKLR-F23",
|
||||
"forms_server": "Lacie Bot Website",
|
||||
"file_server": "File Server",
|
||||
"discord_bot": "Lacie Bot"
|
||||
}
|
||||
display_name = service_display_names.get(service_name, service_name)
|
||||
|
||||
# If we haven't seen this service before, just store it
|
||||
if service_name not in self.last_status:
|
||||
self.last_status[service_name] = current_status
|
||||
continue
|
||||
|
||||
previous_status = self.last_status[service_name]
|
||||
|
||||
# Service went DOWN
|
||||
if current_status == "down" and previous_status != "down":
|
||||
embed = discord.Embed(
|
||||
title="🔴 Service Down Alert",
|
||||
description=f"**{display_name}** has gone offline",
|
||||
color=discord.Color.red(),
|
||||
timestamp=datetime.now(ZoneInfo("America/New_York"))
|
||||
)
|
||||
if error_msg:
|
||||
embed.add_field(name="Error", value=f"```{error_msg}```", inline=False)
|
||||
embed.add_field(name="Service ID", value=f"`{service_name}`", inline=True)
|
||||
embed.set_footer(text="Status Monitor Alert")
|
||||
|
||||
try:
|
||||
await admin.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Failed to send down alert for {service_name}: {e}")
|
||||
|
||||
# Service went DEGRADED
|
||||
elif current_status == "degraded" and previous_status == "up":
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Service Degraded",
|
||||
description=f"**{display_name}** is experiencing issues",
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(ZoneInfo("America/New_York"))
|
||||
)
|
||||
if error_msg:
|
||||
embed.add_field(name="Error", value=f"```{error_msg}```", inline=False)
|
||||
embed.add_field(name="Service ID", value=f"`{service_name}`", inline=True)
|
||||
embed.set_footer(text="Status Monitor Alert")
|
||||
|
||||
try:
|
||||
await admin.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Failed to send degraded alert for {service_name}: {e}")
|
||||
|
||||
# Service RECOVERED
|
||||
elif current_status == "up" and previous_status in ["down", "degraded"]:
|
||||
# Calculate downtime
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT started_at, ended_at, duration_seconds
|
||||
FROM downtime_incidents
|
||||
WHERE service_name = ? AND ended_at IS NOT NULL
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", (service_name,))
|
||||
|
||||
incident = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
embed = discord.Embed(
|
||||
title="✅ Service Recovered",
|
||||
description=f"**{display_name}** is back online",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.now(ZoneInfo("America/New_York"))
|
||||
)
|
||||
|
||||
if incident and incident['duration_seconds']:
|
||||
duration = incident['duration_seconds']
|
||||
if duration < 60:
|
||||
duration_str = f"{int(duration)} seconds"
|
||||
elif duration < 3600:
|
||||
duration_str = f"{int(duration / 60)} minutes"
|
||||
else:
|
||||
duration_str = f"{duration / 3600:.1f} hours"
|
||||
embed.add_field(name="Downtime", value=duration_str, inline=True)
|
||||
|
||||
embed.add_field(name="Service ID", value=f"`{service_name}`", inline=True)
|
||||
embed.set_footer(text="Status Monitor Alert")
|
||||
|
||||
try:
|
||||
await admin.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Failed to send recovery alert for {service_name}: {e}")
|
||||
|
||||
# Update last known status
|
||||
self.last_status[service_name] = current_status
|
||||
|
||||
except Exception as e:
|
||||
print(f"[StatusMonitor] Error checking status: {e}")
|
||||
|
||||
@check_status.before_loop
|
||||
async def before_check_status(self):
|
||||
"""Wait for bot to be ready before starting the loop"""
|
||||
await self.bot.wait_until_ready()
|
||||
print("[StatusMonitor] Starting status monitoring loop...")
|
||||
|
||||
# Initialize last_status with current state on startup
|
||||
try:
|
||||
if self.db_path.exists():
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT service_name, status
|
||||
FROM service_checks
|
||||
WHERE id IN (
|
||||
SELECT MAX(id)
|
||||
FROM service_checks
|
||||
GROUP BY service_name
|
||||
)
|
||||
""")
|
||||
|
||||
for row in cur.fetchall():
|
||||
self.last_status[row['service_name']] = row['status']
|
||||
|
||||
conn.close()
|
||||
print(f"[StatusMonitor] Initialized with {len(self.last_status)} services")
|
||||
except Exception as e:
|
||||
print(f"[StatusMonitor] Failed to initialize: {e}")
|
||||
|
||||
@commands.command(name="statustest")
|
||||
@commands.is_owner()
|
||||
async def status_test(self, ctx):
|
||||
"""Test the status monitoring alerts (Owner only)"""
|
||||
embed = discord.Embed(
|
||||
title="🔔 Status Monitor Test",
|
||||
description="This is a test alert from the status monitoring system",
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now(ZoneInfo("America/New_York"))
|
||||
)
|
||||
embed.add_field(name="Database Path", value=f"`{self.db_path}`", inline=False)
|
||||
embed.add_field(name="Database Exists", value="✅ Yes" if self.db_path.exists() else "❌ No", inline=True)
|
||||
embed.add_field(name="Monitoring", value=f"{len(self.last_status)} services", inline=True)
|
||||
embed.add_field(name="Admin User ID", value=f"`{self.admin_user_id}`", inline=True)
|
||||
|
||||
if self.last_status:
|
||||
status_list = "\n".join([f"• `{name}`: {status}" for name, status in self.last_status.items()])
|
||||
embed.add_field(name="Current Status", value=status_list, inline=False)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
# Try to DM the admin
|
||||
try:
|
||||
admin = await self.bot.fetch_user(self.admin_user_id)
|
||||
test_embed = discord.Embed(
|
||||
title="✅ DM Test Successful",
|
||||
description="If you're seeing this, status alerts will work!",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
await admin.send(embed=test_embed)
|
||||
await ctx.send("✅ Test DM sent successfully!")
|
||||
except Exception as e:
|
||||
await ctx.send(f"❌ Failed to send test DM: {e}")
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(StatusMonitor(bot))
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 533 KiB |
|
|
@ -1,10 +1,202 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext import commands, tasks
|
||||
from .loader import ModerationBase
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class InfractionCommand(ModerationBase):
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
super().__init__(bot)
|
||||
self.approval_channel_id = 1467901790333960377
|
||||
self.migrate_existing_infractions()
|
||||
self.check_auto_removals.start()
|
||||
|
||||
def cog_unload(self):
|
||||
"""Stop the background task and close DB when cog unloads."""
|
||||
self.check_auto_removals.cancel()
|
||||
super().cog_unload()
|
||||
|
||||
def migrate_existing_infractions(self):
|
||||
"""Add new columns to existing infractions table for auto-removal system."""
|
||||
try:
|
||||
# Add removed column (0 = active, 1 = removed)
|
||||
self.c.execute("ALTER TABLE infractions ADD COLUMN removed INTEGER DEFAULT 0")
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
|
||||
try:
|
||||
# Add removed_date column
|
||||
self.c.execute("ALTER TABLE infractions ADD COLUMN removed_date TEXT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Add removed_by column (moderator who approved removal)
|
||||
self.c.execute("ALTER TABLE infractions ADD COLUMN removed_by INTEGER")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Add skip_auto_removal column (1 = staff chose to keep it, skip future checks)
|
||||
self.c.execute("ALTER TABLE infractions ADD COLUMN skip_auto_removal INTEGER DEFAULT 0")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Add pending_approval column (1 = approval request already sent, waiting for staff decision)
|
||||
self.c.execute("ALTER TABLE infractions ADD COLUMN pending_approval INTEGER DEFAULT 0")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
@tasks.loop(hours=24)
|
||||
async def check_auto_removals(self):
|
||||
"""Background task that runs every 24 hours to check for eligible infraction removals."""
|
||||
try:
|
||||
# Get all guilds with active infractions
|
||||
self.c.execute("SELECT DISTINCT guild_id FROM infractions WHERE removed=0 AND skip_auto_removal=0 AND pending_approval=0")
|
||||
guilds = [row[0] for row in self.c.fetchall()]
|
||||
|
||||
for guild_id in guilds:
|
||||
# Get all users with active, non-skipped, non-pending infractions in this guild
|
||||
self.c.execute("""
|
||||
SELECT DISTINCT user_id
|
||||
FROM infractions
|
||||
WHERE guild_id=? AND removed=0 AND skip_auto_removal=0 AND pending_approval=0
|
||||
""", (guild_id,))
|
||||
users = [row[0] for row in self.c.fetchall()]
|
||||
|
||||
for user_id in users:
|
||||
await self.check_user_eligibility(guild_id, user_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in auto-removal check: {e}")
|
||||
|
||||
@check_auto_removals.before_loop
|
||||
async def before_check_auto_removals(self):
|
||||
"""Wait until the bot is ready before starting the loop."""
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
async def check_user_eligibility(self, guild_id: int, user_id: int):
|
||||
"""Check if a user's most recent infraction is eligible for removal."""
|
||||
try:
|
||||
# Get guild
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return
|
||||
|
||||
# Check if user is banned
|
||||
try:
|
||||
ban = await guild.fetch_ban(discord.Object(id=user_id))
|
||||
if ban:
|
||||
return # User is banned, skip
|
||||
except discord.NotFound:
|
||||
pass # User is not banned, continue
|
||||
except Exception:
|
||||
pass # Error checking ban, continue anyway
|
||||
|
||||
# Check if user is in the server
|
||||
member = guild.get_member(user_id)
|
||||
if not member:
|
||||
return # User not in server, skip
|
||||
|
||||
# Get the most recent ACTIVE infraction for this user (not removed, not skipped, not pending)
|
||||
self.c.execute("""
|
||||
SELECT id, timestamp, type, reason, moderator_id
|
||||
FROM infractions
|
||||
WHERE user_id=? AND guild_id=? AND removed=0 AND skip_auto_removal=0 AND pending_approval=0
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
""", (user_id, guild_id))
|
||||
|
||||
most_recent = self.c.fetchone()
|
||||
if not most_recent:
|
||||
return # No active infractions
|
||||
|
||||
inf_id, timestamp_str, inf_type, reason, mod_id = most_recent
|
||||
infraction_date = datetime.fromisoformat(timestamp_str)
|
||||
|
||||
# Check if 4 months have passed
|
||||
four_months_ago = datetime.utcnow() - timedelta(days=120)
|
||||
if infraction_date > four_months_ago:
|
||||
return # Not eligible yet
|
||||
|
||||
# Check if user got ANY infractions after this one
|
||||
self.c.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM infractions
|
||||
WHERE user_id=? AND guild_id=? AND timestamp > ?
|
||||
""", (user_id, guild_id, timestamp_str))
|
||||
|
||||
newer_infractions = self.c.fetchone()[0]
|
||||
|
||||
if newer_infractions > 0:
|
||||
return # User got infractions after this one, not eligible
|
||||
|
||||
# User is eligible! Send to staff for approval
|
||||
await self.send_removal_approval(guild_id, user_id, inf_id, inf_type, reason, timestamp_str, mod_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking eligibility for user {user_id} in guild {guild_id}: {e}")
|
||||
|
||||
async def send_removal_approval(self, guild_id: int, user_id: int, inf_id: int,
|
||||
inf_type: str, reason: str, timestamp: str, mod_id: int):
|
||||
"""Send an infraction removal request to the approval channel."""
|
||||
try:
|
||||
approval_channel = self.bot.get_channel(self.approval_channel_id)
|
||||
if not approval_channel:
|
||||
print(f"Approval channel {self.approval_channel_id} not found")
|
||||
return
|
||||
|
||||
# Get guild, user, and moderator info
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return
|
||||
|
||||
try:
|
||||
user = await self.bot.fetch_user(user_id)
|
||||
user_tag = f"{user.name}#{user.discriminator}"
|
||||
except:
|
||||
user_tag = f"Unknown User ({user_id})"
|
||||
|
||||
try:
|
||||
moderator = await self.bot.fetch_user(mod_id)
|
||||
mod_tag = f"{moderator.name}#{moderator.discriminator}"
|
||||
except:
|
||||
mod_tag = f"Unknown Mod ({mod_id})"
|
||||
|
||||
# Create approval embed
|
||||
embed = discord.Embed(
|
||||
title="🔔 Infraction Eligible for Auto-Removal",
|
||||
description=f"This user has stayed clean for 4 months. Should this infraction be removed?",
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.add_field(name="Server", value=guild.name, inline=False)
|
||||
embed.add_field(name="User", value=user_tag, inline=True)
|
||||
embed.add_field(name="Infraction ID", value=str(inf_id), inline=True)
|
||||
embed.add_field(name="Type", value=inf_type, inline=True)
|
||||
embed.add_field(name="Reason", value=reason or "None", inline=False)
|
||||
embed.add_field(name="Original Date", value=timestamp.replace("T", " ")[:19], inline=True)
|
||||
embed.add_field(name="Original Moderator", value=mod_tag, inline=True)
|
||||
embed.set_footer(text=f"User ID: {user_id} | Guild ID: {guild_id}")
|
||||
|
||||
# Create approval view
|
||||
view = InfractionRemovalView(self, inf_id, user_id, guild_id, user_tag, inf_type, reason, timestamp)
|
||||
|
||||
await approval_channel.send(embed=embed, view=view)
|
||||
|
||||
# Mark infraction as pending approval so we don't send duplicate requests
|
||||
self.c.execute("""
|
||||
UPDATE infractions
|
||||
SET pending_approval=1
|
||||
WHERE id=?
|
||||
""", (inf_id,))
|
||||
self.conn.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending removal approval: {e}")
|
||||
|
||||
@commands.command(name="inf")
|
||||
@ModerationBase.is_admin()
|
||||
async def inf(self, ctx, action: str, *args):
|
||||
|
|
@ -25,6 +217,33 @@ class InfractionCommand(ModerationBase):
|
|||
self.c.execute("""
|
||||
SELECT id, user_id, type, reason, moderator_id, timestamp
|
||||
FROM infractions
|
||||
WHERE user_id=? AND guild_id=? AND removed=0
|
||||
ORDER BY timestamp DESC
|
||||
""", (user_id, ctx.guild.id))
|
||||
results = self.c.fetchall()
|
||||
except Exception as e:
|
||||
await ctx.send(f"Database error: {e}")
|
||||
return
|
||||
|
||||
if not results:
|
||||
await ctx.send("This user has no active infractions.")
|
||||
return
|
||||
|
||||
elif action == "search_full":
|
||||
if not args:
|
||||
await ctx.send("You must provide a user ID to search.")
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = int(args[0])
|
||||
except ValueError:
|
||||
await ctx.send("Invalid user ID.")
|
||||
return
|
||||
|
||||
try:
|
||||
self.c.execute("""
|
||||
SELECT id, user_id, type, reason, moderator_id, timestamp, removed, removed_date
|
||||
FROM infractions
|
||||
WHERE user_id=? AND guild_id=?
|
||||
ORDER BY timestamp DESC
|
||||
""", (user_id, ctx.guild.id))
|
||||
|
|
@ -37,13 +256,98 @@ class InfractionCommand(ModerationBase):
|
|||
await ctx.send("This user has no infractions.")
|
||||
return
|
||||
|
||||
# Build full history rows with status
|
||||
ids_to_cache = set(row[1] for row in results) | set(row[4] for row in results)
|
||||
user_cache = {u.id: u for u in self.bot.users if u.id in ids_to_cache}
|
||||
|
||||
rows = []
|
||||
for row in results:
|
||||
try:
|
||||
user = user_cache.get(row[1]) or await self.bot.fetch_user(int(row[1]))
|
||||
moderator = user_cache.get(row[4]) or await self.bot.fetch_user(int(row[4]))
|
||||
user_cache[user.id] = user
|
||||
user_cache[moderator.id] = moderator
|
||||
except Exception as e:
|
||||
await ctx.send(f"User fetch error: {e}")
|
||||
return
|
||||
|
||||
user_tag = f"{user.name}#{user.discriminator}"
|
||||
mod_tag = f"{moderator.name}#{moderator.discriminator}"
|
||||
timestamp = row[5].replace("T", " ")[:19]
|
||||
reason = row[3] or "None"
|
||||
|
||||
# Check if removed
|
||||
is_removed = row[6] if len(row) > 6 else 0
|
||||
removed_date = row[7] if len(row) > 7 and row[7] else ""
|
||||
|
||||
if is_removed:
|
||||
status = f"Removed ({removed_date.replace('T', ' ')[:19]})" if removed_date else "Removed"
|
||||
else:
|
||||
status = "Active"
|
||||
|
||||
rows.append({
|
||||
"id": str(row[0]),
|
||||
"user": user_tag,
|
||||
"moderator": mod_tag,
|
||||
"timestamp": timestamp,
|
||||
"type": row[2],
|
||||
"reason": reason,
|
||||
"status": status
|
||||
})
|
||||
|
||||
# Prepare table
|
||||
widths = {key: max(len(key), *(len(r[key]) for r in rows)) for key in rows[0].keys()}
|
||||
header = " | ".join(f"{key.capitalize():{widths[key]}}" for key in rows[0].keys())
|
||||
separator = "-" * len(header)
|
||||
|
||||
# Build pages
|
||||
chunk_size = 1800
|
||||
pages = []
|
||||
current_chunk = [header, separator]
|
||||
char_count = len("```md\n") + len(header) + len(separator) + 2
|
||||
|
||||
for r in rows:
|
||||
line = " | ".join(f"{r[key]:{widths[key]}}" for key in r.keys())
|
||||
line_len = len(line) + 1
|
||||
if char_count + line_len > chunk_size:
|
||||
pages.append("```md\n" + "\n".join(current_chunk) + "\n```")
|
||||
current_chunk = [header, separator]
|
||||
char_count = len("```md\n") + len(header) + len(separator) + 2
|
||||
current_chunk.append(line)
|
||||
char_count += line_len
|
||||
|
||||
if current_chunk:
|
||||
pages.append("```md\n" + "\n".join(current_chunk) + "\n```")
|
||||
|
||||
# Pagination with buttons
|
||||
class PageView(discord.ui.View):
|
||||
def __init__(self, pages):
|
||||
super().__init__(timeout=180)
|
||||
self.pages = pages
|
||||
self.current = 0
|
||||
|
||||
async def update_message(self, interaction):
|
||||
await interaction.response.edit_message(content=self.pages[self.current], view=self)
|
||||
|
||||
@discord.ui.button(label="Previous", style=discord.ButtonStyle.blurple)
|
||||
async def previous(self, button: discord.ui.Button, interaction: discord.Interaction):
|
||||
self.current = (self.current - 1) % len(self.pages)
|
||||
await self.update_message(interaction)
|
||||
|
||||
@discord.ui.button(label="Next", style=discord.ButtonStyle.blurple)
|
||||
async def next(self, button: discord.ui.Button, interaction: discord.Interaction):
|
||||
self.current = (self.current + 1) % len(self.pages)
|
||||
await self.update_message(interaction)
|
||||
|
||||
await ctx.send(content=pages[0], view=PageView(pages))
|
||||
return
|
||||
|
||||
elif action == "list":
|
||||
try:
|
||||
self.c.execute("""
|
||||
SELECT id, user_id, type, reason, moderator_id, timestamp
|
||||
FROM infractions
|
||||
WHERE guild_id=?
|
||||
WHERE guild_id=? AND removed=0
|
||||
ORDER BY timestamp DESC
|
||||
""", (ctx.guild.id,))
|
||||
results = self.c.fetchall()
|
||||
|
|
@ -52,7 +356,7 @@ class InfractionCommand(ModerationBase):
|
|||
return
|
||||
|
||||
if not results:
|
||||
await ctx.send("No infractions found in this server.")
|
||||
await ctx.send("No active infractions found in this server.")
|
||||
return
|
||||
|
||||
elif action == "delete":
|
||||
|
|
@ -66,16 +370,54 @@ class InfractionCommand(ModerationBase):
|
|||
await ctx.send("Invalid infraction ID.")
|
||||
return
|
||||
|
||||
# Fetch infraction details before deleting
|
||||
self.c.execute("""
|
||||
SELECT user_id, type, reason, timestamp
|
||||
FROM infractions
|
||||
WHERE id=? AND guild_id=?
|
||||
""", (inf_id, ctx.guild.id))
|
||||
infraction = self.c.fetchone()
|
||||
|
||||
if not infraction:
|
||||
await ctx.send(f"Infraction {inf_id} not found.")
|
||||
return
|
||||
|
||||
user_id, inf_type, reason, timestamp = infraction
|
||||
|
||||
# Delete the infraction (actually delete it, not just mark as removed)
|
||||
self.c.execute("DELETE FROM infractions WHERE id=? AND guild_id=?", (inf_id, ctx.guild.id))
|
||||
self.conn.commit()
|
||||
await ctx.send(f"Infraction {inf_id} deleted.")
|
||||
|
||||
# Send DM to the user
|
||||
try:
|
||||
user = await self.bot.fetch_user(user_id)
|
||||
embed = discord.Embed(
|
||||
title="Infraction Removed",
|
||||
description=f"An infraction has been removed from your record in **{ctx.guild.name}**.",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.add_field(name="Infraction ID", value=str(inf_id), inline=True)
|
||||
embed.add_field(name="Type", value=inf_type, inline=True)
|
||||
embed.add_field(name="Original Reason", value=reason or "None", inline=False)
|
||||
embed.add_field(name="Original Date", value=timestamp.replace("T", " ")[:19], inline=True)
|
||||
embed.add_field(name="Removed By", value=f"{ctx.author.name}#{ctx.author.discriminator}", inline=True)
|
||||
embed.set_footer(text=f"Server: {ctx.guild.name}")
|
||||
|
||||
await user.send(embed=embed)
|
||||
await ctx.send(f"Infraction {inf_id} deleted and user notified.")
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"Infraction {inf_id} deleted, but couldn't DM the user (DMs disabled or blocked).")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Infraction {inf_id} deleted, but failed to notify user: {e}")
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
await ctx.send("Unknown action. Use search, list, or delete.")
|
||||
await ctx.send("Unknown action. Use search, search_full, list, or delete.")
|
||||
return
|
||||
|
||||
# Cache users to avoid repeated API calls
|
||||
# Cache users to avoid repeated API calls (for search and list)
|
||||
ids_to_cache = set(row[1] for row in results) | set(row[4] for row in results)
|
||||
user_cache = {u.id: u for u in self.bot.users if u.id in ids_to_cache}
|
||||
|
||||
|
|
@ -152,5 +494,101 @@ class InfractionCommand(ModerationBase):
|
|||
await ctx.send(content=pages[0], view=PageView(pages))
|
||||
|
||||
|
||||
class InfractionRemovalView(discord.ui.View):
|
||||
"""View for approving or denying infraction auto-removals."""
|
||||
|
||||
def __init__(self, cog, inf_id, user_id, guild_id, user_tag, inf_type, reason, timestamp):
|
||||
super().__init__(timeout=None)
|
||||
self.cog = cog
|
||||
self.inf_id = inf_id
|
||||
self.user_id = user_id
|
||||
self.guild_id = guild_id
|
||||
self.user_tag = user_tag
|
||||
self.inf_type = inf_type
|
||||
self.reason = reason
|
||||
self.timestamp = timestamp
|
||||
|
||||
@discord.ui.button(label="Remove Infraction", style=discord.ButtonStyle.green, custom_id="approve_removal")
|
||||
async def approve_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
"""Approve the removal - mark infraction as removed."""
|
||||
try:
|
||||
# Mark infraction as removed and clear pending flag
|
||||
self.cog.c.execute("""
|
||||
UPDATE infractions
|
||||
SET removed=1, removed_date=?, removed_by=?, pending_approval=0
|
||||
WHERE id=?
|
||||
""", (datetime.utcnow().isoformat(), interaction.user.id, self.inf_id))
|
||||
self.cog.conn.commit()
|
||||
|
||||
# Update embed
|
||||
embed = discord.Embed(
|
||||
title="✅ Infraction Removed",
|
||||
description="This infraction has been removed from the user's active record.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
embed.add_field(name="User", value=self.user_tag, inline=True)
|
||||
embed.add_field(name="Infraction ID", value=str(self.inf_id), inline=True)
|
||||
embed.add_field(name="Type", value=self.inf_type, inline=True)
|
||||
embed.add_field(name="Original Date", value=self.timestamp.replace("T", " ")[:19], inline=True)
|
||||
embed.add_field(name="Removed By", value=interaction.user.mention, inline=True)
|
||||
embed.add_field(name="Removed At", value=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), inline=True)
|
||||
|
||||
await interaction.response.edit_message(embed=embed, view=None)
|
||||
|
||||
# Notify user
|
||||
try:
|
||||
user = await self.cog.bot.fetch_user(self.user_id)
|
||||
guild = self.cog.bot.get_guild(self.guild_id)
|
||||
|
||||
notify_embed = discord.Embed(
|
||||
title="✅ Infraction Removed",
|
||||
description=f"Great news! An infraction has been removed from your record in **{guild.name}** for good behavior.",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
notify_embed.add_field(name="Infraction ID", value=str(self.inf_id), inline=True)
|
||||
notify_embed.add_field(name="Type", value=self.inf_type, inline=True)
|
||||
notify_embed.add_field(name="Original Reason", value=self.reason or "None", inline=False)
|
||||
notify_embed.add_field(name="Original Date", value=self.timestamp.replace("T", " ")[:19], inline=True)
|
||||
notify_embed.set_footer(text=f"You stayed clean for 4 months! Keep up the good behavior.")
|
||||
|
||||
await user.send(embed=notify_embed)
|
||||
except:
|
||||
pass # User has DMs disabled or bot can't reach them
|
||||
|
||||
except Exception as e:
|
||||
await interaction.response.send_message(f"Error removing infraction: {e}", ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="Keep Infraction", style=discord.ButtonStyle.red, custom_id="deny_removal")
|
||||
async def deny_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
"""Deny the removal - mark to skip future auto-removal checks."""
|
||||
try:
|
||||
# Mark infraction to skip future checks and clear pending flag
|
||||
self.cog.c.execute("""
|
||||
UPDATE infractions
|
||||
SET skip_auto_removal=1, pending_approval=0
|
||||
WHERE id=?
|
||||
""", (self.inf_id,))
|
||||
self.cog.conn.commit()
|
||||
|
||||
# Update embed
|
||||
embed = discord.Embed(
|
||||
title="❌ Removal Denied",
|
||||
description="This infraction will remain active and will not be checked for auto-removal again.",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
embed.add_field(name="User", value=self.user_tag, inline=True)
|
||||
embed.add_field(name="Infraction ID", value=str(self.inf_id), inline=True)
|
||||
embed.add_field(name="Type", value=self.inf_type, inline=True)
|
||||
embed.add_field(name="Original Date", value=self.timestamp.replace("T", " ")[:19], inline=True)
|
||||
embed.add_field(name="Decision By", value=interaction.user.mention, inline=True)
|
||||
embed.add_field(name="Decision At", value=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), inline=True)
|
||||
|
||||
await interaction.response.edit_message(embed=embed, view=None)
|
||||
|
||||
except Exception as e:
|
||||
await interaction.response.send_message(f"Error denying removal: {e}", ephemeral=True)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(InfractionCommand(bot))
|
||||
await bot.add_cog(InfractionCommand(bot))
|
||||
|
|
@ -23,7 +23,7 @@ class XPSync(commands.Cog):
|
|||
async def sync_roles_for_user(self, member: discord.Member) -> tuple[int, list[str]]:
|
||||
"""Sync roles for a member based on their lifetime XP level."""
|
||||
config = load_config()
|
||||
ROLE_REWARDS = {int(k): int(v) for k, v in config["ROLE_REWARDS"].items()}
|
||||
ROLE_REWARDS = {int(k): int(v) for k, v in config["role_rewards"].items()}
|
||||
|
||||
# Run database operation in executor to avoid blocking
|
||||
def get_level():
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user