feat(aurora): starting work on the config rewrite

This commit is contained in:
Seaswimmer 2024-01-13 15:44:08 +00:00
parent a1b038057e
commit c42b4eca2d
Signed by untrusted user: cswimr
GPG key ID: D74DDDDF420E13DF
9 changed files with 192 additions and 325 deletions

28
aurora/abc.py Normal file
View file

@ -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

View file

@ -20,6 +20,8 @@ from redbot.core import app_commands, checks, commands, data_manager
from redbot.core.app_commands import Choice from redbot.core.app_commands import Choice
from redbot.core.utils.chat_formatting import box, error, warning 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.galacticbot import ImportGalacticBotView
from .importers.aurora import ImportAuroraView from .importers.aurora import ImportAuroraView
from .utilities.config import config, register_config 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 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. """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. 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.""" 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 completion_time = (time.time() - current_time) * 1000
logger.debug("Completed expiry loop in %sms with %s users unbanned", f"{completion_time:.6f}", global_num) 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"]) @commands.command(aliases=["tdc"])
async def timedeltaconvert(self, ctx: commands.Context, *, duration: str): 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. """This command converts a duration to a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) Python object.

View file

View file

@ -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."))

View file

@ -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

View file

View file

View file

View file

@ -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}"