Skip to content
Open

Dev #22

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 126 additions & 34 deletions cogs/Moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import random
import logging
from discord.ext import commands
from datetime import timedelta, datetime
import asyncio
import re
import datetime
from datetime import timedelta
from cogs.logging.logger import CogLogger
from utils.db import db
from utils.error_handler import ErrorHandler
import os
import json

def parse_duration(duration: str) -> timedelta | None:
"""Parse duration strings like '1h30m' into timedelta."""
Expand All @@ -19,6 +21,32 @@ def parse_duration(duration: str) -> timedelta | None:
time_params = {name: int(val) for name, val in match.groupdict(default='0').items()}
return timedelta(**time_params)

def save_warn_fallback(guild, user, moderator, reason, timestamp):
"""Fallback: Save warning to a JSON file if DB fails."""
data_dir = os.path.join(os.getcwd(), "data")
os.makedirs(data_dir, exist_ok=True)
warn_file = os.path.join(data_dir, f"warns_{guild.id}.json")
try:
if os.path.exists(warn_file):
with open(warn_file, "r", encoding="utf-8") as f:
warns = json.load(f)
else:
warns = {}
except Exception:
warns = {}

warns.setdefault(str(user.id), []).append({
"moderator": str(moderator.id),
"moderator_name": str(moderator),
"reason": reason,
"timestamp": timestamp.isoformat(),
"guild_name": guild.name,
"user_name": str(user)
})

with open(warn_file, "w", encoding="utf-8") as f:
json.dump(warns, f, indent=2)

class Moderation(commands.Cog, ErrorHandler):
def __init__(self, bot):
ErrorHandler.__init__(self)
Expand All @@ -29,7 +57,7 @@ def __init__(self, bot):

async def log_action(self, guild_id: int, embed: discord.Embed):
"""Log moderation action to configured channel"""
settings = await db.get_guild_settings(guild_id)
settings = db.get_guild_settings(str(guild_id))
if log_channel_id := settings.get("moderation", {}).get("log_channel"):
if channel := self.bot.get_channel(log_channel_id):
try:
Expand Down Expand Up @@ -69,15 +97,15 @@ async def timeout(self, ctx, member: discord.Member, duration: str, *, reason=No
embed = discord.Embed(
description=f"Timed out {member.mention} for `{duration}`\nReason: {reason or 'No reason provided'}",
color=discord.Color.orange(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been timed out in **{ctx.guild.name}** for `{duration}`.\nReason: {reason or 'No reason provided'}")
except discord.HTTPException as e:
await ctx.send(f"Failed to timeout: {e}")

@commands.command()
@commands.command(aliases=["uto"])
@commands.has_permissions(moderate_members=True)
async def untimeout(self, ctx, member: discord.Member, *, reason=None):
"""Remove timeout from a member
Expand All @@ -88,15 +116,15 @@ async def untimeout(self, ctx, member: discord.Member, *, reason=None):
embed = discord.Embed(
description=f"Removed timeout for {member.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.green(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"Your timeout was removed in **{ctx.guild.name}**.\nReason: {reason or 'No reason provided'}")
except discord.HTTPException as e:
await ctx.send(f"Failed to remove timeout: {e}")

@commands.command()
@commands.command(aliases=["k"])
@commands.has_permissions(kick_members=True)
async def kick(self, ctx, member: discord.Member, *, reason=None):
"""Kick a member from the server
Expand All @@ -110,15 +138,15 @@ async def kick(self, ctx, member: discord.Member, *, reason=None):
embed = discord.Embed(
description=f"Kicked {member.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.red(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been kicked from **{ctx.guild.name}**.\nReason: {reason or 'No reason provided'}")
except discord.HTTPException as e:
await ctx.send(f"Failed to kick: {e}")

@commands.command()
@commands.command(aliases=["b"])
@commands.has_permissions(ban_members=True)
async def ban(self, ctx, member: discord.Member, *, reason=None):
"""Ban a member from the server
Expand All @@ -132,15 +160,15 @@ async def ban(self, ctx, member: discord.Member, *, reason=None):
embed = discord.Embed(
description=f"Banned {member.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.dark_red(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been banned from **{ctx.guild.name}**.\nReason: {reason or 'No reason provided'}")
except discord.HTTPException as e:
await ctx.send(f"Failed to ban: {e}")

@commands.command()
@commands.command(aliases=["ub"])
@commands.has_permissions(ban_members=True)
async def unban(self, ctx, user: discord.User, *, reason=None):
"""Unban a user by ID or mention
Expand All @@ -151,14 +179,14 @@ async def unban(self, ctx, user: discord.User, *, reason=None):
embed = discord.Embed(
description=f"Unbanned {user.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.green(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
except discord.HTTPException as e:
await ctx.send(f"Failed to unban: {e}")

@commands.command()
@commands.command(aliases=["p"])
@commands.has_permissions(manage_messages=True)
async def purge(self, ctx, amount: int = 10):
"""Delete a number of messages (default 10, max 100)
Expand All @@ -170,12 +198,12 @@ async def purge(self, ctx, amount: int = 10):
embed = discord.Embed(
description=f"Purged {len(deleted)-1} messages.",
color=discord.Color.blurple(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed, delete_after=5)
await self.log_action(ctx.guild.id, embed)

@commands.command()
@commands.command(aliases=["m"])
@commands.has_permissions(manage_roles=True)
async def mute(self, ctx, member: discord.Member, *, reason=None):
"""Mute a member (adds Muted role)
Expand All @@ -193,13 +221,13 @@ async def mute(self, ctx, member: discord.Member, *, reason=None):
embed = discord.Embed(
description=f"Muted {member.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.dark_grey(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been muted in **{ctx.guild.name}**.\nReason: {reason or 'No reason provided'}")

@commands.command()
@commands.command(aliases=["um"])
@commands.has_permissions(manage_roles=True)
async def unmute(self, ctx, member: discord.Member, *, reason=None):
"""Unmute a member (removes Muted role)
Expand All @@ -211,15 +239,15 @@ async def unmute(self, ctx, member: discord.Member, *, reason=None):
embed = discord.Embed(
description=f"Unmuted {member.mention}\nReason: {reason or 'No reason provided'}",
color=discord.Color.green(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been unmuted in **{ctx.guild.name}**.")
else:
await ctx.send("User is not muted.")

@commands.command()
@commands.command(aliases=["sm"])
@commands.has_permissions(manage_messages=True)
async def slowmode(self, ctx, seconds: int = 0):
"""Set slowmode for the current channel (0 to disable)
Expand All @@ -231,12 +259,12 @@ async def slowmode(self, ctx, seconds: int = 0):
embed = discord.Embed(
description=f"Set slowmode to {seconds} seconds.",
color=discord.Color.blurple(),
timestamp=datetime.utcnow()
timestamp=datetime.datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)

@commands.command()
@commands.command(aliases=["w"])
@commands.has_permissions(moderate_members=True)
async def warn(self, ctx, member: discord.Member, *, reason=None):
"""Warn a member (logs warning)
Expand All @@ -245,12 +273,34 @@ async def warn(self, ctx, member: discord.Member, *, reason=None):
can, msg = self.can_act(ctx, member)
if not can:
return await ctx.send(msg)
# You can expand this to store warnings in your DB
timestamp = datetime.datetime.utcnow()
db_failed = False
try:
add_warn_func = getattr(db, "add_warn", None)
if callable(add_warn_func):
result = add_warn_func(ctx.guild.id, member.id, ctx.author.id, reason or "No reason provided", timestamp)
if asyncio.iscoroutine(result):
await result
else:
db_failed = True
except Exception as e:
db_failed = True
self.logger.error(f"DB warn failed: {e}")

if db_failed:
save_warn_fallback(ctx.guild, member, ctx.author, reason or "No reason provided", timestamp)

embed = discord.Embed(
description=f"Warned {member.mention}\nReason: {reason or 'No reason provided'}",
title="⚠️ Member Warned",
description=f"{member.mention} has been warned in **{ctx.guild.name}**.",
color=discord.Color.gold(),
timestamp=datetime.utcnow()
).set_footer(text=f"by {ctx.author}")
timestamp=timestamp
)
embed.add_field(name="User", value=f"{member} (`{member.id}`)", inline=True)
embed.add_field(name="Moderator", value=f"{ctx.author} (`{ctx.author.id}`)", inline=True)
embed.add_field(name="Reason", value=reason or "No reason provided", inline=False)
embed.set_footer(text=f"Guild: {ctx.guild.name}", icon_url=getattr(ctx.guild.icon, 'url', None))

await ctx.send(embed=embed)
await self.log_action(ctx.guild.id, embed)
await self.notify_user(member, f"You have been warned in **{ctx.guild.name}**.\nReason: {reason or 'No reason provided'}")
Expand All @@ -270,43 +320,85 @@ async def generic_error(self, ctx, error, command_name):

@timeout.error
async def timeout_error(self, ctx, error):
await self.generic_error(ctx, error, "timeout")
if isinstance(error, commands.MissingRequiredArgument):
if error.param.name == "member":
await ctx.reply(
"❌ You must mention a user to timeout!\n"
"Example: `.timeout @user 1h30m [reason]`"
)
elif error.param.name == "duration":
await ctx.reply(
"❌ You must provide a duration for the timeout!\n"
"Example: `.timeout @user 1h30m [reason]`\n"
"Duration examples: `10m`, `2h`, `1d`, `1h30m`"
)
else:
await self.generic_error(ctx, error, "timeout")
else:
await self.generic_error(ctx, error, "timeout")

@untimeout.error
async def untimeout_error(self, ctx, error):
await self.generic_error(ctx, error, "untimeout")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to untimeout!\nExample: `.untimeout @user [reason]`")
else:
await self.generic_error(ctx, error, "untimeout")

@kick.error
async def kick_error(self, ctx, error):
await self.generic_error(ctx, error, "kick")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to kick!\nExample: `.kick @user [reason]`")
else:
await self.generic_error(ctx, error, "kick")

@ban.error
async def ban_error(self, ctx, error):
await self.generic_error(ctx, error, "ban")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to ban!\nExample: `.ban @user [reason]`")
else:
await self.generic_error(ctx, error, "ban")

@unban.error
async def unban_error(self, ctx, error):
await self.generic_error(ctx, error, "unban")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "user":
await ctx.reply("❌ You must provide a user ID or mention to unban!\nExample: `.unban user_id [reason]`")
else:
await self.generic_error(ctx, error, "unban")

@purge.error
async def purge_error(self, ctx, error):
await self.generic_error(ctx, error, "purge")
if isinstance(error, commands.BadArgument):
await ctx.reply("❌ Please provide a valid number of messages to purge!\nExample: `.purge 25`")
else:
await self.generic_error(ctx, error, "purge")

@mute.error
async def mute_error(self, ctx, error):
await self.generic_error(ctx, error, "mute")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to mute!\nExample: `.mute @user [reason]`")
else:
await self.generic_error(ctx, error, "mute")

@unmute.error
async def unmute_error(self, ctx, error):
await self.generic_error(ctx, error, "unmute")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to unmute!\nExample: `.unmute @user [reason]`")
else:
await self.generic_error(ctx, error, "unmute")

@slowmode.error
async def slowmode_error(self, ctx, error):
await self.generic_error(ctx, error, "slowmode")
if isinstance(error, commands.BadArgument):
await ctx.reply("❌ Please provide a valid number of seconds!\nExample: `.slowmode 10`")
else:
await self.generic_error(ctx, error, "slowmode")

@warn.error
async def warn_error(self, ctx, error):
await self.generic_error(ctx, error, "warn")
if isinstance(error, commands.MissingRequiredArgument) and error.param.name == "member":
await ctx.reply("❌ You must mention a user to warn!\nExample: `.warn @user [reason]`")
else:
await self.generic_error(ctx, error, "warn")

async def setup(bot):
logger = CogLogger("Moderation")
Expand Down
5 changes: 5 additions & 0 deletions cogs/economy/fishing/AutoFishing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ def __init__(self, bot):
self.CATCH_CHANCE = 0.5
self.autofishing_task = self.bot.loop.create_task(self.autofishing_loop())

<<<<<<< HEAD
async def cog_unload(self):
"""Cancel the autofishing task when cog is unloaded"""
=======
def cog_unload(self):
>>>>>>> 8347f2e296550847d482727eb8e2f0210b51ae8b
if hasattr(self, 'autofishing_task'):
self.autofishing_task.cancel()

Expand Down
7 changes: 4 additions & 3 deletions cogs/fun/Other.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
from datetime import datetime
import pytz
from typing import Optional

TIMEZONE_FILE = "user_timezones.json"

Expand Down Expand Up @@ -73,9 +74,8 @@ async def removetimezone(self, ctx):
@removetimezone.error
async def removetimezone_error(self, ctx, error):
await ctx.reply(f"❌ Error: {error}")

@commands.command(aliases=["tz"])
async def timezone(self, ctx, user: discord.Member = None):
async def timezone(self, ctx, user: Optional[discord.Member] = None):
"""Show a user's current time (or your own)"""
try:
user = user or ctx.author
Expand All @@ -92,7 +92,8 @@ async def timezone(self, ctx, user: discord.Member = None):
)
await ctx.reply(embed=embed)
except Exception:
await ctx.reply(f"Timezone for {user.display_name} is invalid. Please set it again with `.settimezone <zone>`.")
name = user.display_name if user is not None else "This user"
await ctx.reply(f"Timezone for {name} is invalid. Please set it again with `.settimezone <zone>`.")

@timezone.error
async def timezone_error(self, ctx, error):
Expand Down