modified: bot.py

This commit is contained in:
SimolZimol
2025-08-19 22:49:04 +02:00
parent 2097b2da11
commit 6ffcc3eede

397
bot.py
View File

@@ -1991,10 +1991,11 @@ async def modhelp(ctx):
embed.add_field(
name="👮 Moderator Commands (Level 5+)",
value=(
"`/warn <user> [reason]` - Warn a user\n"
"`/warn <user> [reason | message_id]` - Warn a user (with optional message reference)\n"
"`/mute <user> <duration> [reason]` - Mute a user temporarily\n"
"`/unmute <user>` - Manually unmute a user\n"
"`/modinfo [user]` - View comprehensive user information\n"
"`/viewwarn <warning_id>` - View detailed warning information\n"
"`/modstats [user]` - View moderation statistics\n"
"`/processes <action> [type]` - Manage active processes\n"
"`/startgiveaway` - Create server giveaways\n"
@@ -2046,12 +2047,13 @@ async def modhelp(ctx):
# Duration Formats
embed.add_field(
name="⏱️ Duration Formats",
name="⏱️ Command Examples",
value=(
"When using mute commands:\n"
"`10m` = 10 minutes\n"
"`1h` = 1 hour\n"
"`2d` = 2 days"
"**Mute duration formats:**\n"
"`10m` = 10 minutes, `1h` = 1 hour, `2d` = 2 days\n\n"
"**Warning with message reference:**\n"
"`/warn @user Inappropriate behavior | 1234567890123456789`\n"
"The message will be saved even if deleted later."
),
inline=False
)
@@ -2638,8 +2640,8 @@ async def log_moderation_action(guild, action_type, moderator, target_user, reas
except Exception as e:
logger.error(f"Error logging moderation action: {e}")
async def save_warning_to_database(user_id, guild_id, moderator_id, reason, timestamp=None):
"""Saves individual warning records to the database"""
async def save_warning_to_database(user_id, guild_id, moderator_id, reason, timestamp=None, message_data=None):
"""Saves individual warning records to the database with optional message data"""
connection = None
cursor = None
try:
@@ -2649,12 +2651,32 @@ async def save_warning_to_database(user_id, guild_id, moderator_id, reason, time
if timestamp is None:
timestamp = datetime.now()
# Prepare message data if provided
message_id = None
message_content = None
message_attachments = None
message_author_id = None
message_channel_id = None
if message_data:
message_id = message_data.get('id')
message_content = message_data.get('content')
message_attachments = message_data.get('attachments') # JSON string
message_author_id = message_data.get('author_id')
message_channel_id = message_data.get('channel_id')
insert_query = """
INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, created_at)
VALUES (%s, %s, %s, %s, %s)
INSERT INTO user_warnings (user_id, guild_id, moderator_id, reason, message_id,
message_content, message_attachments, message_author_id,
message_channel_id, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (user_id, guild_id, moderator_id, reason, timestamp))
cursor.execute(insert_query, (
user_id, guild_id, moderator_id, reason, message_id,
message_content, message_attachments, message_author_id,
message_channel_id, timestamp
))
connection.commit()
logger.info(f"Saved warning record for user {user_id} in guild {guild_id}")
@@ -2686,13 +2708,37 @@ def create_warnings_table():
guild_id BIGINT NOT NULL,
moderator_id BIGINT NOT NULL,
reason TEXT NOT NULL,
message_id BIGINT NULL,
message_content LONGTEXT NULL,
message_attachments LONGTEXT NULL,
message_author_id BIGINT NULL,
message_channel_id BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_guild (user_id, guild_id),
INDEX idx_created_at (created_at)
INDEX idx_created_at (created_at),
INDEX idx_message_id (message_id)
)
"""
cursor.execute(create_table_query)
# Add new columns if they don't exist (for existing databases)
alter_queries = [
"ALTER TABLE user_warnings ADD COLUMN message_id BIGINT NULL",
"ALTER TABLE user_warnings ADD COLUMN message_content LONGTEXT NULL",
"ALTER TABLE user_warnings ADD COLUMN message_attachments LONGTEXT NULL",
"ALTER TABLE user_warnings ADD COLUMN message_author_id BIGINT NULL",
"ALTER TABLE user_warnings ADD COLUMN message_channel_id BIGINT NULL",
"ALTER TABLE user_warnings ADD INDEX idx_message_id (message_id)"
]
for alter_query in alter_queries:
try:
cursor.execute(alter_query)
except Exception:
# Column already exists, ignore error
pass
connection.commit()
logger.info("User warnings table checked/created successfully")
@@ -2713,7 +2759,8 @@ async def get_user_warnings(user_id, guild_id):
cursor = connection.cursor()
select_query = """
SELECT id, moderator_id, reason, created_at
SELECT id, moderator_id, reason, created_at, message_id, message_content,
message_attachments, message_author_id, message_channel_id
FROM user_warnings
WHERE user_id = %s AND guild_id = %s
ORDER BY created_at DESC
@@ -2728,7 +2775,12 @@ async def get_user_warnings(user_id, guild_id):
"id": row[0],
"moderator_id": row[1],
"reason": row[2],
"created_at": row[3]
"created_at": row[3],
"message_id": row[4],
"message_content": row[5],
"message_attachments": row[6],
"message_author_id": row[7],
"message_channel_id": row[8]
})
return warnings
@@ -2742,6 +2794,75 @@ async def get_user_warnings(user_id, guild_id):
if connection:
close_database_connection(connection)
async def get_message_data(channel, message_id):
"""Retrieves and processes message data for warning documentation"""
try:
message = await channel.fetch_message(message_id)
# Process attachments
attachments_data = []
for attachment in message.attachments:
attachment_info = {
"filename": attachment.filename,
"url": attachment.url,
"proxy_url": attachment.proxy_url,
"size": attachment.size,
"content_type": attachment.content_type
}
# Download and encode image attachments for permanent storage
if attachment.content_type and attachment.content_type.startswith('image/'):
try:
import aiohttp
import base64
async with aiohttp.ClientSession() as session:
async with session.get(attachment.url) as response:
if response.status == 200 and len(await response.read()) < 8 * 1024 * 1024: # Max 8MB
image_data = await response.read()
attachment_info["data"] = base64.b64encode(image_data).decode('utf-8')
except Exception as e:
logger.warning(f"Could not download attachment {attachment.filename}: {e}")
attachments_data.append(attachment_info)
# Process embeds
embeds_data = []
for embed in message.embeds:
embed_info = {
"title": embed.title,
"description": embed.description,
"url": embed.url,
"color": embed.color.value if embed.color else None,
"timestamp": embed.timestamp.isoformat() if embed.timestamp else None
}
embeds_data.append(embed_info)
message_data = {
"id": message.id,
"content": message.content,
"author_id": message.author.id,
"channel_id": message.channel.id,
"attachments": json.dumps(attachments_data) if attachments_data else None,
"embeds": json.dumps(embeds_data) if embeds_data else None,
"created_at": message.created_at.isoformat(),
"edited_at": message.edited_at.isoformat() if message.edited_at else None,
"message_type": str(message.type),
"flags": message.flags.value if message.flags else 0
}
return message_data
except discord.NotFound:
logger.warning(f"Message {message_id} not found")
return None
except discord.Forbidden:
logger.warning(f"No permission to access message {message_id}")
return None
except Exception as e:
logger.error(f"Error retrieving message data: {e}")
return None
async def save_user_roles(user_id, guild_id, roles):
"""Saves a user's roles before a mute"""
connection = None
@@ -2816,9 +2937,42 @@ async def restore_user_roles(user, guild):
close_database_connection(connection)
@client.hybrid_command()
async def warn(ctx, user: discord.User, *, reason: str = "No reason provided"):
"""Warns a user (Requires Permission Level 5 or higher)"""
async def warn(ctx, user: discord.User, *, reason_and_message: str = "No reason provided"):
"""Warns a user (Requires Permission Level 5 or higher)
Usage:
/warn @user Inappropriate behavior
/warn @user Spam messages | 1234567890123456789
Format: reason | message_id (optional)
"""
try:
# Parse reason and optional message ID
reason_parts = reason_and_message.split(" | ")
reason = reason_parts[0].strip()
message_id = None
message_data = None
if len(reason_parts) > 1:
try:
message_id = int(reason_parts[1].strip())
# Try to get message data from current channel first
message_data = await get_message_data(ctx.channel, message_id)
# If not found in current channel, try other channels the bot can access
if message_data is None:
for channel in ctx.guild.text_channels:
try:
message_data = await get_message_data(channel, message_id)
if message_data is not None:
break
except discord.Forbidden:
continue
except ValueError:
await ctx.send("❌ Invalid message ID format. Use: `/warn @user reason | 1234567890123456789`", ephemeral=True)
return
# Load moderator data
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
@@ -2863,7 +3017,8 @@ async def warn(ctx, user: discord.User, *, reason: str = "No reason provided"):
user_id=user.id,
guild_id=ctx.guild.id,
moderator_id=ctx.author.id,
reason=reason
reason=reason,
message_data=message_data
)
# Get guild settings for threshold checking
@@ -2884,6 +3039,34 @@ async def warn(ctx, user: discord.User, *, reason: str = "No reason provided"):
if warning_id:
embed.add_field(name="🆔 Warning ID", value=str(warning_id), inline=True)
# Add message information if available
if message_data:
message_info = f"**Message ID:** `{message_data['id']}`\n"
message_info += f"**Channel:** <#{message_data['channel_id']}>\n"
if message_data['content']:
content_preview = message_data['content']
if len(content_preview) > 100:
content_preview = content_preview[:97] + "..."
message_info += f"**Content:** {content_preview}\n"
# Show attachment info
if message_data['attachments']:
attachments = json.loads(message_data['attachments'])
if attachments:
attachment_names = [att['filename'] for att in attachments[:3]]
message_info += f"**Attachments:** {', '.join(attachment_names)}"
if len(attachments) > 3:
message_info += f" +{len(attachments) - 3} more"
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
elif message_id:
embed.add_field(
name="⚠️ Message Not Found",
value=f"Could not retrieve message `{message_id}` (deleted or no permission)",
inline=False
)
embed.set_footer(text=f"User ID: {user.id}")
embed.set_thumbnail(url=user.display_avatar.url)
@@ -2929,16 +3112,25 @@ async def warn(ctx, user: discord.User, *, reason: str = "No reason provided"):
await ctx.send(embed=embed)
# Log the warning action
log_additional_info = {
"Warning Count": f"{target_data['warns']}/{warn_threshold}",
"Warning ID": str(warning_id) if warning_id else "N/A"
}
if message_data:
log_additional_info["Referenced Message"] = f"ID: {message_data['id']}"
log_additional_info["Message Channel"] = f"<#{message_data['channel_id']}>"
if message_data['content']:
content_preview = message_data['content'][:200] + "..." if len(message_data['content']) > 200 else message_data['content']
log_additional_info["Message Content"] = content_preview
await log_moderation_action(
guild=ctx.guild,
action_type="warn",
moderator=ctx.author,
target_user=user,
reason=reason,
additional_info={
"Warning Count": f"{target_data['warns']}/{warn_threshold}",
"Warning ID": str(warning_id) if warning_id else "N/A"
}
additional_info=log_additional_info
)
# Try to DM the user
@@ -3053,10 +3245,16 @@ async def mywarns(ctx):
# Truncate reason if too long
reason = record["reason"]
if len(reason) > 50:
reason = reason[:47] + "..."
if len(reason) > 40:
reason = reason[:37] + "..."
warning_history += f"`{warning_date}` - **{mod_name}**: {reason}\n"
warning_line = f"`{warning_date}` **{mod_name}**: {reason}"
# Add message indicator if warning was linked to a message
if record.get("message_id"):
warning_line += " 📄"
warning_history += warning_line + "\n"
if len(warning_records) > 5:
warning_history += f"\n*... and {len(warning_records) - 5} more warning(s)*"
@@ -3218,10 +3416,19 @@ async def modinfo(ctx, user: discord.User = None):
# Truncate reason if too long
reason = record["reason"]
if len(reason) > 60:
reason = reason[:57] + "..."
if len(reason) > 50:
reason = reason[:47] + "..."
warning_history += f"`{warning_date}` **{mod_name}**: {reason}\n"
warning_line = f"`{warning_date}` **{mod_name}**: {reason}"
# Add message indicator and content preview if available
if record.get("message_id"):
warning_line += " 📄"
if record.get("message_content"):
content_preview = record["message_content"][:30] + "..." if len(record["message_content"]) > 30 else record["message_content"]
warning_line += f"\n *Message: {content_preview}*"
warning_history += warning_line + "\n"
if len(warning_records) > 3:
warning_history += f"*... and {len(warning_records) - 3} more warning(s)*"
@@ -3287,6 +3494,142 @@ async def modinfo(ctx, user: discord.User = None):
)
await ctx.send(embed=embed)
@client.hybrid_command()
async def viewwarn(ctx, warning_id: int):
"""View detailed information about a specific warning (Requires Permission Level 5 or higher)"""
try:
# Load moderator data
mod_data = await load_user_data(ctx.author.id, ctx.guild.id)
# Check moderation rights
if not check_moderation_permission(mod_data["permission"]):
embed = discord.Embed(
title="❌ Insufficient Permissions",
description="You need moderation permissions (Level 5 or higher) to use this command.",
color=0xff0000
)
await ctx.send(embed=embed, ephemeral=True)
return
# Get warning details from database
connection = None
cursor = None
try:
connection = connect_to_database()
cursor = connection.cursor()
select_query = """
SELECT user_id, guild_id, moderator_id, reason, created_at, message_id,
message_content, message_attachments, message_author_id, message_channel_id
FROM user_warnings
WHERE id = %s AND guild_id = %s
"""
cursor.execute(select_query, (warning_id, ctx.guild.id))
result = cursor.fetchone()
if not result:
embed = discord.Embed(
title="❌ Warning Not Found",
description=f"No warning with ID {warning_id} found in this server.",
color=0xff0000
)
await ctx.send(embed=embed, ephemeral=True)
return
# Parse result
user_id, guild_id, moderator_id, reason, created_at, message_id, message_content, message_attachments, message_author_id, message_channel_id = result
# Get user and moderator objects
warned_user = await client.fetch_user(user_id)
moderator = await client.fetch_user(moderator_id)
# Create detailed embed
embed = discord.Embed(
title=f"⚠️ Warning Details - ID: {warning_id}",
color=0xff9500,
timestamp=created_at
)
embed.add_field(name="👤 Warned User", value=f"{warned_user.mention}\n`{warned_user.id}`", inline=True)
embed.add_field(name="👮 Moderator", value=f"{moderator.mention}\n`{moderator.id}`", inline=True)
embed.add_field(name="📅 Date", value=f"<t:{int(created_at.timestamp())}:F>", inline=True)
embed.add_field(name="📝 Reason", value=reason, inline=False)
# Add message information if available
if message_id:
message_info = f"**Message ID:** `{message_id}`\n"
if message_channel_id:
message_info += f"**Channel:** <#{message_channel_id}>\n"
if message_author_id:
try:
msg_author = await client.fetch_user(message_author_id)
message_info += f"**Author:** {msg_author.mention}\n"
except:
message_info += f"**Author ID:** `{message_author_id}`\n"
if message_content:
content_display = message_content
if len(content_display) > 500:
content_display = content_display[:497] + "..."
message_info += f"**Content:**\n```\n{content_display}\n```"
embed.add_field(name="📄 Referenced Message", value=message_info, inline=False)
# Handle attachments
if message_attachments:
try:
attachments = json.loads(message_attachments)
if attachments:
attachment_info = ""
for i, att in enumerate(attachments[:3]): # Show max 3 attachments
attachment_info += f"**{att['filename']}** ({att['size']} bytes)\n"
# Show image if available and encoded
if att.get('data') and att['content_type'].startswith('image/'):
try:
import base64
import io
# Create temporary file-like object
image_data = base64.b64decode(att['data'])
file = discord.File(io.BytesIO(image_data), filename=att['filename'])
# Send image separately if it's the first attachment
if i == 0:
await ctx.send(f"📎 **Attachment from Warning {warning_id}:**", file=file)
except Exception as e:
logger.warning(f"Could not display attachment: {e}")
if len(attachments) > 3:
attachment_info += f"*... and {len(attachments) - 3} more attachment(s)*"
embed.add_field(name="📎 Attachments", value=attachment_info, inline=False)
except Exception as e:
logger.error(f"Error processing attachments: {e}")
embed.set_thumbnail(url=warned_user.display_avatar.url)
embed.set_footer(text=f"Warning ID: {warning_id} | Guild: {ctx.guild.name}")
await ctx.send(embed=embed)
finally:
if cursor:
cursor.close()
if connection:
close_database_connection(connection)
except Exception as e:
logger.error(f"Error in viewwarn command: {e}")
embed = discord.Embed(
title="❌ Error",
description="An error occurred while retrieving warning details. Please try again.",
color=0xff0000
)
await ctx.send(embed=embed)
@client.hybrid_command()
async def mute(ctx, user: discord.User, duration: str, *, reason: str = "No reason provided"):
"""Mutes a user for a specified duration (Requires Permission Level 5 or higher)