SeaCogs/aurora/utilities/factory.py
cswimr 5d22e67864
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Successful in 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 41s
fix(aurora): don't mutate a list while iterating through it
2024-08-19 17:53:15 -04:00

581 lines
22 KiB
Python

# 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, inline, 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 not moderation_type.removes_from_guild:
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(),
)
show_moderator = await config.custom("types", guild.id, moderation_type.key).show_moderator()
if show_moderator is None:
show_moderator = await config.guild(guild).show_moderator()
if 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:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if moderation.duration is not None:
duration_embed = (
f"{humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.end_timestamp}:R>"
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:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if moderation.duration:
embed.description = (
embed.description
+ f"\n**Duration:** {humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.unix_timestamp}:R>"
)
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:** <t:{moderation.unix_timestamp}> | <t:{moderation.unix_timestamp}:R>"
if moderation.duration:
duration_embed = (
f"{humanize_timedelta(timedelta=moderation.duration)} | <t:{moderation.unix_timestamp}:R>"
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:** <t:{moderation.metadata['imported_timestamp']}> | <t:{moderation.metadata['imported_timestamp']}:R>"
)
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"<t:{change.unix_timestamp}> | <t:{change.unix_timestamp}:R>"
end_timestamp = f"<t:{change.unix_end_timestamp}> | <t:{change.unix_end_timestamp}:R>" if change.end_timestamp else None
change_str = [
f"{bold('User:')} {inline(user.name)} ({user.id})",
f"{bold('Reason:')} {change.reason}" if change.reason else "",
f"{bold('Duration:')} {humanize_timedelta(timedelta=change.duration)}" if change.duration else "",
f"{bold('End Timestamp:')} {end_timestamp}" if end_timestamp else "",
f"{bold('Timestamp')} {timestamp}",
]
copy = change_str.copy()
for string in change_str:
if string == "":
copy.remove(string)
embed.add_field(
name=change.type.title(),
value="\n".join(copy),
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("types", ctx.guild.id, moderation_type.key).show_in_history(),
"show_moderator": await config.custom("types", ctx.guild.id, moderation_type.key).show_moderator(),
"use_discord_permissions": await config.custom("types", ctx.guild.id, moderation_type.key).use_discord_permissions(),
"dm_users": await config.custom("types", 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 += f": {moderation_type.string.title()} Configuration"
e.description = (
f"""
Use the buttons below to manage Aurora's configuration for the {bold(moderation_type.string)} moderation type.
If an option has a question mark (\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}) next to it, Aurora will default to the guild level setting instead.
See `{ctx.prefix}aurora set guild` for more information.\n
"""
+ guild_str
)
return e