From ece19e948c26ac0d7d7a7a994e9caf229bccea4a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:03:25 -0700 Subject: [PATCH 1/4] Add a modal and /factoid add command --- modules/operation/factoids.py | 171 +++++++++++++++++++++++++++++++--- 1 file changed, 158 insertions(+), 13 deletions(-) diff --git a/modules/operation/factoids.py b/modules/operation/factoids.py index 008f8ecb..47d8d491 100644 --- a/modules/operation/factoids.py +++ b/modules/operation/factoids.py @@ -233,6 +233,7 @@ async def create_factoid_call( message: str, embed_config: str, alias: str = None, + properties: list[bool] = None, ) -> None: """Calls the DB to create a factoid @@ -242,11 +243,16 @@ async def create_factoid_call( message (str): Message the factoid should send embed_config (str): Whether the factoid has an embed set up alias (str, optional): The parent factoid. Defaults to None. + properties (list[bool]): A list of true/false for properties. Defaults to None. + 0: Disabled, 1: Hidden, 2: Protected, 3: Restricted Raises: TooLongFactoidMessageError: When the message argument is over 2k chars, discords limit """ + if not properties: + properties = [False, False, False, False] + if len(message) > 2000: raise custom_errors.TooLongFactoidMessageError @@ -260,6 +266,10 @@ async def create_factoid_call( message=message, embed_config=embed_config, alias=alias, + disabled=properties[0], + hidden=properties[1], + protected=properties[2], + restricted=properties[3], ) await factoid.create() @@ -299,13 +309,18 @@ async def modify_factoid_call( # -- Utility -- async def confirm_factoid_deletion( - self: Self, factoid_name: str, ctx: commands.Context, fmt: str + self: Self, + factoid_name: str, + channel: discord.abc.GuildChannel, + author: discord.Member, + fmt: str, ) -> bool: """Confirms if a factoid should be deleted/modified Args: factoid_name (str): The factoid that is being prompted for deletion - ctx (commands.Context): Used to return the message + channel (discord.abc.GuildChannel): The channel the factoid is being deleted in + author (discord.Member): The member deleting the factoid fmt (str): Formatting for the returned message Returns: @@ -317,8 +332,8 @@ async def confirm_factoid_deletion( message=( f"The factoid `{factoid_name}` already exists. Should I overwrite it?" ), - channel=ctx.channel, - author=ctx.author, + channel=channel, + author=author, ) await view.wait() @@ -328,7 +343,7 @@ async def confirm_factoid_deletion( if view.value is ui.ConfirmResponse.DENIED: await auxiliary.send_deny_embed( message=f"The factoid `{factoid_name}` was not {fmt}.", - channel=ctx.channel, + channel=channel, ) return False @@ -608,7 +623,8 @@ async def get_list_of_aliases( async def add_factoid( self: Self, - ctx: commands.Context, + channel: discord.abc.Messageable, + author: discord.Member, factoid_name: str, guild: str, message: str, @@ -619,6 +635,8 @@ async def add_factoid( Args: ctx (commands.Context): The context used for the confirmation message + channel (discord.abc.Messageable): The channel the factoid was added from + author (discord.Member): The member who created this factoid factoid_name (str): The name of the factoid guild (str): The guild of the factoid message (str): The message of the factoid @@ -634,7 +652,7 @@ async def add_factoid( if factoid.protected: await auxiliary.send_deny_embed( message=f"`{factoid.name}` is protected and cannot be modified", - channel=ctx.channel, + channel=channel, ) return name = factoid.name.lower() # Name of the parent @@ -645,7 +663,7 @@ async def add_factoid( if not message: await auxiliary.send_deny_embed( message="You did not provide the factoid message!", - channel=ctx.channel, + channel=channel, ) return @@ -661,11 +679,14 @@ async def add_factoid( else: fmt = "modified" # Confirms modification - if await self.confirm_factoid_deletion(factoid_name, ctx, fmt) is False: + if ( + await self.confirm_factoid_deletion(factoid_name, channel, author, fmt) + is False + ): return # Modifies the old entry - factoid = await self.get_raw_factoid_entry(name, str(ctx.guild.id)) + factoid = await self.get_raw_factoid_entry(name, str(channel.guild.id)) factoid.name = name # if no message was supplied, keep the original factoid's message. if message: @@ -678,7 +699,7 @@ async def add_factoid( await self.handle_cache(guild, name) await auxiliary.send_confirm_embed( message=f"Successfully {fmt} the factoid `{factoid_name}`", - channel=ctx.channel, + channel=channel, ) async def delete_factoid( @@ -1088,6 +1109,71 @@ async def factoid_call_command( sent_message, interaction.user, interaction.channel, factoid.message ) + @factoid_app_group.command( + name="add", + description="Creates a new factoid.", + ) + async def factoid_add_command( + self: Self, interaction: discord.Interaction, factoid_name: str + ) -> None: + query = factoid_name.replace("\n", " ").split(" ")[0].lower() + try: + await self.get_factoid(query, str(interaction.guild.id)) + embed = auxiliary.prepare_deny_embed( + message=f"The factoid `{factoid_name}` already exists" + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + except custom_errors.FactoidNotFoundError: + ... + + form = NewFactoid(factoid_name) + await interaction.response.send_modal(form) + await form.wait() + + embed_json_string = "" + + if form.embed.component.values: + embed_file: discord.Attachment = form.embed.component.values[0] + if not embed_file.filename.endswith(".json"): + embed = auxiliary.prepare_deny_embed( + message="I don't recognize your upload as a json file", + ) + await interaction.followup.send(embed=embed) + return + + try: + json_bytes = await embed_file.read() + attachment_json = json.loads(json_bytes.decode("UTF-8")) + embed_json_string = json.dumps(attachment_json) + except Exception: + embed = auxiliary.prepare_deny_embed( + message="I couldn't parse the uploaded JSON file.", + ) + await interaction.followup.send(embed=embed) + return + + selected = set(form.properties.component.values) + properties = [ + "disabled" in selected, + "hidden" in selected, + "protected" in selected, + "restricted" in selected, + ] + + await self.create_factoid_call( + factoid_name=factoid_name, + guild=str(interaction.guild.id), + message=form.plaintext.component.value, + embed_config=embed_json_string if embed_json_string else "", + properties=properties, + ) + embed = auxiliary.prepare_confirm_embed( + message=f"Your factoid `{factoid_name}` was successfully created!", + ) + await interaction.followup.send(embed=embed) + # -- Factoid job related functions -- async def kickoff_jobs(self: Self) -> None: """Gets a list of cron jobs and starts them""" @@ -1323,7 +1409,8 @@ async def remember( message = None await self.add_factoid( - ctx, + ctx.channel, + ctx.author, factoid_name=factoid_name, guild=str(ctx.guild.id), message=message, @@ -2309,7 +2396,9 @@ async def alias( return # Confirms deletion of old entry - if not await self.confirm_factoid_deletion(alias_name, ctx, "replaced"): + if not await self.confirm_factoid_deletion( + alias_name, ctx.channel, ctx.author, "replaced" + ): return # If the target entry is the parent @@ -2884,3 +2973,59 @@ async def delete_button( if interaction.message: await interaction.message.delete() + + +class NewFactoid(discord.ui.Modal): + """A Modal that contains information to make a new factoid + This has the user fill in plaintext content, upload an embed json file + And select default properties for the factoid + + Args: + factoid (str): The name of the factoid, to display in the title + + Attributes: + plaintext (discord.ui.Label): The plaintext representation of the factoid + embed (discord.ui.Label): The json file attachment of the factoid + properties (discord.ui.Label): The properties of the factoid, such as hidden or disabled + """ + + def __init__(self: Self, factoid: str) -> None: + super().__init__(title=f"Creating factoid {factoid}") + + plaintext: discord.ui.Label = discord.ui.Label( + text="Plaintext:", + component=discord.ui.TextInput(style=discord.TextStyle.long, required=True), + ) + embed: discord.ui.Label = discord.ui.Label( + text="Embed json:", component=discord.ui.FileUpload(required=False) + ) + properties: discord.ui.Label = discord.ui.Label( + text="Properties:", + component=discord.ui.CheckboxGroup( + max_values=4, + required=False, + options=[ + discord.CheckboxGroupOption( + default=False, label="Disabled", value="disabled" + ), + discord.CheckboxGroupOption( + default=False, label="Hidden", value="hidden" + ), + discord.CheckboxGroupOption( + default=False, label="Protected", value="protected" + ), + discord.CheckboxGroupOption( + default=False, label="Restricted", value="restricted" + ), + ], + ), + ) + + async def on_submit(self: Self, interaction: discord.Interaction) -> None: + """What happens when the form has been successfully submitted + + Args: + interaction (discord.Interaction): The interaction that caused the form to be show + """ + await interaction.response.defer() + return From 52eeef62a426add01d29d12f879068c13eea7379 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:04:34 -0700 Subject: [PATCH 2/4] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 63070e44..9f1fbcfa 100644 --- a/changelog.md +++ b/changelog.md @@ -86,6 +86,7 @@ Changes since 2026.06.04 ### Factoid - The /factoid call command now has an optional parameter to ping a member in the factoid display - Factoids called using /factoid call will now have a button to allow the invoker to delete the factoid +- A new /factoid add command has been added, using a modal to create new factoids ### Forum - This changes the way the first message in a forum channel is obtained for initial post rejection detection From 344341fd919695f62b7087d77b9595fcdc1188d9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:06:20 -0700 Subject: [PATCH 3/4] Formatting --- modules/operation/factoids.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/operation/factoids.py b/modules/operation/factoids.py index 47d8d491..417e65d0 100644 --- a/modules/operation/factoids.py +++ b/modules/operation/factoids.py @@ -1116,6 +1116,12 @@ async def factoid_call_command( async def factoid_add_command( self: Self, interaction: discord.Interaction, factoid_name: str ) -> None: + """A /factoid add command, to add a factoid using a Modal + + Args: + interaction (discord.Interaction): The interaction that called this command + factoid_name (str): The name of the factoid to add + """ query = factoid_name.replace("\n", " ").split(" ")[0].lower() try: await self.get_factoid(query, str(interaction.guild.id)) From 8d6482bd80e2a696633009c3013ea5ab57e3dcae Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:09:24 -0700 Subject: [PATCH 4/4] Update docstrings --- modules/operation/factoids.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/operation/factoids.py b/modules/operation/factoids.py index 417e65d0..9e22159b 100644 --- a/modules/operation/factoids.py +++ b/modules/operation/factoids.py @@ -244,7 +244,7 @@ async def create_factoid_call( embed_config (str): Whether the factoid has an embed set up alias (str, optional): The parent factoid. Defaults to None. properties (list[bool]): A list of true/false for properties. Defaults to None. - 0: Disabled, 1: Hidden, 2: Protected, 3: Restricted + 0 Disabled, 1 Hidden, 2 Protected, 3 Restricted Raises: TooLongFactoidMessageError: @@ -634,7 +634,6 @@ async def add_factoid( """Adds a factoid with confirmation, modifies it if it already exists Args: - ctx (commands.Context): The context used for the confirmation message channel (discord.abc.Messageable): The channel the factoid was added from author (discord.Member): The member who created this factoid factoid_name (str): The name of the factoid