diff --git a/src/cogs/backups.py b/src/cogs/BackupsCog.py similarity index 100% rename from src/cogs/backups.py rename to src/cogs/BackupsCog.py diff --git a/src/cogs/economy.py b/src/cogs/EconomyCog.py similarity index 97% rename from src/cogs/economy.py rename to src/cogs/EconomyCog.py index fa9e109..721ed7c 100644 --- a/src/cogs/economy.py +++ b/src/cogs/EconomyCog.py @@ -1,240 +1,240 @@ -from typing import Union - -import disnake -from disnake import ( - Embed, - Localized, - CommandInteraction, - ui, - ButtonStyle, - MessageInteraction, - Color, - Member, - MessageCommandInteraction, -) -from disnake.ext import commands -from disnake.ext.commands import MemberConverter - -from src.utils import economy, Economy as EcoDB - - -class Buttons(ui.View): - def __init__( - self, - ctx: MessageCommandInteraction, - bot: commands.Bot, - receiver: Member, - money: int, - economy_data: EcoDB, - ) -> None: - super().__init__(timeout=20) - self.ctx = ctx - self.bot = bot - self.receiver = receiver - self.money = money - self.economy = economy_data - - @ui.button(emoji="✅", style=ButtonStyle.secondary, custom_id="test") - async def yes_callback(self, _: ui.Button, interaction: MessageInteraction) -> None: - await interaction.send(content="Please, wait...") - received_balance = await self.economy.get_balance(user_id=self.receiver.id) - new_received_balance = received_balance + self.money - old_bal_sender = await self.economy.get_balance(user_id=self.ctx.author.id) - new_sender_balance = old_bal_sender - self.money - print(new_received_balance, new_sender_balance) - try: - await self.economy.update_db( - {"id": self.receiver.id}, {"balance": new_received_balance} - ) - await self.economy.update_db( - {"id": self.ctx.author.id}, {"balance": new_sender_balance} - ) - await interaction.edit_original_response( - content="", - embed=Embed( - title="Successful", - description=f"You successfully transferred {self.receiver} {self.money} 🪙!", - color=Color.green(), - ), - ) - except (Exception, BaseException): - await interaction.edit_original_response( - content="", - embed=Embed( - title="Error", - description=f"Failed to transfer to {self.receiver} {self.money} 🪙", - color=Color.red(), - ), - ) - - @disnake.ui.button( - emoji="❌", - style=ButtonStyle.secondary, - custom_id="danger", - ) - async def no_callback(self, _, interaction): - await interaction.response.send_message("OK!") - - async def interaction_check(self, interaction: MessageInteraction) -> bool: - if interaction.user != self.ctx.author: - return await interaction.response.send_message( - "You cannot press these buttons!", ephemeral=True - ) - else: - return True - - async def on_timeout(self) -> None: - return - - -class Economy(commands.Cog): - """Economy commands""" - - EMOJI = "<:gift:1169690502635991241>" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - self.economy = economy - - async def cog_load(self) -> None: - await self.economy.fetch_and_cache_all() - - @commands.slash_command(description=Localized("test", key="test")) - async def balance(self, interaction: CommandInteraction): - if await self.economy.find_one({"id": interaction.author.id}) is None: - await self.economy.add_to_db( - {"id": interaction.author.id, "balance": 0, "bank": 0} - ) - - money = await self.economy.get_balance(interaction.author) - bank = await self.economy.get_bank(interaction.author) - total = money + bank - - await interaction.send( - embed=Embed( - title="Balance", - description=f"{interaction.author.name}, your balance:\n**Cash:** {money} 🪙\n**Bank:** {bank}🪙\n**Total:** {total}🪙", - ), - ephemeral=True, - ) - - @commands.slash_command( - name=Localized("bank", key="BANK_COMMAND_NAME"), - description=Localized("bank", key="BANK_COMMAND_DESC"), - ) - async def bank( - self, - interaction: disnake.MessageCommandInteraction, - money: int | str = "all", - ) -> None: - """Send money to the bank - - Arguments: number or "all" - """ - - # Check if user has an existing economy record, if not, add one - if await self.economy.find_one({"id": interaction.author.id}) is None: - await self.economy.add_to_db( - {"id": interaction.author.id, "balance": 0, "bank": 0} - ) - - # Get the current balance - current_money = await self.economy.get_balance(interaction.author) - - # If money value is "all", set it to current balance - if isinstance(money, str) and money == "all": - money = current_money - - # Check if money is a valid number - if not isinstance(money, int): - return await interaction.send( - 'Please enter a valid number or "all" argument to send 🪙 to bank!' - ) - - # Check if money is positive - if money <= 0: - return await interaction.send( - "You can't send negative or zero 🪙 to your bank!" - ) - - # Check if user has enough money - if current_money < money: - return await interaction.send("You don't have enough 🪙!") - - # Update the database with new bank and balance values - await self.economy.update_db( - {"id": interaction.author.id}, - { - "bank": current_money if money == current_money else money, - "balance": 0 if money == current_money else current_money - money, - }, - ) - - # Calculate bank, cash, and total values - bank = await self.economy.get_bank(interaction.author) - cash = current_money - money - total = cash + bank - - # Reply with balance information - await interaction.send( - embed=Embed( - title="Balance", - description=f"{interaction.author.name}, your balance now:\n**Cash:** {cash} 🪙\n**Bank:** {bank}🪙\n**Total:** {total}🪙", - ) - ) - - @commands.slash_command( - name=Localized("pay", key="PAY_COMMAND_NAME"), - description=Localized("", key="PAY_COMMAND_DESC"), - ) - async def pay( - self, - interaction: disnake.MessageCommandInteraction, - money: int = 0, - user: Union[int, str, Member] = None, - ): - if user is None: - return await interaction.send("Please specify the user (mention or id)") - - if money == 0: - return await interaction.send("Please specify money to transfer") - - elif await self.economy.get_balance(user_id=interaction.author.id) < money: - return await interaction.send( - "Not enough to transfer the money to the user" - ) - elif money > 9223372036854775807: - await interaction.send( - embed=Embed( - title="🚫 | Transfer error", - description="You want to transfer too much money!", - color=Color.red(), - ), - ) - elif money < 1: - return await interaction.send( - embed=Embed( - title="🚫 | Transfer error", - description="You can't transfer less than 1 🪙", - color=Color.red(), - ).set_footer(text=f"Command executed by {interaction.author}"), - ) # - else: - if isinstance(user, Member): - res = user.id - else: - res = await MemberConverter().convert(interaction, user) - await interaction.send( - f"Are you sure you want transfer {money} 🪙 to {res.mention}?", - view=Buttons( - ctx=interaction, - bot=self.bot, - receiver=res, - money=money, - economy_data=self.economy, - ), - ) - - -def setup(bot: commands.Bot) -> None: - bot.add_cog(Economy(bot=bot)) +from typing import Union + +import disnake +from disnake import ( + Embed, + Localized, + CommandInteraction, + ui, + ButtonStyle, + MessageInteraction, + Color, + Member, + MessageCommandInteraction, +) +from disnake.ext import commands +from disnake.ext.commands import MemberConverter + +from src.utils import economy, Economy as EcoDB + + +class Buttons(ui.View): + def __init__( + self, + ctx: MessageCommandInteraction, + bot: commands.Bot, + receiver: Member, + money: int, + economy_data: EcoDB, + ) -> None: + super().__init__(timeout=20) + self.ctx = ctx + self.bot = bot + self.receiver = receiver + self.money = money + self.economy = economy_data + + @ui.button(emoji="✅", style=ButtonStyle.secondary, custom_id="test") + async def yes_callback(self, _: ui.Button, interaction: MessageInteraction) -> None: + await interaction.send(content="Please, wait...") + received_balance = await self.economy.get_balance(user_id=self.receiver.id) + new_received_balance = received_balance + self.money + old_bal_sender = await self.economy.get_balance(user_id=self.ctx.author.id) + new_sender_balance = old_bal_sender - self.money + print(new_received_balance, new_sender_balance) + try: + await self.economy.update_db( + {"id": self.receiver.id}, {"balance": new_received_balance} + ) + await self.economy.update_db( + {"id": self.ctx.author.id}, {"balance": new_sender_balance} + ) + await interaction.edit_original_response( + content="", + embed=Embed( + title="Successful", + description=f"You successfully transferred {self.receiver} {self.money} 🪙!", + color=Color.green(), + ), + ) + except (Exception, BaseException): + await interaction.edit_original_response( + content="", + embed=Embed( + title="Error", + description=f"Failed to transfer to {self.receiver} {self.money} 🪙", + color=Color.red(), + ), + ) + + @disnake.ui.button( + emoji="❌", + style=ButtonStyle.secondary, + custom_id="danger", + ) + async def no_callback(self, _, interaction): + await interaction.response.send_message("OK!") + + async def interaction_check(self, interaction: MessageInteraction) -> bool: + if interaction.user != self.ctx.author: + return await interaction.response.send_message( + "You cannot press these buttons!", ephemeral=True + ) + else: + return True + + async def on_timeout(self) -> None: + return + + +class Economy(commands.Cog): + """Economy commands""" + + EMOJI = "<:gift:1169690502635991241>" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.economy = economy + + async def cog_load(self) -> None: + await self.economy.fetch_and_cache_all() + + @commands.slash_command(description=Localized("test", key="test")) + async def balance(self, interaction: CommandInteraction): + if await self.economy.find_one({"id": interaction.author.id}) is None: + await self.economy.add_to_db( + {"id": interaction.author.id, "balance": 0, "bank": 0} + ) + + money = await self.economy.get_balance(interaction.author) + bank = await self.economy.get_bank(interaction.author) + total = money + bank + + await interaction.send( + embed=Embed( + title="Balance", + description=f"{interaction.author.name}, your balance:\n**Cash:** {money} 🪙\n**Bank:** {bank}🪙\n**Total:** {total}🪙", + ), + ephemeral=True, + ) + + @commands.slash_command( + name=Localized("bank", key="BANK_COMMAND_NAME"), + description=Localized("bank", key="BANK_COMMAND_DESC"), + ) + async def bank( + self, + interaction: disnake.MessageCommandInteraction, + money: int | str = "all", + ) -> None: + """Send money to the bank + + Arguments: number or "all" + """ + + # Check if user has an existing economy record, if not, add one + if await self.economy.find_one({"id": interaction.author.id}) is None: + await self.economy.add_to_db( + {"id": interaction.author.id, "balance": 0, "bank": 0} + ) + + # Get the current balance + current_money = await self.economy.get_balance(interaction.author) + + # If money value is "all", set it to current balance + if isinstance(money, str) and money == "all": + money = current_money + + # Check if money is a valid number + if not isinstance(money, int): + return await interaction.send( + 'Please enter a valid number or "all" argument to send 🪙 to bank!' + ) + + # Check if money is positive + if money <= 0: + return await interaction.send( + "You can't send negative or zero 🪙 to your bank!" + ) + + # Check if user has enough money + if current_money < money: + return await interaction.send("You don't have enough 🪙!") + + # Update the database with new bank and balance values + await self.economy.update_db( + {"id": interaction.author.id}, + { + "bank": current_money if money == current_money else money, + "balance": 0 if money == current_money else current_money - money, + }, + ) + + # Calculate bank, cash, and total values + bank = await self.economy.get_bank(interaction.author) + cash = current_money - money + total = cash + bank + + # Reply with balance information + await interaction.send( + embed=Embed( + title="Balance", + description=f"{interaction.author.name}, your balance now:\n**Cash:** {cash} 🪙\n**Bank:** {bank}🪙\n**Total:** {total}🪙", + ) + ) + + @commands.slash_command( + name=Localized("pay", key="PAY_COMMAND_NAME"), + description=Localized("", key="PAY_COMMAND_DESC"), + ) + async def pay( + self, + interaction: disnake.MessageCommandInteraction, + money: int = 0, + user: Union[int, str, Member] = None, + ): + if user is None: + return await interaction.send("Please specify the user (mention or id)") + + if money == 0: + return await interaction.send("Please specify money to transfer") + + elif await self.economy.get_balance(user_id=interaction.author.id) < money: + return await interaction.send( + "Not enough to transfer the money to the user" + ) + elif money > 9223372036854775807: + await interaction.send( + embed=Embed( + title="🚫 | Transfer error", + description="You want to transfer too much money!", + color=Color.red(), + ), + ) + elif money < 1: + return await interaction.send( + embed=Embed( + title="🚫 | Transfer error", + description="You can't transfer less than 1 🪙", + color=Color.red(), + ).set_footer(text=f"Command executed by {interaction.author}"), + ) # + else: + if isinstance(user, Member): + res = user.id + else: + res = await MemberConverter().convert(interaction, user) + await interaction.send( + f"Are you sure you want transfer {money} 🪙 to {res.mention}?", + view=Buttons( + ctx=interaction, + bot=self.bot, + receiver=res, + money=money, + economy_data=self.economy, + ), + ) + + +def setup(bot: commands.Bot) -> None: + bot.add_cog(Economy(bot=bot)) diff --git a/src/cogs/forms.py b/src/cogs/FormsCog.py similarity index 100% rename from src/cogs/forms.py rename to src/cogs/FormsCog.py diff --git a/src/cogs/fun.py b/src/cogs/FunCog.py similarity index 100% rename from src/cogs/fun.py rename to src/cogs/FunCog.py diff --git a/src/cogs/giveaways.py b/src/cogs/GiveawaysCog.py similarity index 100% rename from src/cogs/giveaways.py rename to src/cogs/GiveawaysCog.py diff --git a/src/cogs/invitetracker.py b/src/cogs/InviteTrackerCog.py similarity index 100% rename from src/cogs/invitetracker.py rename to src/cogs/InviteTrackerCog.py diff --git a/src/cogs/logger.py b/src/cogs/LoggerCog.py similarity index 100% rename from src/cogs/logger.py rename to src/cogs/LoggerCog.py diff --git a/src/cogs/ModerationCog.py b/src/cogs/ModerationCog.py new file mode 100644 index 0000000..9f40270 --- /dev/null +++ b/src/cogs/ModerationCog.py @@ -0,0 +1,572 @@ +import asyncio +import datetime +import time +from typing import Union, List, Any + +import disnake +from disnake import Embed +from disnake.ext import commands +from disnake.ext.commands import UserConverter, MemberConverter +from disnake.ui import ActionRow + +from src.utils import warns +from src.utils.misc import emoji, str_to_seconds, hms + + +class Moderation(commands.Cog): + """Helper commands for server moderation""" + + EMOJI = "<:hammer:1169685339720384512>" + + def __init__(self, bot: commands.Bot) -> None: + super().__init__() + self.bot = bot + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + @commands.has_permissions(kick_members=True, ban_members=True) + async def ban(self, ctx, user: Union[int, str, disnake.Member], *, reason=None): + embed = disnake.Embed(color=0x2F3236) + + if isinstance(user, disnake.Member): + member = user.id + else: + member = await UserConverter().convert(ctx, str(user)) + + if member == ctx.author: + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + ErrorEmbed.description = f"{emoji('error')} | You can't ban yourself!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + else: + embed.title = "<:ban:1170712517308317756> Successfully banned" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Member:** {member.mention} ({member})\n" + f"**Reason:** {reason}" + ) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await ctx.send(embed=embed) + + try: + embed.title = "<:ban:1170712517308317756> You were banned" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Reason:** {reason}\n" + f"**Server:** {ctx.guild.name}" + ) + await member.send(embed=embed) + + except (Exception, BaseException, disnake.Forbidden): + pass + + await ctx.guild.ban(member, reason=f"{ctx.author}: {reason}") + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + @commands.has_permissions(kick_members=True, ban_members=True) + async def unban(self, ctx, id: int): + if isinstance(id, disnake.Member): + member = id + else: + member = await UserConverter().convert(ctx, str(id)) + + embed = disnake.Embed(color=0x2F3236) + embed.title = "<:invite:1169690514430382160> Successfully unbanned" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Member:** {member.mention} (`{id}`)" + ) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + + await ctx.guild.unban(member) + await ctx.send(embed=embed) + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + @commands.has_permissions(kick_members=True, ban_members=True) + async def kick(self, ctx, user: Union[int, str, disnake.Member], *, reason=None): + embed = disnake.Embed(color=0x2F3236) + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + if isinstance(user, disnake.Member): + member = user.id + else: + member = await MemberConverter().convert(ctx, str(user)) + + if member.top_role >= ctx.author.top_role: + + ErrorEmbed.description = f"{emoji('error')} | Your role is not higher than {member.mention}'s role!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.top_role >= ctx.guild.get_member(self.bot.user.id).top_role: + ErrorEmbed.description = f"{emoji('error')} | {member.mention}'s role is higher than mine, I can't kick him." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member == ctx.author: + ErrorEmbed.description = f"{emoji('error')} | You can't kick yourself!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.id == self.bot.user.id: + ErrorEmbed.description = f"{emoji('error')} | You can't kick me!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + else: + embed.title = "<:kick:1170712514288435271> Successfully kicked" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Member:** {member.mention} ({member})\n" + f"**Reason:** {reason}" + ) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await ctx.send(embed=embed) + + try: + embed.title = "<:kick:1170712514288435271> You were kicked" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Reason:** {reason}\n" + f"**Server:** {ctx.guild.name}" + ) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await member.send(embed=embed) + + except (Exception, BaseException, disnake.Forbidden): + pass + + await member.kick(reason=reason) + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + @commands.has_permissions(kick_members=True, ban_members=True) + async def mute(self, ctx, member: Union[int, str, disnake.Member], mute_time: str, *, reason=None): + embed = disnake.Embed(color=0x2F3236) + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + if isinstance(member, disnake.Member): + member = member.id + else: + member = await MemberConverter().convert(ctx, str(member)) + + try: + str_time = await str_to_seconds(mute_time) + except ValueError: + ErrorEmbed.description = f"{emoji('error')} | Invalid mute time format." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + if member == ctx.author: + ErrorEmbed.description = f"{emoji('error')} | You can't mute yourself!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.id == self.bot.user.id: + ErrorEmbed.description = f"{emoji('error')} | You can't mute me!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif str_time > 2419200: # 28 days in seconds + ErrorEmbed.description = f"{emoji('error')} | Mute time cannot be more than 28 days." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif str_time < 60: + ErrorEmbed.description = f"{emoji('error')} | Mute time cannot be less then 1 minute." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + else: + embed.title = "<:mute:1170712518725992529> Successfully muted" + embed.description = ( + f"**Member:** {member.mention} ({member})\n" + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Time:** {str(await hms(float(str_time)))}\n" + f"**Reason:** {reason}\n" + f"**Unmute date:** " + ) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + + await member.edit(timeout=disnake.utils.utcnow() + datetime.timedelta(seconds=str_time)) + await ctx.send(embed=embed) + + try: + embed.title = "<:mute:1170712518725992529> You were muted" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Server:** {ctx.guild.name}\n" + f"**Time:** {str(await hms(float(str_time)))}\n" + f"**Reason:** {reason}\n" + f"**Unmute date:** " + ) + embed.set_footer(text="Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await member.send(embed=embed) + + except (Exception, BaseException, disnake.Forbidden): + pass + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + @commands.has_permissions(kick_members=True, ban_members=True) + async def unmute(self, ctx, member: Union[int, str, disnake.Member]): + embed = disnake.Embed(color=0x2F3236) + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + if isinstance(member, disnake.Member): + member = member.id + else: + member = await MemberConverter().convert(ctx, str(member)) + + if member == ctx.author: + ErrorEmbed.description = f"{emoji('error')} | You can't unmute yourself!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + embed.title = "<:unmute:1169690521472614500> Successfully unmuted" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Member:** {member.mention} ({member})" + ) + embed.set_footer(text="Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + + await member.edit(timeout=None) + await ctx.send(embed=embed) + + try: + embed.title = "<:unmute:1169690521472614500> You were unmuted" + embed.description = ( + f"**Administrator:** {ctx.author.mention} ({ctx.author})\n" + f"**Server:** {ctx.guild.name}\n" + ) + embed.set_footer(text="Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await member.send(embed=embed) + + except (Exception, BaseException, disnake.Forbidden): + pass + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + async def mutes(self, ctx): + pages = [] + per_page = 5 + total = 0 + index = 0 + + for member in [m for m in ctx.guild.members if not m.bot and m.current_timeout is not None]: + total += 1 + unmuted_at = member.current_timeout + pages.append( + f"`[{total}]` **{member.mention}** - ()") + + if not pages: + embed = disnake.Embed(color=0x2F3136, description="There are currently no mutes on this server.") + embed.set_footer(text=ctx.author, icon_url=ctx.author.avatar) + return await ctx.send(embed=embed) + + total_pages = (total + per_page - 1) // per_page + + async def refresh_buttons(): + buttons = [ActionRow( + disnake.ui.Button(custom_id="back", emoji="◀️", style=disnake.ButtonStyle.blurple, disabled=index == 0), + disnake.ui.Button(custom_id="forward", emoji="▶️", style=disnake.ButtonStyle.blurple, + disabled=index == total_pages - 1), + disnake.ui.Button(custom_id="close", emoji="<:delete:1169690519677440093>", style=disnake.ButtonStyle.danger) + )] if total_pages > 1 else [ + ActionRow(disnake.ui.Button(custom_id="close", emoji="<:delete:1169690519677440093>", style=disnake.ButtonStyle.danger))] + return buttons + + async def refresh_embed(): + embed: Embed = disnake.Embed(title="<:calendar:1169690539168366712> Active mutes", color=0x2F3136) + if total_pages > 1: + embed.set_footer(text=f"{ctx.author} | Page {index + 1} of {total_pages}", icon_url=ctx.author.avatar) + else: + embed.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar) + for page in pages[index * per_page:(index + 1) * per_page]: + embed.add_field(name="", value=page, inline=False) + return embed + + embed = await refresh_embed() + msg = await ctx.send(embed=embed, components=await refresh_buttons()) + + def check(interaction: disnake.MessageInteraction) -> bool: + return interaction.message.id == msg.id and interaction.author == ctx.author + + while True: + try: + inter = await self.bot.wait_for('button_click', check=check, timeout=600) + except asyncio.TimeoutError: + break + + if inter.component.custom_id == "back": + index = max(index - 1, 0) + elif inter.component.custom_id == "forward": + index = min(index + 1, total_pages - 1) + elif inter.component.custom_id == "close": + break + + await msg.edit(embed=await refresh_embed(), components=await refresh_buttons()) + await inter.response.defer(ephemeral=True) + + await msg.delete() + + @commands.command() + @commands.cooldown(1, 5, commands.BucketType.guild) + async def bans(self, ctx): + pages = [] + per_page = 5 + total = 0 + index = 0 + + async for ban in ctx.guild.bans(limit=200): + if not ban.user.bot: + member = ban.user + reason = ban.reason if ban.reason else "No Reason" + total += 1 + pages.append(f"`[{total}]` **{member}** - {reason}") + + if not pages: + embed = disnake.Embed(color=0x2F3136, description="There are currently no bans on this server.") + embed.set_footer(text=ctx.author, icon_url=ctx.author.avatar) + return await ctx.send(embed=embed) + + total_pages = (total + per_page - 1) // per_page + + async def refresh_buttons(): + buttons = [ActionRow( + disnake.ui.Button(custom_id="back", emoji="◀️", style=disnake.ButtonStyle.blurple, disabled=index == 0), + disnake.ui.Button(custom_id="forward", emoji="▶️", style=disnake.ButtonStyle.blurple, + disabled=index == total_pages - 1), + disnake.ui.Button(custom_id="close", emoji="<:delete:1169690519677440093>", style=disnake.ButtonStyle.danger) + )] if total_pages > 1 else [ + ActionRow(disnake.ui.Button(custom_id="close", emoji="<:delete:1169690519677440093>", style=disnake.ButtonStyle.danger))] + return buttons + + async def refresh_embed(): + embed: Embed = disnake.Embed(title="<:calendar:1169690539168366712> Active bans", color=0x2F3136) + if total_pages > 1: + embed.set_footer(text=f"{ctx.author} | Page {index + 1} of {total_pages}", icon_url=ctx.author.avatar) + else: + embed.set_footer(text=f"{ctx.author}", icon_url=ctx.author.avatar) + for page in pages[index * per_page:(index + 1) * per_page]: + embed.add_field(name="", value=page, inline=False) + return embed + + embed = await refresh_embed() + msg = await ctx.send(embed=embed, components=await refresh_buttons()) + + def check(interaction: disnake.MessageInteraction) -> bool: + return interaction.message.id == msg.id and interaction.author == ctx.author + + while True: + try: + inter = await self.bot.wait_for('button_click', check=check, timeout=600) + except asyncio.TimeoutError: + break + + if inter.component.custom_id == "back": + index = max(index - 1, 0) + elif inter.component.custom_id == "forward": + index = min(index + 1, total_pages - 1) + elif inter.component.custom_id == "close": + break + + await msg.edit(embed=await refresh_embed(), components=await refresh_buttons()) + await inter.response.defer(ephemeral=True) + + await msg.delete() + + @commands.command() + @commands.cooldown(1, 20, commands.BucketType.guild) + @commands.has_permissions(administrator=True) + async def warn(self, ctx, user: Union[int, str, disnake.Member], *, reason=None): + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + if isinstance(user, disnake.Member): + member = user.id + else: + member = await MemberConverter().convert(ctx, str(user)) + + if member == ctx.author: + ErrorEmbed.description = f"{emoji('error')} | You can't warn yourself." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.bot: + ErrorEmbed.description = f"{emoji('error')} | You can't warn a bot." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.top_role >= ctx.author.top_role: + ErrorEmbed.description = f"{emoji('error')} | {member.mention}'s role is not lower than yours." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.id == self.bot.user.id: + ErrorEmbed.description = f"{emoji('error')} | You can't warn me" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.top_role >= ctx.guild.get_member(self.bot.user.id).top_role and not ctx.author.guild.owner: + ErrorEmbed.description = f"{emoji('error')} | {member.mention}'s role is higher than my role." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + case = await warns.add_warn(ctx.guild.id, ctx.author, member, reason) + + embed = disnake.Embed(title=f"<:icons_warning:1170751866905296978> Warned {member} (Case #{case})", color=0x2F3136) + embed.add_field(name="**Administrator:**", value=f"{ctx.author.mention} ({ctx.author})", inline=False) + embed.add_field(name="**Member:**", value=f"{member.mention} ({member})", inline=False) + embed.add_field(name="**Reason:**", value=reason, inline=False) + + embed.set_thumbnail(url=str(member.display_avatar.url)) + embed.set_footer(text="Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar.url) + await ctx.send(embed=embed) + + @commands.command() + @commands.cooldown(1, 20, commands.BucketType.guild) + @commands.has_permissions(administrator=True) + async def warns(self, ctx, member: Union[int, disnake.Member] = None): + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + if not member: + member = ctx.author + if member.bot: + ErrorEmbed.description = f"{emoji('error')} | You can't see bot's warnings" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + if isinstance(member, int): + member = await ctx.guild.fetch_member(member) + + warnings = await warns.get_user_warnings(ctx.guild.id, member) + + index = 0 + total_pages = (len(warnings) + 4) // 5 # Calculate the total number of pages + + async def refresh_buttons(index, total_pages): + prev_button = disnake.ui.Button( + label="️◀️", style=disnake.ButtonStyle.blurple, custom_id=f"warns_prev:{index}", + disabled=index == 0 + ) + next_button = disnake.ui.Button( + label="▶️", style=disnake.ButtonStyle.blurple, custom_id=f"warns_next:{index}", + disabled=index == total_pages - 1 + ) + delete_button = disnake.ui.Button( + style=disnake.ButtonStyle.red, + custom_id="delete_warns", + emoji="<:delete:1169690519677440093>" + ) + return [prev_button, next_button, delete_button] + + async def refresh_embed(ctx, warnings, index, total_pages): + embed = disnake.Embed(color=0x2F3136) + embed.title = f"Warns of {member}" + embed.description = f"**Total warns count:** {len(warnings)}" + embed.set_thumbnail(url=member.avatar) + + per_page = 5 + start = index * per_page + end = (index + 1) * per_page + + for i, warning in enumerate(warnings[start:end], start=start + 1): + timestamp = warning["timestamp"] + administrator = ctx.guild.get_member(int(warning["moderator_id"])) + reason = warning["reason"] + embed.add_field( + name=f"Warn #{i}", + value=f"Administrator: {administrator.mention}\n" + f"Timestamp: {timestamp}\n" # Display timestamp as is + f"Reason: {reason}", + inline=False + ) + return embed + + embed = await refresh_embed(ctx, warnings, index, total_pages) + buttons = await refresh_buttons(index, total_pages) + msg = await ctx.send(embed=embed, components=[disnake.ui.ActionRow(*buttons)]) + + def check(inter: disnake.MessageInteraction): + return inter.message.id == msg.id and inter.user == ctx.author + + while True: + try: + inter = await self.bot.wait_for('button_click', check=check, timeout=600) + except asyncio.TimeoutError: + break + + if inter.data.custom_id.startswith("warns_prev:"): + index = max(0, index - 1) + elif inter.data.custom_id.startswith("warns_next:"): + index = min(total_pages - 1, index + 1) + elif inter.data.custom_id == "delete_warns": + await msg.delete() + + embed = await refresh_embed(ctx, warnings, index, total_pages) + buttons = await refresh_buttons(index, total_pages) + await inter.response.edit_message(embed=embed, components=[disnake.ui.ActionRow(*buttons)]) + + @commands.command() + @commands.cooldown(1, 20, commands.BucketType.guild) + @commands.has_permissions(administrator=True) + async def unwarn(self, ctx, member: Union[int, disnake.Member] = None, amount: int = 1): + ErrorEmbed = disnake.Embed(color=disnake.Color.red()) + + member_id = member.id if isinstance(member, disnake.Member) else member + + if not isinstance(member_id, int): # Check if member_id is not an integer (an actual user) + member = await MemberConverter().convert(ctx, str(member_id)) + if member == ctx.author: + ErrorEmbed.description = f"{emoji('error')} | You can't remove your warns" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member.bot: + ErrorEmbed.description = f"{emoji('error')} | You can't remove warns from a bot" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + if not ctx.guild.get_member(member_id): # Check if the member is in the guild + ErrorEmbed.description = f"{emoji('error')} | Member not found in the guild" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + if ctx.guild.get_member(member_id).top_role >= ctx.author.top_role: + ErrorEmbed.description = f"{emoji('error')} | Your role is not higher than {ctx.guild.get_member(member_id).mention}'s role!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif member_id == self.bot.user.id: + ErrorEmbed.description = f"{emoji('error')} | You can't remove warns from me!" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + elif ctx.guild.get_member(self.bot.user.id).top_role >= ctx.guild.get_member( + member_id).top_role and not ctx.guild.owner: + ErrorEmbed.description = f"{emoji('error')} | {ctx.guild.get_member(member_id).mention}'s role is higher than mine, I can't remove warns." + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + count_deleted = await warns.delete_warnings(ctx.guild.id, member_id, amount) + if isinstance(count_deleted, int): + embed = disnake.Embed( + title=f"Removed {count_deleted} warns", + description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" + f"Member: {ctx.guild.get_member(member_id).mention} ({ctx.guild.get_member(member_id)})", + color=0x2F3136 + ) + embed.set_thumbnail(url=str(ctx.guild.get_member(member_id).display_avatar.url)) + embed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + await ctx.send(embed=embed) + else: + ErrorEmbed.description = f"{emoji('error')} | Sorry, I couldn't remove any warns" + ErrorEmbed.set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar) + return await ctx.send(embed=ErrorEmbed) + + +def setup(bot): + bot.add_cog(Moderation(bot)) diff --git a/src/cogs/settings.py b/src/cogs/SettingsCog.py similarity index 97% rename from src/cogs/settings.py rename to src/cogs/SettingsCog.py index cbf0891..bdf05a8 100644 --- a/src/cogs/settings.py +++ b/src/cogs/SettingsCog.py @@ -1,106 +1,106 @@ -from disnake import Message, Embed -from disnake.ext import commands - -from src.utils import main_db - - -# from disnake.ext.ipc import Server, ClientPayload - - -class Settings(commands.Cog): - """Helper commands to set up the bot.""" - - EMOJI = "<:settings:1169685352114552922>️" - - def __init__(self, bot) -> None: - self.bot = bot - self.settings_db = main_db - - async def cog_load(self) -> None: - await self.settings_db.fetch_and_cache_all() - - # @Server.route() - # async def get_prefix(self, data: ClientPayload) -> Dict[str, str]: - # prefix = await self.settings_db.get_prefix(data.guild_id) - # - # return { - # "message": f"Current prefix is {prefix}", - # "prefix": prefix, - # "status": "OK", - # } - # - # @Server.route() - # async def set_prefix(self, data: ClientPayload) -> Dict[str, str]: - # current_prefix = await self.settings_db.get_prefix(data.guild_id) - # - # if current_prefix == data.prefix: - # return { - # "message": f"Prefix already set to {data.prefix}", - # "prefix": current_prefix, - # "status": "ALREADY_IN_DB", - # } - # - # await self.settings_db.set_prefix(data.guild_id, data.prefix) - # return { - # "message": f"Successfully set prefix to {data.prefix}", - # "prefix": data.prefix, - # "status": "OK", - # } - - @commands.command() - async def set_prefix(self, ctx: commands.Context, prefix: str) -> Message: - """Set current prefix to another one""" - if prefix is None or prefix == "": - return await ctx.reply("Please enter a prefix!") - elif len(prefix) >= 5: - return await ctx.reply("Your prefix is too long!") - else: - await self.settings_db.set_prefix(ctx.guild.id, prefix) - return await ctx.reply(f"Successfully set prefix to {prefix}") - - @commands.command() - async def command_disable(self, ctx: commands.Context, command: str) -> Message: - """Disable command for this guild (required administrator privileges)""" - - # first, try to get command from name - command_name = ctx.bot.get_command(command) - if command_name is None: - # try to get command from alias - for cmd in ctx.bot.commands: - if command in cmd.aliases: - command_name = cmd - break - - if command_name and command_name != ctx.command: - if isinstance(command_name, commands.Group): - for command in command_name.commands: - await self.settings_db.add_command( - guild_id=ctx.guild.id, command=command.name - ) - await self.settings_db.add_command( - guild_id=ctx.guild.id, command=command_name.name - ) - return await ctx.reply( - embed=Embed( - title="Information", - description=f"Successfully disabled command " - f"{'group' if isinstance(command_name, commands.Group) else ''} " - f"{command_name.name}", - ) - ) - elif command_name == ctx.command: - return await ctx.reply( - embed=Embed( - title="Error", description=f"You can't disable this command" - ) - ) - else: - return await ctx.reply( - embed=Embed( - title="Error", description=f"Could not find command {command}" - ) - ) - - -def setup(bot: commands.Bot) -> None: - bot.add_cog(Settings(bot=bot)) +from disnake import Message, Embed +from disnake.ext import commands + +from src.utils import main_db + + +# from disnake.ext.ipc import Server, ClientPayload + + +class Settings(commands.Cog): + """Helper commands to set up the bot.""" + + EMOJI = "<:settings:1169685352114552922>️" + + def __init__(self, bot) -> None: + self.bot = bot + self.settings_db = main_db + + async def cog_load(self) -> None: + await self.settings_db.fetch_and_cache_all() + + # @Server.route() + # async def get_prefix(self, data: ClientPayload) -> Dict[str, str]: + # prefix = await self.settings_db.get_prefix(data.guild_id) + # + # return { + # "message": f"Current prefix is {prefix}", + # "prefix": prefix, + # "status": "OK", + # } + # + # @Server.route() + # async def set_prefix(self, data: ClientPayload) -> Dict[str, str]: + # current_prefix = await self.settings_db.get_prefix(data.guild_id) + # + # if current_prefix == data.prefix: + # return { + # "message": f"Prefix already set to {data.prefix}", + # "prefix": current_prefix, + # "status": "ALREADY_IN_DB", + # } + # + # await self.settings_db.set_prefix(data.guild_id, data.prefix) + # return { + # "message": f"Successfully set prefix to {data.prefix}", + # "prefix": data.prefix, + # "status": "OK", + # } + + @commands.command() + async def set_prefix(self, ctx: commands.Context, prefix: str) -> Message: + """Set current prefix to another one""" + if prefix is None or prefix == "": + return await ctx.reply("Please enter a prefix!") + elif len(prefix) >= 5: + return await ctx.reply("Your prefix is too long!") + else: + await self.settings_db.set_prefix(ctx.guild.id, prefix) + return await ctx.reply(f"Successfully set prefix to {prefix}") + + @commands.command() + async def command_disable(self, ctx: commands.Context, command: str) -> Message: + """Disable command for this guild (required administrator privileges)""" + + # first, try to get command from name + command_name = ctx.bot.get_command(command) + if command_name is None: + # try to get command from alias + for cmd in ctx.bot.commands: + if command in cmd.aliases: + command_name = cmd + break + + if command_name and command_name != ctx.command: + if isinstance(command_name, commands.Group): + for command in command_name.commands: + await self.settings_db.add_command( + guild_id=ctx.guild.id, command=command.name + ) + await self.settings_db.add_command( + guild_id=ctx.guild.id, command=command_name.name + ) + return await ctx.reply( + embed=Embed( + title="Information", + description=f"Successfully disabled command " + f"{'group' if isinstance(command_name, commands.Group) else ''} " + f"{command_name.name}", + ) + ) + elif command_name == ctx.command: + return await ctx.reply( + embed=Embed( + title="Error", description=f"You can't disable this command" + ) + ) + else: + return await ctx.reply( + embed=Embed( + title="Error", description=f"Could not find command {command}" + ) + ) + + +def setup(bot: commands.Bot) -> None: + bot.add_cog(Settings(bot=bot)) diff --git a/src/cogs/basic.py b/src/cogs/UtilityCog.py similarity index 99% rename from src/cogs/basic.py rename to src/cogs/UtilityCog.py index c55c2b6..f2aa2bb 100644 --- a/src/cogs/basic.py +++ b/src/cogs/UtilityCog.py @@ -12,7 +12,7 @@ startup = datetime.datetime.now() -class BasicUtility(commands.Cog): +class Utility(commands.Cog): """Utility commands""" EMOJI = "<:globe:1169690501063123065>" @@ -442,4 +442,4 @@ async def lock( def setup(bot: commands.Bot): - bot.add_cog(BasicUtility(bot=bot)) + bot.add_cog(Utility(bot=bot)) diff --git a/src/cogs/moderation.py b/src/cogs/moderation.py deleted file mode 100644 index 792e71f..0000000 --- a/src/cogs/moderation.py +++ /dev/null @@ -1,120 +0,0 @@ -import disnake -from disnake.ext import commands -from disnake.ext.commands import UserConverter, MemberConverter -from typing import Union - -from src.utils.misc import emoji - - -class Moderation(commands.Cog): - """Helper commands for server moderation""" - - EMOJI = "<:hammer:1169685339720384512>" - - def __init__(self, bot: commands.Bot) -> None: - super().__init__() - self.bot = bot - - @commands.command() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.has_permissions(kick_members=True, ban_members=True) - async def ban(self, ctx, user: Union[int, str, disnake.Member], *, reason=None): - if isinstance(user, disnake.Member): - member = user.id - else: - member = await UserConverter().convert(ctx, str(user)) - - if member == ctx.author: - return await ctx.send(mbed=disnake.Embed( - description=f"{emoji('error')} | You can't ban yourself!", - color=disnake.Color.red() - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - else: - await ctx.send(embed=disnake.Embed( - title="<:hammer:1169685339720384512> Successfully banned", - description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" - f"Member: {member.mention} ({member})\n" - f"Reason: {reason}", - color=0x2F3236 - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - try: - await member.send(embed=disnake.Embed( - title="<:hammer:1169685339720384512> You were banned", - description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" - f"Reason: {reason}" - f"Server: {ctx.guild.name}", - color=0x2F3236).set_footer(text=f"Synth © 2023 | All Rights Reserved", - icon_url=self.bot.user.avatar)) - except (Exception, BaseException, disnake.Forbidden): - pass - await ctx.guild.ban(member, reason=f"Administrator: {ctx.author}, Reason: {reason}") - - @commands.command() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.has_permissions(kick_members=True, ban_members=True) - async def unban(self, ctx, id: int): - if isinstance(id, disnake.Member): - member = id - else: - member = await UserConverter().convert(ctx, str(id)) - await ctx.guild.unban(member) - await ctx.send(embed=disnake.Embed( - title="<:hammer:1169685339720384512> Successfully unbanned", - description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" - f"Member: {member.mention} (`{id}`)", - color=0x2F3236 - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - - @commands.command() - @commands.cooldown(1, 5, commands.BucketType.guild) - @commands.has_permissions(kick_members=True, ban_members=True) - async def kick(self, ctx, user: Union[int, str, disnake.Member], *, reason=None): - if isinstance(user, disnake.Member): - member = user.id - else: - member = await MemberConverter().convert(ctx, str(user)) - - if member.top_role >= ctx.author.top_role: - return await ctx.send(embed=disnake.Embed( - description=f"{emoji('error')} | Your role is not higher than {member.mention}'s role!", - color=disnake.Color.red() - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - - elif member.top_role >= ctx.guild.get_member(self.bot.user.id).top_role: - return await ctx.send(embed=disnake.Embed( - description=f"{emoji('error')} | {member.mention}'s role is higher than mine, I can't kick him.", - color=disnake.Color.red() - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - elif member == ctx.author: - return await ctx.send(mbed=disnake.Embed( - description=f"{emoji('error')} | You can't kick yourself!", - color=disnake.Color.red() - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - elif member.id == self.bot.user.id: - return await ctx.send(embed=disnake.Embed( - description=f"{emoji('error')} | You can't kick me!", - color=disnake.Color.red() - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - else: - await member.kick(reason=reason) - await ctx.send(embed=disnake.Embed( - title="<:hammer:1169685339720384512> Successfully kicked", - description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" - f"Member: {member.mention} ({member})\n" - f"Reason: {reason}", - color=0x2F3236 - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - try: - await member.send(embed=disnake.Embed( - title="<:hammer:1169685339720384512> You were kicked", - description=f"Administrator: {ctx.author.mention} ({ctx.author})\n" - f"Reason: {reason}\n" - f"Server: {ctx.guild.name}", - color=0x2F3236 - ).set_footer(text=f"Synth © 2023 | All Rights Reserved", icon_url=self.bot.user.avatar)) - except (Exception, BaseException, disnake.Forbidden): - pass - - -def setup(bot): - bot.add_cog(Moderation(bot)) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index a06a1d2..613cd97 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -4,6 +4,7 @@ from .database.invite import InviteTrackerDatabase from .database.logger import LoggerDatabase from .database.tickets import TicketDatabase +from .database.warns import WarnDatabase economy = Economy("economy") main_db = MainDatabase("bot") @@ -12,3 +13,4 @@ logger = LoggerDatabase("logger") backups = BackupsDatabase("backups") invites = InviteTrackerDatabase("invites") +warns = WarnDatabase("warns") \ No newline at end of file diff --git a/src/utils/database/warns.py b/src/utils/database/warns.py new file mode 100644 index 0000000..1a790f1 --- /dev/null +++ b/src/utils/database/warns.py @@ -0,0 +1,99 @@ +from datetime import datetime +from typing import Union + +import disnake + +from .base import BaseDatabase + + +class WarnDatabase(BaseDatabase): + def __init__(self, database_name: str) -> None: + super().__init__(database_name=database_name) + + async def add_warn(self, guild_id: int, administrator: disnake.Member, user: disnake.Member, reason: str) -> int: + """ + Add a warning for a user in the database. + :param administrator: Administrator who gave the warn + :param guild_id: The ID of the guild where the warning is issued. + :param user: The disnake.Member object representing the user being warned. + :param reason: The reason for the warning. + :return: The case number of the warning. + """ + + # Fetch the existing warnings for the guild + warns_data = await self.find_one_from_db({"guild_id": guild_id}) + if not warns_data: + warns_data = { + "guild_id": guild_id, + "warnings": {} # A dictionary to store user warnings + } + + # Check if the user already has warnings, or initialize an empty list + user_warnings = warns_data["warnings"].get(str(user.id), []) + + # Get the current Unix timestamp (seconds since epoch) + timestamp = int(datetime.now().timestamp()) + + # Add the new warning to the user's list of warnings with a timestamp + new_case = { + "moderator_id": administrator.id, + "reason": reason, + "timestamp": timestamp + } + user_warnings.append(new_case) + + # Update the warnings for the user in the guild's data + warns_data["warnings"][str(user.id)] = user_warnings + + # Update the database with the modified guild data + await self.update_db({"guild_id": guild_id}, warns_data) + + # Return the case number of the new warning (index of the new warning in the user's list) + return len(user_warnings) + + async def get_user_warnings(self, guild_id: int, user: disnake.Member) -> list: + """ + Get the list of warnings for a specific user in a guild. + :param guild_id: The ID of the guild where the user's warnings are stored. + :param user: The disnake.Member object representing the user. + :return: A list of warning cases for the user (including reason and timestamp). + """ + # Fetch the existing warnings for the guild + warns_data = await self.find_one_from_db({"guild_id": guild_id}) + if not warns_data: + return [] # No warnings data for the guild + + # Check if the user has warnings, or return an empty list + user_warnings = warns_data["warnings"].get(str(user.id), []) + return user_warnings + + async def delete_warnings(self, guild_id: int, user: Union[int, disnake.Member], amount: int) -> int | None: + user_id = user.id if isinstance(user, disnake.Member) else user + + # Fetch the existing warnings for the guild + warns_data = await self.find_one({"guild_id": guild_id}, return_first_result=True) + if not warns_data: + return None # No warnings data for the guild + + # Check if the user has warnings, or return 0 + user_warnings = warns_data["warnings"].get(str(user_id), None) + if not user_warnings: + return 0 + + # Ensure we don't remove more warnings than available + if amount > len(user_warnings): + amount = len(user_warnings) + + # Remove the specified number of warnings + removed_warnings = user_warnings[:amount] + user_warnings = user_warnings[amount:] + + # Update the warnings for the user in the guild's data + warns_data["warnings"][str(user_id)] = user_warnings + + # Update the database with the modified guild data + await self.update_db({"guild_id": guild_id}, {"warnings": warns_data["warnings"]}) + + return len(removed_warnings) + + diff --git a/src/utils/help.py b/src/utils/help.py index 06f3b48..8feeaef 100644 --- a/src/utils/help.py +++ b/src/utils/help.py @@ -170,3 +170,5 @@ async def send_cog_help(self, cog: commands.Cog) -> None: await self.get_destination().send(embed=embed) send_group_help = send_command_help + + diff --git a/src/utils/misc.py b/src/utils/misc.py index 7dea413..b73a83c 100644 --- a/src/utils/misc.py +++ b/src/utils/misc.py @@ -1,6 +1,7 @@ import io from io import BytesIO from typing import List, Union, Literal +import re import ujson from disnake import ( @@ -52,8 +53,8 @@ async def is_command_disabled(message: Message, command: str) -> bool: async def check_channel( - channel: TextChannel, - interaction: Union[MessageCommandInteraction, commands.Context], + channel: TextChannel, + interaction: Union[MessageCommandInteraction, commands.Context], ) -> bool: await interaction.send( f"Checking access to channel {channel.mention}...", ephemeral=True @@ -137,3 +138,64 @@ class ConfirmEnum(StrEnum): FAIL = "confirm_no" +async def str_to_seconds(input_string): + units = { + "s": 1, + "с": 1, + "m": 60, + "х": 60, + "h": 3600, + "г": 3600, + "d": 86400, + "д": 86400, + "w": 604800, + "т": 604800, + "o": 2592000, + "м": 2592000, + "y": 31536000, + "р": 31536000 + } + + input_string = str(input_string).lower().strip('-').replace('мес', 'е').replace('mo', 'o') + + total_seconds = 0 + matches = re.findall(r'(\d+)\s*([a-zA-Zа-яА-Я]+)', input_string) + + for number, unit in matches: + multiplier = units.get(unit, 1) + total_seconds += int(number) * multiplier + + total_seconds = max(total_seconds, 0) + + return total_seconds + + +def word_correct(number, p1, p2, p3): + ld = str(number)[-2:] + cases = {"0": p3, "1": p1, "2": p2, "3": p2, "4": p2, "5": p3, "6": p3, "7": p3, "8": p3, "9": p3} + if ld[0] == "1" and len(ld) > 1: + case = p3 + else: + if len(ld) == 1: + case = cases.get(ld[0], p1) + else: + case = cases.get(ld[1], p2) + return case + + +async def hms(sec): + time_units = [(604800, "week", "weeks"), (86400, "day", "days"), (3600, "hour", "hours"), (60, "minute", "minutes"), + (1, "second", "seconds")] + + for unit, singular, plural in time_units: + if sec >= unit: + value = int(sec // unit) + word = word_correct(value, singular, plural, plural) + display = f'{value} {word}' + sec %= unit + if sec > 0: + display += ' ' + await hms(sec) + return display + + ms = int(sec * 1000) + return f'{ms} {word_correct(ms, "millisecond", "milliseconds", "milliseconds")}'