diff --git a/aurora/aurora.py b/aurora/aurora.py index 1f946f4..7414b78 100644 --- a/aurora/aurora.py +++ b/aurora/aurora.py @@ -48,7 +48,7 @@ class Aurora(commands.Cog): This cog stores all of its data in an SQLite database.""" __author__ = ["SeaswimmerTheFsh"] - __version__ = "2.0.6" + __version__ = "2.1.6" async def red_delete_data_for_user(self, *, requester, user_id: int): if requester == "discord_deleted_user": @@ -180,50 +180,47 @@ class Aurora(commands.Cog): ### COMMANDS ####################################################################################################################### - @app_commands.command(name="note") + @commands.hybrid_command(name="note") + @commands.mod_or_permissions(moderate_members=True) + @app_commands.describe( + target="Who are you noting?", + reason="Why are you noting this user?", + silent="Should the user be messaged?", + ) async def note( self, - interaction: discord.Interaction, + ctx: commands.Context, target: discord.User, reason: str, silent: bool = None, ): - """Add a note to a user. - - Parameters - ----------- - target: discord.User - Who are you noting? - reason: str - Why are you noting this user? - silent: bool - Should the user be messaged?""" - if not await check_moddable(target, interaction, ["moderate_members"]): + """Add a note to a user.""" + if not await check_moddable(target, ctx, ["moderate_members"]): return - await interaction.response.send_message( + message = await ctx.send( content=f"{target.mention} has recieved a note!\n**Reason** - `{reason}`" ) if silent is None: - silent = not await config.guild(interaction.guild).dm_users() + silent = not await config.guild(ctx.guild).dm_users() if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), - guild=interaction.guild, - moderator=interaction.user, + await self.bot.get_embed_color(ctx.channel), + guild=ctx.guild, + moderator=ctx.author, reason=reason, moderation_type="note", - response=await interaction.original_response(), + response=message, ) await target.send(embed=embed) except discord.errors.HTTPException: pass moderation_id = await mysql_log( - interaction.guild.id, - interaction.user.id, + ctx.guild.id, + ctx.author.id, "NOTE", "USER", target.id, @@ -231,58 +228,55 @@ class Aurora(commands.Cog): "NULL", reason, ) - await interaction.edit_original_response( + await message.edit( content=f"{target.mention} has received a note! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) + await log(ctx, moderation_id) - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + case = await fetch_case(moderation_id, ctx.guild.id) + await send_evidenceformat(ctx, case) - @app_commands.command(name="warn") + @commands.hybrid_command(name="warn") + @commands.mod_or_permissions(moderate_members=True) + @app_commands.describe( + target="Who are you warning?", + reason="Why are you warning this user?", + silent="Should the user be messaged?", + ) async def warn( self, - interaction: discord.Interaction, + ctx: commands.Context, target: discord.Member, reason: str, silent: bool = None, ): - """Warn a user. - - Parameters - ----------- - target: discord.Member - Who are you warning? - reason: str - Why are you warning this user? - silent: bool - Should the user be messaged?""" - if not await check_moddable(target, interaction, ["moderate_members"]): + """Warn a user.""" + if not await check_moddable(target, ctx, ["moderate_members"]): return - await interaction.response.send_message( + message = await ctx.send( content=f"{target.mention} has been warned!\n**Reason** - `{reason}`" ) if silent is None: - silent = not await config.guild(interaction.guild).dm_users() + silent = not await config.guild(ctx.guild).dm_users() if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), - guild=interaction.guild, - moderator=interaction.user, + await self.bot.get_embed_color(ctx.channel), + guild=ctx.guild, + moderator=ctx.author, reason=reason, moderation_type="warned", - response=await interaction.original_response(), + response=message, ) await target.send(embed=embed) except discord.errors.HTTPException: pass moderation_id = await mysql_log( - interaction.guild.id, - interaction.user.id, + ctx.guild.id, + ctx.author.id, "WARN", "USER", target.id, @@ -290,13 +284,13 @@ class Aurora(commands.Cog): "NULL", reason, ) - await interaction.edit_original_response( + await message.edit( content=f"{target.mention} has been warned! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) + await log(ctx, moderation_id) - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + case = await fetch_case(moderation_id, ctx.guild.id) + await send_evidenceformat(ctx, case) @app_commands.command(name="addrole") async def addrole( @@ -407,32 +401,28 @@ class Aurora(commands.Cog): case = await fetch_case(moderation_id, interaction.guild.id) await send_evidenceformat(interaction, case) - @app_commands.command(name="mute") + @commands.hybrid_command(name="mute") + @commands.mod_or_permissions(moderate_members=True) + @app_commands.describe( + target="Who are you muting?", + duration="How long are you muting this user for?", + reason="Why are you muting this user?", + silent="Should the user be messaged?", + ) async def mute( self, - interaction: discord.Interaction, + ctx: commands.Context, target: discord.Member, duration: str, reason: str, silent: bool = None, ): - """Mute a user. - - Parameters - ----------- - target: discord.Member - Who are you unbanning? - duration: str - How long are you muting this user for? - reason: str - Why are you unbanning this user? - silent: bool - Should the user be messaged?""" - if not await check_moddable(target, interaction, ["moderate_members"]): + """Mute a user.""" + if not await check_moddable(target, ctx, ["moderate_members"]): return if target.is_timed_out() is True: - await interaction.response.send_message( + await ctx.send( error(f"{target.mention} is already muted!"), allowed_mentions=discord.AllowedMentions(users=False), ephemeral=True, @@ -442,36 +432,36 @@ class Aurora(commands.Cog): try: parsed_time = parse(sval=duration, as_timedelta=True, raise_exception=True) except ValueError: - await interaction.response.send_message( + await ctx.send( error("Please provide a valid duration!"), ephemeral=True ) return if parsed_time.total_seconds() / 1000 > 2419200000: - await interaction.response.send_message( + await ctx.send( error("Please provide a duration that is less than 28 days.") ) return await target.timeout( - parsed_time, reason=f"Muted by {interaction.user.id} for: {reason}" + parsed_time, reason=f"Muted by {ctx.author.id} for: {reason}" ) - await interaction.response.send_message( + message = await ctx.send( content=f"{target.mention} has been muted for {humanize.precisedelta(parsed_time)}!\n**Reason** - `{reason}`" ) if silent is None: - silent = not await config.guild(interaction.guild).dm_users() + silent = not await config.guild(ctx.guild).dm_users() if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), - guild=interaction.guild, - moderator=interaction.user, + await ctx.embed_color(), + guild=ctx.guild, + moderator=ctx.author, reason=reason, moderation_type="muted", - response=await interaction.original_response(), + response=message, duration=parsed_time, ) await target.send(embed=embed) @@ -479,8 +469,8 @@ class Aurora(commands.Cog): pass moderation_id = await mysql_log( - interaction.guild.id, - interaction.user.id, + ctx.guild.id, + ctx.author.id, "MUTE", "USER", target.id, @@ -488,37 +478,34 @@ class Aurora(commands.Cog): parsed_time, reason, ) - await interaction.edit_original_response( + await message.edit( content=f"{target.mention} has been muted for {humanize.precisedelta(parsed_time)}! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) + await log(ctx, moderation_id) - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + case = await fetch_case(moderation_id, ctx.guild.id) + await send_evidenceformat(ctx, case) - @app_commands.command(name="unmute") + @commands.hybrid_command(name="unmute") + @commands.mod_or_permissions(moderate_members=True) + @app_commands.describe( + target="Who are you unmuting?", + reason="Why are you unmuting this user?", + silent="Should the user be messaged?", + ) async def unmute( self, - interaction: discord.Interaction, + ctx: commands.Context, target: discord.Member, reason: str = None, silent: bool = None, ): - """Unmute a user. - - Parameters - ----------- - target: discord.user - Who are you unmuting? - reason: str - Why are you unmuting this user? - silent: bool - Should the user be messaged?""" - if not await check_moddable(target, interaction, ["moderate_members"]): + """Unmute a user.""" + if not await check_moddable(target, ctx, ["moderate_members"]): return if target.is_timed_out() is False: - await interaction.response.send_message( + await ctx.send( error(f"{target.mention} is not muted!"), allowed_mentions=discord.AllowedMentions(users=False), ephemeral=True, @@ -527,35 +514,35 @@ class Aurora(commands.Cog): if reason: await target.timeout( - None, reason=f"Unmuted by {interaction.user.id} for: {reason}" + None, reason=f"Unmuted by {ctx.author.id} for: {reason}" ) else: - await target.timeout(None, reason=f"Unbanned by {interaction.user.id}") + await target.timeout(None, reason=f"Unmuted by {ctx.author.id}") reason = "No reason given." - await interaction.response.send_message( + message = await ctx.send( content=f"{target.mention} has been unmuted!\n**Reason** - `{reason}`" ) if silent is None: - silent = not await config.guild(interaction.guild).dm_users() + silent = not await config.guild(ctx.guild).dm_users() if silent is False: try: embed = await message_factory( - await self.bot.get_embed_color(interaction.channel), - guild=interaction.guild, - moderator=interaction.user, + await ctx.embed_color(), + guild=ctx.guild, + moderator=ctx.author, reason=reason, moderation_type="unmuted", - response=await interaction.original_response(), + response=message, ) await target.send(embed=embed) except discord.errors.HTTPException: pass moderation_id = await mysql_log( - interaction.guild.id, - interaction.user.id, + ctx.guild.id, + ctx.author.id, "UNMUTE", "USER", target.id, @@ -563,13 +550,13 @@ class Aurora(commands.Cog): "NULL", reason, ) - await interaction.edit_original_response( + await message.edit( content=f"{target.mention} has been unmuted! (Case `#{moderation_id:,}`)\n**Reason** - `{reason}`" ) - await log(interaction, moderation_id) + await log(ctx, moderation_id) - case = await fetch_case(moderation_id, interaction.guild.id) - await send_evidenceformat(interaction, case) + case = await fetch_case(moderation_id, ctx.guild.id) + await send_evidenceformat(ctx, case) @app_commands.command(name="kick") async def kick( diff --git a/aurora/utilities/factory.py b/aurora/utilities/factory.py index b7c414c..69d8cda 100644 --- a/aurora/utilities/factory.py +++ b/aurora/utilities/factory.py @@ -3,8 +3,7 @@ from datetime import datetime, timedelta from typing import Union import humanize -from discord import (Color, Embed, Guild, Interaction, InteractionMessage, - Member, Role, User) +from discord import Color, Embed, Guild, Member, Message, Role, User from redbot.core import commands from redbot.core.utils.chat_formatting import bold, box, error, warning @@ -21,7 +20,7 @@ async def message_factory( moderation_type: str, moderator: Union[Member, User] = None, duration: timedelta = None, - response: InteractionMessage = None, + response: Message = None, role: Role = None, ) -> Embed: """This function creates a message from set parameters, meant for contacting the moderated user. @@ -33,10 +32,9 @@ async def message_factory( moderation_type (str): 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 (InteractionMessage, optional): The response message. Defaults to None. + response (Message, optional): The response message. Defaults to None. role (Role, optional): The role that was added or removed. Defaults to None. - Returns: embed: The message embed. """ @@ -92,7 +90,7 @@ async def message_factory( async def log_factory( - interaction: Interaction, case_dict: dict, resolved: bool = False + ctx: commands.Context, case_dict: dict, resolved: bool = False ) -> Embed: """This function creates a log embed from set parameters, meant for moderation logging. @@ -103,20 +101,20 @@ async def log_factory( """ if resolved: if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) + target_user = await fetch_user_dict(ctx.bot, case_dict["target_id"]) target_name = ( f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`" ) elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) + target_user = await fetch_channel_dict(ctx.guild, case_dict["target_id"]) if target_user["mention"]: target_name = f"{target_user['mention']}" else: target_name = f"`{target_user['name']}`" - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) + moderator_user = await fetch_user_dict(ctx.bot, case_dict["moderator_id"]) moderator_name = ( f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" @@ -125,7 +123,7 @@ async def log_factory( embed = Embed( title=f"📕 Case #{case_dict['moderation_id']:,} Resolved", - color=await interaction.client.get_embed_color(interaction.channel), + color=await ctx.client.get_embed_color(ctx.channel), ) embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** | " @@ -152,7 +150,7 @@ async def log_factory( embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False) - resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"]) + resolved_user = await fetch_user_dict(ctx.bot, case_dict["resolved_by"]) resolved_name = ( resolved_user["name"] if resolved_user["discriminator"] == "0" @@ -166,20 +164,20 @@ async def log_factory( ) else: if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) + target_user = await fetch_user_dict(ctx.bot, case_dict["target_id"]) target_name = ( f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`" ) elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) + target_user = await fetch_channel_dict(ctx.guild, case_dict["target_id"]) if target_user["mention"]: target_name = target_user["mention"] else: target_name = f"`{target_user['name']}`" - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) + moderator_user = await fetch_user_dict(ctx.bot, case_dict["moderator_id"]) moderator_name = ( f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" @@ -188,7 +186,7 @@ async def log_factory( embed = Embed( title=f"📕 Case #{case_dict['moderation_id']:,}", - color=await interaction.client.get_embed_color(interaction.channel), + color=await ctx.bot.get_embed_color(ctx.channel), ) embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Timestamp:** | " @@ -211,28 +209,28 @@ async def log_factory( return embed -async def case_factory(interaction: Interaction, case_dict: dict) -> Embed: +async def case_factory(ctx: commands.Context, case_dict: dict) -> Embed: """This function creates a case embed from set parameters. Args: - interaction (Interaction): The interaction object. + ctx (commands.Context): The context object. case_dict (dict): The case dictionary. """ if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) + target_user = await fetch_user_dict(ctx.bot, case_dict["target_id"]) target_name = ( f"`{target_user['name']}`" if target_user["discriminator"] == "0" else f"`{target_user['name']}#{target_user['discriminator']}`" ) elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) + target_user = await fetch_channel_dict(ctx.guild, case_dict["target_id"]) if target_user["mention"]: target_name = f"{target_user['mention']}" else: target_name = f"`{target_user['name']}`" - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) + moderator_user = await fetch_user_dict(ctx.bot, case_dict["moderator_id"]) moderator_name = ( f"`{moderator_user['name']}`" if moderator_user["discriminator"] == "0" @@ -241,7 +239,7 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed: embed = Embed( title=f"📕 Case #{case_dict['moderation_id']:,}", - color=await interaction.client.get_embed_color(interaction.channel), + color=await ctx.bot.get_embed_color(ctx.channel), ) embed.description = f"**Type:** {str.title(case_dict['moderation_type'])}\n**Target:** {target_name} ({target_user['id']})\n**Moderator:** {moderator_name} ({moderator_user['id']})\n**Resolved:** {bool(case_dict['resolved'])}\n**Timestamp:** | " @@ -276,7 +274,7 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed: embed.add_field(name="Reason", value=box(case_dict["reason"]), inline=False) if case_dict["resolved"] == 1: - resolved_user = await fetch_user_dict(interaction.client, case_dict["resolved_by"]) + resolved_user = await fetch_user_dict(ctx.bot, case_dict["resolved_by"]) resolved_name = ( f"`{resolved_user['name']}`" if resolved_user["discriminator"] == "0" @@ -291,16 +289,16 @@ async def case_factory(interaction: Interaction, case_dict: dict) -> Embed: return embed -async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed: +async def changes_factory(ctx: commands.Context, case_dict: dict) -> Embed: """This function creates a changes embed from set parameters. Args: - interaction (Interaction): The interaction object. + ctx (commands.Context): The context object. case_dict (dict): The case dictionary. """ embed = Embed( title=f"📕 Case #{case_dict['moderation_id']:,} Changes", - color=await interaction.client.get_embed_color(interaction.channel), + color=await ctx.bot.get_embed_color(ctx.channel), ) memory_dict = {} @@ -309,7 +307,7 @@ async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed: for change in case_dict["changes"]: if change["user_id"] not in memory_dict: memory_dict[str(change["user_id"])] = await fetch_user_dict( - interaction.client, change["user_id"] + ctx.bot, change["user_id"] ) user = memory_dict[str(change["user_id"])] @@ -348,7 +346,7 @@ async def changes_factory(interaction: Interaction, case_dict: dict) -> Embed: return embed -async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> str: +async def evidenceformat_factory(ctx: commands.Context, case_dict: dict) -> str: """This function creates a codeblock in evidence format from set parameters. Args: @@ -356,7 +354,7 @@ async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> s case_dict (dict): The case dictionary. """ if case_dict["target_type"] == "USER": - target_user = await fetch_user_dict(interaction.client, case_dict["target_id"]) + target_user = await fetch_user_dict(ctx.bot, case_dict["target_id"]) target_name = ( target_user["name"] if target_user["discriminator"] == "0" @@ -364,10 +362,10 @@ async def evidenceformat_factory(interaction: Interaction, case_dict: dict) -> s ) elif case_dict["target_type"] == "CHANNEL": - target_user = await fetch_channel_dict(interaction.guild, case_dict["target_id"]) + target_user = await fetch_channel_dict(ctx.guild, case_dict["target_id"]) target_name = target_user["name"] - moderator_user = await fetch_user_dict(interaction.client, case_dict["moderator_id"]) + moderator_user = await fetch_user_dict(ctx.bot, case_dict["moderator_id"]) moderator_name = ( moderator_user["name"] if moderator_user["discriminator"] == "0" diff --git a/aurora/utilities/utils.py b/aurora/utilities/utils.py index 12c8400..eb0eb16 100644 --- a/aurora/utilities/utils.py +++ b/aurora/utilities/utils.py @@ -40,11 +40,11 @@ def check_permissions( async def check_moddable( - target: Union[User, Member], interaction: Interaction, permissions: list + target: Union[User, Member], ctx: Union[commands.Context, Interaction], permissions: list ) -> bool: """Checks if a moderator can moderate a target.""" - if check_permissions(interaction.client.user, permissions, guild=interaction.guild): - await interaction.response.send_message( + if check_permissions(ctx.bot.user, permissions, guild=ctx.guild): + await ctx.send( error( f"I do not have the `{permissions}` permission, required for this action." ), @@ -52,9 +52,9 @@ async def check_moddable( ) return False - if await config.guild(interaction.guild).use_discord_permissions() is True: - if check_permissions(interaction.user, permissions, guild=interaction.guild): - await interaction.response.send_message( + if await config.guild(ctx.guild).use_discord_permissions() is True: + if check_permissions(ctx.author, permissions, guild=ctx.guild): + await ctx.send( error( f"You do not have the `{permissions}` permission, required for this action." ), @@ -62,21 +62,21 @@ async def check_moddable( ) return False - if interaction.user.id == target.id: - await interaction.response.send_message( + if ctx.author.id == target.id: + await ctx.send( content="You cannot moderate yourself!", ephemeral=True ) return False if target.bot: - await interaction.response.send_message( + await ctx.send( content="You cannot moderate bots!", ephemeral=True ) return False if isinstance(target, Member): - if interaction.user.top_role <= target.top_role: - await interaction.response.send_message( + if ctx.author.top_role <= target.top_role: + await ctx.send( content=error( "You cannot moderate members with a higher role than you!" ), @@ -85,10 +85,10 @@ async def check_moddable( return False if ( - interaction.guild.get_member(interaction.client.user.id).top_role + ctx.guild.get_member(ctx.bot.user.id).top_role <= target.top_role ): - await interaction.response.send_message( + await ctx.send( content=error( "You cannot moderate members with a role higher than the bot!" ), @@ -100,7 +100,7 @@ async def check_moddable( for role in target.roles: if role.id in immune_roles: - await interaction.response.send_message( + await ctx.send( content=error("You cannot moderate members with an immune role!"), ephemeral=True, ) @@ -203,19 +203,19 @@ async def fetch_role_dict(guild: Guild, role_id: int) -> dict: return role_dict -async def log(interaction: Interaction, moderation_id: int, resolved: bool = False) -> None: +async def log(ctx: Union[commands.Context, Interaction], moderation_id: int, resolved: bool = False) -> None: """This function sends a message to the guild's configured logging channel when an infraction takes place.""" from .database import fetch_case from .factory import log_factory - logging_channel_id = await config.guild(interaction.guild).log_channel() + logging_channel_id = await config.guild(ctx.guild).log_channel() if logging_channel_id != " ": - logging_channel = interaction.guild.get_channel(logging_channel_id) + logging_channel = ctx.guild.get_channel(logging_channel_id) - case = await fetch_case(moderation_id, interaction.guild.id) + case = await fetch_case(moderation_id, ctx.guild.id) if case: embed = await log_factory( - interaction=interaction, case_dict=case, resolved=resolved + ctx=ctx, case_dict=case, resolved=resolved ) try: await logging_channel.send(embed=embed) @@ -223,20 +223,20 @@ async def log(interaction: Interaction, moderation_id: int, resolved: bool = Fal return -async def send_evidenceformat(interaction: Interaction, case_dict: dict) -> None: +async def send_evidenceformat(ctx: commands.Context, case_dict: dict) -> None: """This function sends an ephemeral message to the moderator who took the moderation action, with a pre-made codeblock for use in the mod-evidence channel.""" from .factory import evidenceformat_factory send_evidence_bool = ( - await config.user(interaction.user).auto_evidenceformat() - or await config.guild(interaction.guild).auto_evidenceformat() + await config.user(ctx.author).auto_evidenceformat() + or await config.guild(ctx.guild).auto_evidenceformat() or False ) if send_evidence_bool is False: return - content = await evidenceformat_factory(interaction=interaction, case_dict=case_dict) - await interaction.followup.send(content=content, ephemeral=True) + content = await evidenceformat_factory(ctx=ctx, case_dict=case_dict) + await ctx.send(content=content, ephemeral=True) def convert_timedelta_to_str(timedelta: td) -> str: