# pylint: disable=cyclic-import from datetime import datetime, timedelta from typing import Union from discord import Color, Embed, Guild, Interaction, Member, Message, Role, User from redbot.core import commands from redbot.core.bot import Red from redbot.core.utils.chat_formatting import bold, box, error, humanize_timedelta, warning from ..models.moderation import Moderation from ..models.partials import PartialUser from ..models.type import Type from .config import config from .utils import get_bool_emoji, get_pagesize_str async def message_factory( bot: Red, color: Color, guild: Guild, reason: str, moderation_type: Type, moderator: Union[Member, User] | None = None, duration: timedelta | None = None, response: Message | None = None, case: bool = True, ) -> Embed: """This function creates a message from set parameters, meant for contacting the moderated user. Args: bot (Red): The bot instance. color (Color): The color of the embed. guild (Guild): The guild the moderation occurred in. reason (str): The reason for the moderation. moderation_type (Type): The type of moderation. moderator (Union[Member, User], optional): The moderator who performed the moderation. Defaults to None. duration (timedelta, optional): The duration of the moderation. Defaults to None. response (Message, optional): The response message. Defaults to None. case (bool, optional): Whether the message is for a moderation case. Defaults to True. Returns: embed: The message embed. """ if response is not None and moderation_type.key not in [ "kick", "ban", "tempban", "unban", ]: guild_name = f"[{guild.name}]({response.jump_url})" else: guild_name = guild.name if duration: embed_duration = f" for {humanize_timedelta(timedelta=duration)}" else: embed_duration = "" embed = Embed( title=str.title(moderation_type.verb), description=f"You have {moderation_type.embed_desc}{moderation_type.verb}{embed_duration} in {guild_name}.", color=color, timestamp=datetime.now(), ) if await config.guild(guild).show_moderator() and moderator is not None: embed.add_field( name="Moderator", value=f"`{moderator.name} ({moderator.id})`", inline=False ) embed.add_field(name="Reason", value=f"`{reason}`", inline=False) if guild.icon.url is not None: embed.set_author(name=guild.name, icon_url=guild.icon.url) else: embed.set_author(name=guild.name) if case: embed.set_footer( text=f"Case #{await Moderation.get_next_case_number(bot=bot, guild_id=guild.id):,}", icon_url="attachment://arrow.png", ) return embed async def resolve_factory(moderation: Moderation, reason: str) -> Embed: """This function creates a resolved embed from set parameters, meant for contacting the moderated user. Args: moderation (aurora.models.Moderation): The moderation object. reason (str): The reason for resolving the moderation. Returns: `discord.Embed` """ embed = Embed( title=str.title(moderation.type.name) + " Resolved", description=f"Your {moderation.type.name} in {moderation.guild.name} has been resolved.", color=await moderation.bot.get_embed_color(moderation.guild.channels[0]), timestamp=datetime.now(), ) embed.add_field(name="Reason", value=f"`{reason}`", inline=False) if moderation.guild.icon.url is not None: embed.set_author(name=moderation.guild.name, icon_url=moderation.guild.icon.url) else: embed.set_author(name=moderation.guild.name) embed.set_footer( text=f"Case #{moderation.id:,}", icon_url="attachment://arrow.png", ) return embed async def log_factory( ctx: commands.Context, moderation: Moderation, resolved: bool = False ) -> Embed: """This function creates a log embed from set parameters, meant for moderation logging. Args: ctx (commands.Context): The ctx object. moderation (aurora.models.Moderation): The moderation object. resolved (bool, optional): Whether the case is resolved or not. Defaults to False. """ target = await moderation.get_target() moderator = await moderation.get_moderator() if resolved: embed = Embed( title=f"📕 Case #{moderation.id:,} Resolved", color=await ctx.bot.get_embed_color(ctx.channel), ) resolved_by = await moderation.get_resolved_by() embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** | " if moderation.duration is not None: duration_embed = ( f"{humanize_timedelta(timedelta=moderation.duration)} | " if not moderation.expired else str(humanize_timedelta(timedelta=moderation.duration)) ) embed.description = ( embed.description + f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}" ) if moderation.metadata.items(): for key, value in moderation.metadata.items(): embed.description += f"\n**{key.title()}:** {value}" embed.add_field(name="Reason", value=box(moderation.reason), inline=False) embed.add_field( name="Resolve Reason", value=f"Resolved by `{resolved_by.name}` ({resolved_by.id}) for:\n" + box(moderation.resolve_reason), inline=False, ) else: embed = Embed( title=f"📕 Case #{moderation.id:,}", color=await ctx.bot.get_embed_color(ctx.channel), ) embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** {target.name} ({target.id})\n**Moderator:** {moderator.name} ({moderator.id})\n**Timestamp:** | " if moderation.duration: embed.description = ( embed.description + f"\n**Duration:** {humanize_timedelta(timedelta=moderation.duration)} | " ) if moderation.metadata.items(): for key, value in moderation.metadata.items(): embed.description += f"\n**{key.title()}:** {value}" embed.add_field(name="Reason", value=box(moderation.reason), inline=False) return embed async def case_factory(interaction: Interaction, moderation: Moderation) -> Embed: """This function creates a case embed from set parameters. Args: interaction (discord.Interaction): The interaction object. moderation (aurora.models.Moderation): The moderation object. """ target = await moderation.get_target() moderator = await moderation.get_moderator() embed = Embed( title=f"📕 Case #{moderation.id:,}", color=await interaction.client.get_embed_color(interaction.channel), ) embed.description = f"**Type:** {str.title(moderation.type.string)}\n**Target:** `{target.name}` ({target.id})\n**Moderator:** `{moderator.name}` ({moderator.id})\n**Resolved:** {moderation.resolved}\n**Timestamp:** | " if moderation.duration: duration_embed = ( f"{humanize_timedelta(timedelta=moderation.duration)} | " if moderation.expired is False else str(humanize_timedelta(timedelta=moderation.duration)) ) embed.description += f"\n**Duration:** {duration_embed}\n**Expired:** {moderation.expired}" embed.description += ( f"\n**Changes:** {len(moderation.changes) - 1}" if moderation.changes else "\n**Changes:** 0" ) if moderation.role_id: role = await moderation.get_role() embed.description += f"\n**Role:** {role.name}" if moderation.metadata: if moderation.metadata.get("imported_from"): embed.description += ( f"\n**Imported From:** {moderation.metadata['imported_from']}" ) moderation.metadata.pop("imported_from") if moderation.metadata.get("imported_timestamp"): embed.description += ( f"\n**Imported Timestamp:** | " ) moderation.metadata.pop("imported_timestamp") if moderation.metadata.items(): for key, value in moderation.metadata.items(): embed.description += f"\n**{key.title()}:** {value}" embed.add_field(name="Reason", value=box(moderation.reason), inline=False) if moderation.resolved: resolved_user = await moderation.get_resolved_by() embed.add_field( name="Resolve Reason", value=f"Resolved by `{resolved_user.name}` ({resolved_user.id}) for:\n{box(moderation.resolve_reason)}", inline=False, ) return embed async def changes_factory(interaction: Interaction, moderation: Moderation) -> Embed: """This function creates a changes embed from set parameters. Args: interaction (discord.Interaction): The interaction object. moderation (aurora.models.Moderation): The moderation object. """ embed = Embed( title=f"📕 Case #{moderation.id:,} Changes", color=await interaction.client.get_embed_color(interaction.channel), ) memory_dict = {} if moderation.changes: for change in moderation.changes: if change.user_id not in memory_dict: memory_dict[str(change.user_id)] = await change.get_user() user: PartialUser = memory_dict[str(change.user_id)] timestamp = f" | " if change.type == "ORIGINAL": embed.add_field( name="Original", value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) elif change.type == "EDIT": embed.add_field( name="Edit", value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) elif change.type == "RESOLVE": embed.add_field( name="Resolve", value=f"**User:** `{user.name}` ({user.id})\n**Reason:** {change.reason}\n**Timestamp:** {timestamp}", inline=False, ) else: embed.description = "*No changes have been made to this case.* 🙁" return embed async def evidenceformat_factory(moderation: Moderation) -> str: """This function creates a codeblock in evidence format from set parameters. Args: interaction (discord.Interaction): The interaction object. moderation (aurora.models.Moderation): The moderation object. """ target = await moderation.get_target() moderator = await moderation.get_moderator() content = f"Case: {moderation.id:,} ({str.title(moderation.type.string)})\nTarget: {target.name} ({target.id})\nModerator: {moderator.name} ({moderator.id})" if moderation.duration is not None: content += f"\nDuration: {humanize_timedelta(timedelta=moderation.duration)}" if moderation.role_id: role = await moderation.get_role() content += "\nRole: " + (role.name if role is not None else moderation.role_id) content += f"\nReason: {moderation.reason}" for key, value in moderation.metadata.items(): content += f"\n{key.title()}: {value}" return box(content, "prolog") ######################################################################################################################## ### Configuration Embeds # ######################################################################################################################## async def _config(ctx: commands.Context) -> Embed: """Generates the core embed for configuration menus to use.""" e = Embed(title="Aurora Configuration Menu", color=await ctx.embed_color()) e.set_thumbnail(url=ctx.bot.user.display_avatar.url) return e async def overrides_embed(ctx: commands.Context) -> Embed: """Generates a configuration menu embed for a user's overrides.""" override_settings = { "ephemeral": await config.user(ctx.author).history_ephemeral(), "inline": await config.user(ctx.author).history_inline(), "inline_pagesize": await config.user(ctx.author).history_inline_pagesize(), "pagesize": await config.user(ctx.author).history_pagesize(), "auto_evidenceformat": await config.user(ctx.author).auto_evidenceformat(), } override_str = [ "- " + bold("Auto Evidence Format: ") + get_bool_emoji(override_settings["auto_evidenceformat"]), "- " + bold("Ephemeral: ") + get_bool_emoji(override_settings["ephemeral"]), "- " + bold("History Inline: ") + get_bool_emoji(override_settings["inline"]), "- " + bold("History Inline Pagesize: ") + get_pagesize_str(override_settings["inline_pagesize"]), "- " + bold("History Pagesize: ") + get_pagesize_str(override_settings["pagesize"]), ] override_str = "\n".join(override_str) e = await _config(ctx) e.title += ": User Overrides" e.description = ( """ Use the buttons below to manage your user overrides. These settings will override the relevant guild settings.\n """ + override_str ) return e async def guild_embed(ctx: commands.Context) -> Embed: """Generates a configuration menu field value for a guild's settings.""" guild_settings = { "show_moderator": await config.guild(ctx.guild).show_moderator(), "use_discord_permissions": await config.guild( ctx.guild ).use_discord_permissions(), "ignore_modlog": await config.guild(ctx.guild).ignore_modlog(), "ignore_other_bots": await config.guild(ctx.guild).ignore_other_bots(), "dm_users": await config.guild(ctx.guild).dm_users(), "log_channel": await config.guild(ctx.guild).log_channel(), "history_ephemeral": await config.guild(ctx.guild).history_ephemeral(), "history_inline": await config.guild(ctx.guild).history_inline(), "history_pagesize": await config.guild(ctx.guild).history_pagesize(), "history_inline_pagesize": await config.guild( ctx.guild ).history_inline_pagesize(), "auto_evidenceformat": await config.guild(ctx.guild).auto_evidenceformat(), "respect_hierarchy": await config.guild(ctx.guild).respect_hierarchy(), } channel = ctx.guild.get_channel(guild_settings["log_channel"]) if channel is None: channel = warning("Not Set") else: channel = channel.mention guild_str = [ "- " + bold("Show Moderator: ") + get_bool_emoji(guild_settings["show_moderator"]), "- " + bold("Use Discord Permissions: ") + get_bool_emoji(guild_settings["use_discord_permissions"]), "- " + bold("Respect Hierarchy: ") + get_bool_emoji(guild_settings["respect_hierarchy"]), "- " + bold("Ignore Modlog: ") + get_bool_emoji(guild_settings["ignore_modlog"]), "- " + bold("Ignore Other Bots: ") + get_bool_emoji(guild_settings["ignore_other_bots"]), "- " + bold("DM Users: ") + get_bool_emoji(guild_settings["dm_users"]), "- " + bold("Auto Evidence Format: ") + get_bool_emoji(guild_settings["auto_evidenceformat"]), "- " + bold("Ephemeral: ") + get_bool_emoji(guild_settings["history_ephemeral"]), "- " + bold("History Inline: ") + get_bool_emoji(guild_settings["history_inline"]), "- " + bold("History Pagesize: ") + get_pagesize_str(guild_settings["history_pagesize"]), "- " + bold("History Inline Pagesize: ") + get_pagesize_str(guild_settings["history_inline_pagesize"]), "- " + bold("Log Channel: ") + channel, ] guild_str = "\n".join(guild_str) e = await _config(ctx) e.title += ": Server Configuration" e.description = ( """ Use the buttons below to manage Aurora's server configuration.\n """ + guild_str ) return e async def addrole_embed(ctx: commands.Context) -> Embed: """Generates a configuration menu field value for a guild's addrole whitelist.""" roles = [] async with config.guild(ctx.guild).addrole_whitelist() as whitelist: for role in whitelist: evalulated_role = ctx.guild.get_role(role) or error(f"`{role}` (Not Found)") if isinstance(evalulated_role, Role): roles.append({ "id": evalulated_role.id, "mention": evalulated_role.mention, "position": evalulated_role.position }) else: roles.append({ "id": role, "mention": error(f"`{role}` (Not Found)"), "position": 0 }) if roles: roles = sorted(roles, key=lambda x: x["position"], reverse=True) roles = [role["mention"] for role in roles] whitelist_str = "\n".join(roles) else: whitelist_str = warning("No roles are on the addrole whitelist!") e = await _config(ctx) e.title += ": Addrole Whitelist" e.description = ( "Use the select menu below to manage this guild's addrole whitelist." ) if len(whitelist_str) > 4000 and len(whitelist_str) < 5000: lines = whitelist_str.split("\n") chunks = [] chunk = "" for line in lines: if len(chunk) + len(line) > 1024: chunks.append(chunk) chunk = line else: chunk += "\n" + line if chunk else line chunks.append(chunk) for chunk in chunks: e.add_field(name="", value=chunk) else: e.description += "\n\n" + whitelist_str return e async def immune_embed(ctx: commands.Context) -> Embed: """Generates a configuration menu embed for a guild's immune roles.""" roles = [] async with config.guild(ctx.guild).immune_roles() as immune_roles: for role in immune_roles: evalulated_role = ctx.guild.get_role(role) or error(f"`{role}` (Not Found)") if isinstance(evalulated_role, Role): roles.append({ "id": evalulated_role.id, "mention": evalulated_role.mention, "position": evalulated_role.position }) else: roles.append({ "id": role, "mention": error(f"`{role}` (Not Found)"), "position": 0 }) if roles: roles = sorted(roles, key=lambda x: x["position"], reverse=True) roles = [role["mention"] for role in roles] immune_str = "\n".join(roles) else: immune_str = warning("No roles are set as immune roles!") e = await _config(ctx) e.title += ": Immune Roles" e.description = "Use the select menu below to manage this guild's immune roles." if len(immune_str) > 4000 and len(immune_str) < 5000: lines = immune_str.split("\n") chunks = [] chunk = "" for line in lines: if len(chunk) + len(line) > 1024: chunks.append(chunk) chunk = line else: chunk += "\n" + line if chunk else line chunks.append(chunk) for chunk in chunks: e.add_field(name="", value=chunk) else: e.description += "\n\n" + immune_str return e async def type_embed(ctx: commands.Context, moderation_type = Type) -> Embed: """Generates a configuration menu field value for a guild's settings.""" type_settings = { "show_in_history": await config.custom("type", ctx.guild.id, moderation_type.key).show_in_history(), "show_moderator": await config.custom("type", ctx.guild.id, moderation_type.key).show_moderator(), "use_discord_permissions": await config.custom("type", ctx.guild.id, moderation_type.key).use_discord_permissions(), "dm_users": await config.custom("type", ctx.guild.id, moderation_type.key).dm_users(), } guild_str = [ "- " + bold("Show in History: ") + get_bool_emoji(type_settings["show_in_history"]), "- " + bold("Show Moderator: ") + get_bool_emoji(type_settings["show_moderator"]), "- " + bold("Use Discord Permissions: ") + get_bool_emoji(type_settings["use_discord_permissions"]), "- " + bold("DM Users: ") + get_bool_emoji(type_settings["dm_users"]), ] guild_str = "\n".join(guild_str) e = await _config(ctx) e.title += ": Server Configuration" e.description = ( """ Use the buttons below to manage Aurora's server configuration.\n """ + guild_str ) return e