diff --git a/aurora/abc.py b/aurora/abc.py new file mode 100644 index 0000000..42c9fdf --- /dev/null +++ b/aurora/abc.py @@ -0,0 +1,28 @@ +from abc import ABC + +from redbot.core import commands +from redbot.core.bot import Red + +from .utilities.config import Config + + +class CompositeMetaClass(type(commands.Cog), type(ABC)): + """ + This allows the metaclass used for proper type detection to + coexist with discord.py's metaclass + """ + + pass + + +class Mixin(ABC): + """ + Base class for well behaved type hint detection with composite class. + + Basically, to keep developers sane when not all attributes are defined in each mixin. + """ + + def __init__(self, *_args): + super().__init__() + self.config: Config + self.bot: Red diff --git a/aurora/aurora.py b/aurora/aurora.py index 405372d..d77b670 100644 --- a/aurora/aurora.py +++ b/aurora/aurora.py @@ -20,6 +20,8 @@ from redbot.core import app_commands, checks, commands, data_manager from redbot.core.app_commands import Choice from redbot.core.utils.chat_formatting import box, error, warning +from .abc import CompositeMetaClass +from .configuration.commands import Configuration from .importers.galacticbot import ImportGalacticBotView from .importers.aurora import ImportAuroraView from .utilities.config import config, register_config @@ -29,7 +31,7 @@ from .utilities.logger import logger from .utilities.utils import convert_timedelta_to_str, check_moddable, check_permissions, fetch_channel_dict, fetch_user_dict, generate_dict, log, send_evidenceformat -class Aurora(commands.Cog): +class Aurora(Configuration, commands.Cog, metaclass=CompositeMetaClass): """Aurora is a fully-featured moderation system. It is heavily inspired by GalacticBot, and is designed to be a more user-friendly alternative to Red's core Mod cogs. This cog stores all of its data in an SQLite database.""" @@ -1016,330 +1018,6 @@ class Aurora(commands.Cog): completion_time = (time.time() - current_time) * 1000 logger.debug("Completed expiry loop in %sms with %s users unbanned", f"{completion_time:.6f}", global_num) - ####################################################################################################################### - ### CONFIGURATION COMMANDS - ####################################################################################################################### - - @commands.group(autohelp=True, aliases=['moderationset', 'modset', 'moderationsettings', 'aurorasettings', 'auroraconfig']) - async def auroraset(self, ctx: commands.Context): - """Manage moderation commands.""" - - @auroraset.command(name='list', aliases=['view', 'show']) - async def auroraset_list(self, ctx: commands.Context): - """List all moderation settings.""" - if ctx.guild: - guild_settings = await config.guild(ctx.guild).all() - - guild_settings_string = "" - for setting in guild_settings: - if 'roles' in setting: - continue - if setting == 'log_channel': - channel = ctx.guild.get_channel(guild_settings[setting]) - guild_settings_string += f"**{setting}**: {channel.mention}\n" if channel else f"**{setting}**: {guild_settings[setting]}\n" - else: - guild_settings_string += f"**{setting}**: {guild_settings[setting]}\n" - - user_settings = await config.user(ctx.author).all() - user_settings_string = "" - for setting in user_settings: - user_settings_string += f"**{setting}**: {user_settings[setting]}\n" - - embed = discord.Embed(color=await self.bot.get_embed_color(ctx.channel)) - embed.set_author(icon_url=ctx.guild.icon.url, name=f"{ctx.guild.name} Moderation Settings") - if ctx.guild: - embed.add_field(name="Guild Settings", value=guild_settings_string) - embed.add_field(name="User Settings", value=user_settings_string) - - await ctx.send(embed=embed) - - @auroraset.group(autohelp=True, name='user') - async def auroraset_user(self, ctx: commands.Context): - """Manage configurations for user configuration options.""" - - @auroraset_user.command(name='autoevidence') - async def auroraset_user_autoevidence(self, ctx: commands.Context, enabled: bool): - """Toggle if the evidenceformat codeblock should be sent automatically.""" - await config.user(ctx.author).auto_evidenceformat.set(enabled) - await ctx.send(f"Auto evidenceformat setting set to {enabled}") - - @auroraset_user.group(autohelp=True, name='history') - async def auroraset_user_history(self, ctx: commands.Context): - """Manage configuration for the /history command.""" - - @auroraset_user_history.command(name='ephemeral', aliases=['hidden', 'hide']) - async def auroraset_user_history_ephemeral(self, ctx: commands.Context, enabled: bool): - """Toggle if the /history command should be ephemeral.""" - await config.user(ctx.author).history_ephemeral.set(enabled) - await ctx.send(f"Ephemeral setting set to {enabled}") - - @auroraset_user_history.command(name='pagesize') - async def auroraset_user_history_pagesize(self, ctx: commands.Context, pagesize: int): - """Set the amount of cases to display per page.""" - if pagesize > 20: - await ctx.send("Pagesize cannot be greater than 20!") - return - if pagesize < 1: - await ctx.send("Pagesize cannot be less than 1!") - return - await config.user(ctx.author).history_pagesize.set(pagesize) - await ctx.send(f"Pagesize set to {await config.user(ctx.author).history_pagesize()}") - - @auroraset_user_history.group(name='inline') - async def auroraset_user_history_inline(self, ctx: commands.Context): - """Manage configuration for the /history command's inline argument.""" - - @auroraset_user_history_inline.command(name='toggle') - async def auroraset_user_history_inline_toggle(self, ctx: commands.Context, enabled: bool): - """Enable the /history command's inline argument by default.""" - await config.user(ctx.author).history_inline.set(enabled) - await ctx.send(f"Inline setting set to {enabled}") - - @auroraset_user_history_inline.command(name='pagesize') - async def auroraset_user_history_inline_pagesize(self, ctx: commands.Context, pagesize: int): - """Set the amount of cases to display per page.""" - if pagesize > 20: - await ctx.send(error("Pagesize cannot be greater than 20!")) - return - if pagesize < 1: - await ctx.send(error("Pagesize cannot be less than 1!")) - return - await config.user(ctx.author).history_inline_pagesize.set(pagesize) - await ctx.send(f"Inline pagesize set to {await config.user(ctx.author).history_inline_pagesize()}") - - @auroraset.group(autohelp=True, name='guild') - @checks.admin() - async def auroraset_guild(self, ctx: commands.Context): - """Manage default configurations for user configuration options, per guild.""" - - @auroraset_guild.command(name='autoevidence') - async def auroraset_guild_autoevidence(self, ctx: commands.Context, enabled: bool): - """Toggle if the evidenceformat codeblock should be sent automatically.""" - await config.guild(ctx.guild).auto_evidenceformat.set(enabled) - await ctx.send(f"Auto evidenceformat setting set to {enabled}") - - @auroraset_guild.group(autohelp=True, name='history') - @checks.admin() - async def auroraset_guild_history(self, ctx: commands.Context): - """Manage configuration for the /history command.""" - - @auroraset_guild_history.command(name='ephemeral', aliases=['hidden', 'hide']) - @checks.admin() - async def auroraset_guild_history_ephemeral(self, ctx: commands.Context, enabled: bool): - """Toggle if the /history command should be ephemeral.""" - await config.guild(ctx.guild).history_ephemeral.set(enabled) - await ctx.send(f"Ephemeral setting set to {enabled}") - - @auroraset_guild_history.command(name='pagesize') - @checks.admin() - async def auroraset_guild_history_pagesize(self, ctx: commands.Context, pagesize: int): - """Set the amount of cases to display per page.""" - if pagesize > 20: - await ctx.send("Pagesize cannot be greater than 20!") - return - if pagesize < 1: - await ctx.send("Pagesize cannot be less than 1!") - return - await config.guild(ctx.guild).history_pagesize.set(pagesize) - await ctx.send(f"Pagesize set to {await config.guild(ctx.guild).history_pagesize()}") - - @auroraset_guild_history.group(name='inline') - @checks.admin() - async def auroraset_guild_history_inline(self, ctx: commands.Context): - """Manage configuration for the /history command's inline argument.""" - - @auroraset_guild_history_inline.command(name='toggle') - @checks.admin() - async def auroraset_guild_history_inline_toggle(self, ctx: commands.Context, enabled: bool): - """Enable the /history command's inline argument by default.""" - await config.guild(ctx.guild).history_inline.set(enabled) - await ctx.send(f"Inline setting set to {enabled}") - - @auroraset_guild_history_inline.command(name='pagesize') - @checks.admin() - async def auroraset_guild_history_inline_pagesize(self, ctx: commands.Context, pagesize: int): - """Set the amount of cases to display per page.""" - if pagesize > 20: - await ctx.send("Pagesize cannot be greater than 20!") - return - if pagesize < 1: - await ctx.send("Pagesize cannot be less than 1!") - return - await config.guild(ctx.guild).history_inline_pagesize.set(pagesize) - await ctx.send(f"Inline pagesize set to {await config.guild(ctx.guild).history_inline_pagesize()}") - - @auroraset.group(autohelp=True, name='immunity') - @checks.admin() - async def auroraset_immunity(self, ctx: commands.Context): - """Manage configuration for immune roles.""" - - @auroraset_immunity.command(name='add') - @checks.admin() - async def auroraset_immunity_add(self, ctx: commands.Context, role: discord.Role): - """Add a role to the immune roles list.""" - immune_roles: list = await config.guild(ctx.guild).immune_roles() - if role.id in immune_roles: - await ctx.send(error("Role is already immune!")) - return - immune_roles.append(role.id) - await config.guild(ctx.guild).immune_roles.set(immune_roles) - await ctx.send(f"Role {role.name} added to immune roles.") - - @auroraset_immunity.command(name='remove') - @checks.admin() - async def auroraset_immunity_remove(self, ctx: commands.Context, role: discord.Role): - """Remove a role from the immune roles list.""" - immune_roles: list = await config.guild(ctx.guild).immune_roles() - if role.id not in immune_roles: - await ctx.send(error("Role is not immune!")) - return - immune_roles.remove(role.id) - await config.guild(ctx.guild).immune_roles.set(immune_roles) - await ctx.send(f"Role {role.name} removed from immune roles.") - - @auroraset_immunity.command(name='list') - @checks.admin() - async def auroraset_immunity_list(self, ctx: commands.Context): - """List all immune roles.""" - immune_roles: list = await config.guild(ctx.guild).immune_roles() - if not immune_roles: - await ctx.send("No immune roles set!") - return - role_list = "" - for role_id in immune_roles: - role = ctx.guild.get_role(role_id) - if role: - role_list += f"{role.mention}\n" - if role_list: - embed = discord.Embed(title="Immune Roles", description=role_list, color=await self.bot.get_embed_color(ctx.channel)) - await ctx.send(embed=embed) - - @auroraset.group(autohelp=True, name='blacklist') - @checks.admin() - async def auroraset_blacklist(self, ctx: commands.Context): - """Manage configuration for the /blacklist command.""" - - @auroraset_blacklist.command(name='add') - @checks.admin() - async def auroraset_blacklist_add(self, ctx: commands.Context, role: discord.Role, duration: str): - """Add a role to the blacklist.""" - blacklist_roles: list = await config.guild(ctx.guild).blacklist_roles() - for blacklist_role in blacklist_roles: - if role.id == blacklist_role['role']: - await ctx.send(error("Role already has an associated blacklist type!")) - return - - try: - parsed_time = parse(sval=duration, as_timedelta=True, raise_exception=True) - except ValueError: - await ctx.send(error("Please provide a valid duration!")) - return - - blacklist_roles.append( - { - 'role': role.id, - 'duration': str(parsed_time) - } - ) - await config.guild(ctx.guild).blacklist_roles.set(blacklist_roles) - await ctx.send(f"Role {role.mention} added as a blacklist type.", allowed_mentions=discord.AllowedMentions.none()) - - @auroraset_blacklist.command(name='remove') - @checks.admin() - async def auroraset_blacklist_remove(self, ctx: commands.Context, role: discord.Role): - """Remove a role's blacklist type.""" - blacklist_roles: list = await config.guild(ctx.guild).blacklist_roles() - for blacklist_role in blacklist_roles: - if role.id == blacklist_role['role']: - blacklist_roles.remove(blacklist_role) - await config.guild(ctx.guild).blacklist_roles.set(blacklist_roles) - await ctx.send(f"Role {role.mention} removed from blacklist types.", allowed_mentions=discord.AllowedMentions.none()) - return - await ctx.send(error("Role does not have an associated blacklist type!")) - - @auroraset_blacklist.command(name='list') - @checks.admin() - async def auroraset_blacklist_list(self, ctx: commands.Context): - """List all blacklist types.""" - blacklist_roles: list = await config.guild(ctx.guild).blacklist_roles() - if not blacklist_roles: - await ctx.send("No blacklist types set!") - return - blacklist_list = "" - for blacklist_role in blacklist_roles: - role = ctx.guild.get_role(blacklist_role['role']) - if role: - blacklist_list += f"{role.mention} - {blacklist_role['duration']}\n" - if blacklist_list: - embed = discord.Embed(title="Blacklist Types", description=blacklist_list, color=await self.bot.get_embed_color(ctx.channel)) - await ctx.send(embed=embed) - - @auroraset.command(name="ignorebots") - @checks.admin() - async def auroraset_ignorebots(self, ctx: commands.Context): - """Toggle if the cog should ignore other bots' moderations.""" - await config.guild(ctx.guild).ignore_other_bots.set(not await config.guild(ctx.guild).ignore_other_bots()) - await ctx.send(f"Ignore bots setting set to {await config.guild(ctx.guild).ignore_other_bots()}") - - @auroraset.command(name="dm") - @checks.admin() - async def auroraset_dm(self, ctx: commands.Context): - """Toggle automatically messaging moderated users. - - This option can be overridden by specifying the `silent` argument in any moderation command.""" - await config.guild(ctx.guild).dm_users.set(not await config.guild(ctx.guild).dm_users()) - await ctx.send(f"DM users setting set to {await config.guild(ctx.guild).dm_users()}") - - @auroraset.command(name="permissions") - @checks.admin() - async def auroraset_permissions(self, ctx: commands.Context): - """Toggle whether the bot will check for discord permissions.""" - await config.guild(ctx.guild).use_discord_permissions.set(not await config.guild(ctx.guild).use_discord_permissions()) - await ctx.send(f"Use Discord Permissions setting set to {await config.guild(ctx.guild).use_discord_permissions()}") - - @auroraset.command(name="logchannel") - @checks.admin() - async def auroraset_logchannel(self, ctx: commands.Context, channel: discord.TextChannel = None): - """Set a channel to log infractions to.""" - if channel: - await config.guild(ctx.guild).log_channel.set(channel.id) - await ctx.send(f"Logging channel set to {channel.mention}.") - else: - await config.guild(ctx.guild).log_channel.set(" ") - await ctx.send(warning("Logging channel disabled.")) - - @auroraset.command(name="showmoderator") - @checks.admin() - async def auroraset_showmoderator(self, ctx: commands.Context): - """Toggle if the cog should show the moderator in the case embed when dming a user.""" - await config.guild(ctx.guild).show_moderator.set(not await config.guild(ctx.guild).show_moderator()) - await ctx.send(f"Show moderator setting set to {await config.guild(ctx.guild).show_moderator()}") - - @auroraset.group(autohelp=True, name='import') - @checks.admin() - async def auroraset_import(self, ctx: commands.Context): - """Import moderations from other bots.""" - - @auroraset_import.command(name="aurora") - @checks.admin() - async def auroraset_import_aurora(self, ctx: commands.Context): - """Import moderations from another bot using Aurora.""" - if ctx.message.attachments and ctx.message.attachments[0].content_type == 'application/json; charset=utf-8': - message = await ctx.send(warning("Are you sure you want to import moderations from another bot?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*")) - await message.edit(view=ImportAuroraView(60, ctx, message)) - else: - await ctx.send(error("Please provide a valid Aurora export file.")) - - @auroraset_import.command(name="galacticbot") - @checks.admin() - async def auroraset_import_galacticbot(self, ctx: commands.Context): - """Import moderations from GalacticBot.""" - if ctx.message.attachments and ctx.message.attachments[0].content_type == 'application/json; charset=utf-8': - message = await ctx.send(warning("Are you sure you want to import GalacticBot moderations?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*")) - await message.edit(view=ImportGalacticBotView(60, ctx, message)) - else: - await ctx.send(error("Please provide a valid GalacticBot moderation export file.")) - @commands.command(aliases=["tdc"]) async def timedeltaconvert(self, ctx: commands.Context, *, duration: str): """This command converts a duration to a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) Python object. diff --git a/aurora/configuration/__init__.py b/aurora/configuration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aurora/configuration/commands.py b/aurora/configuration/commands.py new file mode 100644 index 0000000..d562975 --- /dev/null +++ b/aurora/configuration/commands.py @@ -0,0 +1,42 @@ +from redbot.core import commands +from redbot.core.utils.chat_formatting import error, warning + +from .embed import embed +from ..abc import Mixin +from ..importers.aurora import ImportAuroraView +from ..importers.galacticbot import ImportGalacticBotView + +class Configuration(Mixin): + """Configuration commands for Aurora.""" + + @commands.guild_only() + @commands.group(autohelp=True, aliases=['moderationset', 'modset', 'moderationsettings', 'aurorasettings', 'auroraconfig']) + async def auroraset(self, ctx: commands.Context): + """Set Aurora configuration options.""" + await ctx.reply(embed=embed(ctx)) + + @auroraset.group(autohelp=True, name='import') + @commands.admin() + @commands.guild_only() + async def auroraset_import(self, ctx: commands.Context): + """Import moderations from other bots.""" + + @auroraset_import.command(name="aurora") + @commands.admin() + async def auroraset_import_aurora(self, ctx: commands.Context): + """Import moderations from another bot using Aurora.""" + if ctx.message.attachments and ctx.message.attachments[0].content_type == 'application/json; charset=utf-8': + message = await ctx.send(warning("Are you sure you want to import moderations from another bot?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*")) + await message.edit(view=ImportAuroraView(60, ctx, message)) + else: + await ctx.send(error("Please provide a valid Aurora export file.")) + + @auroraset_import.command(name="galacticbot") + @commands.admin() + async def auroraset_import_galacticbot(self, ctx: commands.Context): + """Import moderations from GalacticBot.""" + if ctx.message.attachments and ctx.message.attachments[0].content_type == 'application/json; charset=utf-8': + message = await ctx.send(warning("Are you sure you want to import GalacticBot moderations?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*")) + await message.edit(view=ImportGalacticBotView(60, ctx, message)) + else: + await ctx.send(error("Please provide a valid GalacticBot moderation export file.")) diff --git a/aurora/configuration/embed.py b/aurora/configuration/embed.py new file mode 100644 index 0000000..b5f244a --- /dev/null +++ b/aurora/configuration/embed.py @@ -0,0 +1,111 @@ +from typing import Union + +from discord import Embed, Guild, Member, User +from redbot.core import commands +from redbot.core.utils.chat_formatting import bold, error, warning + +from .utils import get_bool_emoji +from ..utilities.config import config + +async def _core(ctx: commands.Context) -> Embed: + """Generates the core embed for configuration menus to use.""" + embed = Embed( + title="Aurora Configuration Menu", + description="Use the buttons below to configure Aurora.", + color=await ctx.embed_color() + ) + embed.set_thumbnail(url=ctx.bot.user.avatar_url) + return embed + +async def _overrides(user: Union[Member, User]) -> str: + """Generates a configuration menu field value for a user's overrides.""" + + override_settings = { + "ephemeral": await config.user(user).history_ephemeral(), + "inline": await config.user(user).history_inline(), + "inline_pagesize": await config.user(user).history_inline_pagesize(), + "pagesize": await config.user(user).history_pagesize(), + "auto_evidenceformat": await config.user(user).auto_evidenceformat() + } + + overrides = [ + "These settings will override the relevant guild settings.\n", # Add an extra line between the subtitle and the settings + bold("Auto Evidence Format: ") + get_bool_emoji(override_settings['auto_evidenceformat']), + bold("Ephemeral: ") + get_bool_emoji(override_settings['ephemeral']), + bold("Inline: ") + get_bool_emoji(override_settings['inline']), + bold("Inline Pagesize: ") + override_settings['inline_pagesize'] + " cases per page", + bold("Pagesize: ") + override_settings['pagesize'] + " cases per page", + ] + overrides = '\n'.join(overrides) + return overrides + +async def _guild(guild: Guild) -> str: + """Generates a configuration menu field value for a guild's settings.""" + + guild_settings = { + "show_moderator": await config.guild(guild).show_moderator(), + "use_discord_permissions": await config.guild(guild).use_discord_permissions(), + "ignore_modlog": await config.guild(guild).ignore_modlog(), + "ignore_other_bots": await config.guild(guild).ignore_other_bots(), + "dm_users": await config.guild(guild).dm_users(), + "log_channel": await config.guild(guild).log_channel(), + "history_ephemeral": await config.guild(guild).history_ephemeral(), + "history_inline": await config.guild(guild).history_inline(), + "history_pagesize": await config.guild(guild).history_pagesize(), + "history_inline_pagesize": await config.guild(guild).history_inline_pagesize(), + "auto_evidenceformat": await config.guild(guild).auto_evidenceformat(), + } + + channel = 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("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("Log Channel: ") + channel, + 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: ") + guild_settings['history_pagesize'] + " cases per page", + bold("History Inline Pagesize: ") + guild_settings['history_inline_pagesize'] + " cases per page" + ] + guild_str = '\n'.join(guild_str) + return guild_str + +async def _blacklist(guild: Guild) -> str: + """Generates a configuration menu field value for a guild's blacklist.""" + + blacklist = await config.guild(guild).blacklist_roles() + if blacklist: + blacklist = [guild.get_role(role).mention or error(f"`{role}` (Not Found)") for role in blacklist] + blacklist = '\n'.join(blacklist) + else: + blacklist = warning("No roles are set as blacklist roles!") + return blacklist + +async def _immune(guild: Guild) -> str: + """Generates a configuration menu field value for a guild's immune roles.""" + + immune = await config.guild(guild).immune_roles() + if immune: + immune = [guild.get_role(role).mention or error(f"`{role}` (Not Found)") for role in immune] + immune = '\n'.join(immune) + else: + immune = warning("No roles are set as immune roles!") + return immune + +async def embed(ctx: commands.Context) -> Embed: + """Generates the configuration embed for a guild.""" + embed = await _core(ctx) + embed.add_field(name="User Overrides", value=await _overrides(ctx.author)) + if ctx.guild is not None and (ctx.author.guild_permissions.administrator or ctx.author.guild_permissions.manage_guild): + embed.add_field(name="Guild Settings", value=await _guild(ctx.guild)) + embed.add_field(name="Blacklist Roles", value=await _blacklist(ctx.guild)) + embed.add_field(name="Immune Roles", value=await _immune(ctx.guild)) + return embed diff --git a/aurora/configuration/menus/__init__.py b/aurora/configuration/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aurora/configuration/menus/blacklist.py b/aurora/configuration/menus/blacklist.py new file mode 100644 index 0000000..e69de29 diff --git a/aurora/configuration/menus/immune.py b/aurora/configuration/menus/immune.py new file mode 100644 index 0000000..e69de29 diff --git a/aurora/configuration/utils.py b/aurora/configuration/utils.py new file mode 100644 index 0000000..6e70e48 --- /dev/null +++ b/aurora/configuration/utils.py @@ -0,0 +1,8 @@ +def get_bool_emoji(value: bool) -> str: + """Returns a unicode emoji based on a boolean value.""" + if value is True: + return "\N{WHITE HEAVY CHECK MARK}" + if value is False: + return "\N{NO ENTRY SIGN}" + if value is None: + return "\N{BLACK QUESTION MARK ORNAMENT}\N{VARIATION SELECTOR-16}"