From 39b22c9514000e0245068db2e2fe198fe5858caf Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Mar 2026 15:59:42 -0700 Subject: [PATCH] fix: correct announcement API endpoint and add optional content fields (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fixes the announcement API URL from `/announcement` to `/api/announcement` - Adds three optional additional content text inputs (Parts 2–4) to the announcement modal - The bot concatenates all non-empty content fields with double newlines before forwarding to the API - Only the first content field and the category selector remain required ## Test Plan - [ ] Run `/announcement` and verify the modal opens with four content fields and a category selector - [ ] Submit with only the first content field filled — verify it sends correctly - [ ] Submit with multiple content fields filled — verify they are concatenated in the API request - [ ] Verify the API no longer returns HTTP 405 Reviewed-on: https://git.nhcarrigan.com/nhcarrigan/hikari/pulls/20 Co-authored-by: Hikari Co-committed-by: Hikari --- bot/src/commands/announcement.ts | 38 ++++++++++++++++++++-- bot/src/config/entitlements.ts | 8 ++--- bot/src/modules/handleAnnouncementModal.ts | 29 ++++++++++++----- bot/src/utils/checkEntitlement.ts | 4 +-- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/bot/src/commands/announcement.ts b/bot/src/commands/announcement.ts index 683e986..80bdbf8 100644 --- a/bot/src/commands/announcement.ts +++ b/bot/src/commands/announcement.ts @@ -11,7 +11,7 @@ import { TextInputBuilder, TextInputStyle, } from "discord.js"; -import { entitledUsers } from "../config/entitlements.js"; +import { naomiId } from "../config/entitlements.js"; import { errorHandler } from "../utils/errorHandler.js"; import type { Command } from "../interfaces/command.js"; @@ -21,9 +21,10 @@ import type { Command } from "../interfaces/command.js"; * @param _hikari - Hikari's Discord instance (unused). * @param interaction - The command interaction payload from Discord. */ +// eslint-disable-next-line max-lines-per-function -- Modal requires many input components export const announcement: Command = async(_hikari, interaction) => { try { - if (!entitledUsers.includes(interaction.user.id)) { + if (interaction.user.id !== naomiId) { await interaction.reply({ content: "This command is restricted to the owner.", ephemeral: true, @@ -41,6 +42,24 @@ export const announcement: Command = async(_hikari, interaction) => { setMaxLength(4000). setRequired(true); + const contentInput2 = new TextInputBuilder(). + setCustomId("content_2"). + setStyle(TextInputStyle.Paragraph). + setMaxLength(4000). + setRequired(false); + + const contentInput3 = new TextInputBuilder(). + setCustomId("content_3"). + setStyle(TextInputStyle.Paragraph). + setMaxLength(4000). + setRequired(false); + + const contentInput4 = new TextInputBuilder(). + setCustomId("content_4"). + setStyle(TextInputStyle.Paragraph). + setMaxLength(4000). + setRequired(false); + const categorySelect = new StringSelectMenuBuilder(). setCustomId("category"). setPlaceholder("Select a category"). @@ -55,12 +74,27 @@ export const announcement: Command = async(_hikari, interaction) => { "Your version of the announcement, to send to the AI for processing.", ). setTextInputComponent(contentInput); + // eslint-disable-next-line stylistic/max-len -- Label chain exceeds line length limit + const contentLabel2 = new LabelBuilder().setLabel("Additional Copy (Part 2)"). + setDescription("Optional continuation of your announcement copy."). + setTextInputComponent(contentInput2); + // eslint-disable-next-line stylistic/max-len -- Label chain exceeds line length limit + const contentLabel3 = new LabelBuilder().setLabel("Additional Copy (Part 3)"). + setDescription("Optional continuation of your announcement copy."). + setTextInputComponent(contentInput3); + // eslint-disable-next-line stylistic/max-len -- Label chain exceeds line length limit + const contentLabel4 = new LabelBuilder().setLabel("Additional Copy (Part 4)"). + setDescription("Optional continuation of your announcement copy."). + setTextInputComponent(contentInput4); const categoryLabel = new LabelBuilder().setLabel("Announcement Category"). setDescription("The category of the announcement."). setStringSelectMenuComponent(categorySelect); modal.addLabelComponents( contentLabel, + contentLabel2, + contentLabel3, + contentLabel4, categoryLabel, ); diff --git a/bot/src/config/entitlements.ts b/bot/src/config/entitlements.ts index 36ca468..dada84a 100644 --- a/bot/src/config/entitlements.ts +++ b/bot/src/config/entitlements.ts @@ -3,12 +3,12 @@ * @license Naomi's Public License * @author Naomi Carrigan */ +const naomiId = "465650873650118659"; + const entitledGuilds = [ "1354624415861833870", ]; -const entitledUsers = [ - "465650873650118659", -]; +const entitledUsers = [ naomiId ]; -export { entitledGuilds, entitledUsers }; +export { entitledGuilds, entitledUsers, naomiId }; diff --git a/bot/src/modules/handleAnnouncementModal.ts b/bot/src/modules/handleAnnouncementModal.ts index d931939..dafa3a1 100644 --- a/bot/src/modules/handleAnnouncementModal.ts +++ b/bot/src/modules/handleAnnouncementModal.ts @@ -87,26 +87,37 @@ const buildAnnouncementFiles = (rawPost: RawPost): Array => { * to the owner's DMs, and replies ephemerally with the platform recap. * @param interaction - The modal submit interaction payload from Discord. */ +// eslint-disable-next-line max-lines-per-function -- This is a big function. export const handleAnnouncementModal = async( interaction: ModalSubmitInteraction, ): Promise => { try { await interaction.deferReply({ ephemeral: true }); - const content = interaction.fields.getTextInputValue("content"); + const content = [ + interaction.fields.getTextInputValue("content"), + interaction.fields.getTextInputValue("content_2"), + interaction.fields.getTextInputValue("content_3"), + interaction.fields.getTextInputValue("content_4"), + ].filter((part) => { + return part.length > 0; + }).join("\n\n"); const categoryValues = interaction.fields.getStringSelectValues("category"); const type = categoryValues[0] ?? "company"; - const response = await fetch("https://hikari.nhcarrigan.com/announcement", { - body: JSON.stringify({ content, type }), - headers: { + const response = await fetch( + "https://hikari.nhcarrigan.com/api/announcement", + { + body: JSON.stringify({ content, type }), + headers: { // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header capitalisation convention - "Authorization": process.env.ANNOUNCEMENT_TOKEN ?? "", - // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header naming convention - "Content-Type": "application/json", + "Authorization": process.env.ANNOUNCEMENT_TOKEN ?? "", + // eslint-disable-next-line @typescript-eslint/naming-convention -- HTTP header naming convention + "Content-Type": "application/json", + }, + method: "POST", }, - method: "POST", - }); + ); if (!response.ok) { await interaction.editReply({ diff --git a/bot/src/utils/checkEntitlement.ts b/bot/src/utils/checkEntitlement.ts index be27113..828815e 100644 --- a/bot/src/utils/checkEntitlement.ts +++ b/bot/src/utils/checkEntitlement.ts @@ -4,7 +4,7 @@ * @author Naomi Carrigan */ -import { entitledGuilds, entitledUsers } from "../config/entitlements.js"; +import { entitledGuilds, naomiId } from "../config/entitlements.js"; import type { Client, Guild, User } from "discord.js"; /** @@ -17,7 +17,7 @@ const checkUserEntitlement = async( hikari: Client, user: User, ): Promise => { - if (entitledUsers.includes(user.id)) { + if (user.id === naomiId) { return true; } const entitlements = await hikari.application?.entitlements.fetch({