diff --git a/commands.json b/commands.json index b0eb9cc..67bbbfb 100644 --- a/commands.json +++ b/commands.json @@ -1,86 +1,87 @@ [ - { - "name": "onboard-mentee", - "description": "Onboard a new mentee to the nhcarrigan-mentorship GitHub org", - "options": [ - { - "name": "mentee", - "description": "The mentee's Discord user", - "type": 6, - "required": true - }, - { - "name": "mentee_name", - "description": "The mentee's full name", - "type": 3, - "required": true - }, - { - "name": "github_username", - "description": "The mentee's GitHub username", - "type": 3, - "required": true - } - ] - }, - { - "name": "create-task", - "description": "Create an AI-augmented task on Leantime", - "options": [ - { - "name": "title", - "description": "The task title", - "type": 3, - "required": true - }, - { - "name": "description", - "description": "Additional context for the task (AI will expand this)", - "type": 3, - "required": false - }, - { - "name": "priority", - "description": "Task priority level", - "type": 4, - "required": false, - "choices": [ - { "name": "Urgent", "value": 1 }, - { "name": "High", "value": 2 }, - { "name": "Medium", "value": 3 }, - { "name": "Low", "value": 4 } - ] - } - ] - }, { "name": "create-issue", - "description": "Create an AI-augmented issue on a Gitea repository", + "type": 1, + "description": "Creates a Gitea issue with an AI-generated body.", "options": [ { "name": "owner", - "description": "The repository owner", + "description": "The owner of the repository.", "type": 3, "required": true }, { "name": "repo", - "description": "The repository name", + "description": "The name of the repository.", "type": 3, "required": true }, { "name": "title", - "description": "The issue title", + "description": "The issue title.", "type": 3, "required": true }, { "name": "description", - "description": "Additional context for the issue (AI will expand this)", + "description": "Optional additional context for the issue body.", "type": 3, "required": false } ] + }, + { + "name": "create-task", + "type": 1, + "description": "Creates a Leantime task with an AI-generated description.", + "options": [ + { + "name": "title", + "description": "The task title.", + "type": 3, + "required": true + }, + { + "name": "description", + "description": "Optional additional context for the task description.", + "type": 3, + "required": false + }, + { + "name": "priority", + "description": "The task priority level (1-5, default 3).", + "type": 4, + "required": false + } + ] + }, + { + "name": "onboard-mentee", + "type": 1, + "description": "Onboards a new mentee by setting up their GitHub repository.", + "options": [ + { + "name": "mentee_name", + "description": "The mentee's full name.", + "type": 3, + "required": true + }, + { + "name": "github_username", + "description": "The mentee's GitHub username.", + "type": 3, + "required": true + }, + { + "name": "mentee", + "description": "The mentee's Discord account.", + "type": 6, + "required": true + } + ] + }, + { + "name": "Forward to Naomi", + "type": 3 } ] diff --git a/src/commands/createTask.ts b/src/commands/createTask.ts index 1b206ec..ba5d528 100644 --- a/src/commands/createTask.ts +++ b/src/commands/createTask.ts @@ -43,7 +43,7 @@ const generateTaskDescription = async( system: taskSystemPrompt, }); const [ firstContent ] = aiResponse.content; - return firstContent.type === "text" + return firstContent?.type === "text" ? firstContent.text : description; }; diff --git a/src/commands/forwardToOwner.ts b/src/commands/forwardToOwner.ts index b638c28..b15683e 100644 --- a/src/commands/forwardToOwner.ts +++ b/src/commands/forwardToOwner.ts @@ -5,47 +5,21 @@ */ import { - ContextMenuCommandBuilder, - ApplicationCommandType, - type MessageContextMenuCommandInteraction, DiscordAPIError, - ButtonBuilder, - EmbedBuilder, - ActionRowBuilder, - ButtonStyle, - type Message, + MessageFlags, + type MessageContextMenuCommandInteraction, } from "discord.js"; import { ids } from "../config/ids.js"; +import { getComponentsForNaomi } from "../utils/getComponentsForNaomi.js"; +import { logger } from "../utils/logger.js"; -const buildForwardedEmbed = (message: Message): EmbedBuilder => { - const forwardedEmbed = new EmbedBuilder(). - setColor(0x58_65_F2). - setTitle(`Message from ${String(message.author.tag)}!`). - setDescription( - `${(message.attachments.size > 0 - ? `**Attachments:** ${String(message.attachments.size)} - file(s)\n\n` - : "\n") - + (message.embeds.length > 0 - ? `**Embeds:** ${String(message.embeds.length)}\n\n` - : "")} - \n${message.content}\n\n`, - ); - return forwardedEmbed; -}; -const buildViewButtonFunction = (message: Message): ButtonBuilder => { - const viewButton = new ButtonBuilder(). - setLabel("View Message"). - setURL(message.url). - setStyle(ButtonStyle.Link); - return viewButton; -}; - -const data = new ContextMenuCommandBuilder().setName("Forward to Naomi"). - setType(ApplicationCommandType.Message); - -const execute = async(interaction: MessageContextMenuCommandInteraction): -Promise => { +/** + * Forwards a message to Naomi via DM using a context menu command. + * @param interaction -- The message context menu interaction. + */ +const forwardToOwner = async( + interaction: MessageContextMenuCommandInteraction, +): Promise => { await interaction.deferReply({ ephemeral: true }); if (interaction.user.id !== ids.users.naomi) { @@ -63,33 +37,26 @@ Promise => { try { const naomi = await interaction.client.users.fetch(ids.users.naomi); - const forwardedEmbed = buildForwardedEmbed(message); - const viewButton = buildViewButtonFunction(message); - - const row = new ActionRowBuilder().addComponents( - viewButton, - ); await naomi.send({ - components: [ row ], - embeds: [ forwardedEmbed ], - files: message.attachments.map((att) => { - return att.url; - }), - }); - - await interaction.editReply({ - content: "✅ Forwarded to your DMs!", + components: getComponentsForNaomi( + message.author, + message.content, + message.url, + ), + flags: [ MessageFlags.IsComponentsV2 ], }); + await logger.metric("forwarded_message", 1, { user: message.author.id }); + await interaction.editReply({ content: "✅ Forwarded to your DMs!" }); } catch (error) { let replyText = "❌ Failed to forward message."; if (error instanceof DiscordAPIError && error.code === 50_007) { replyText = `${replyText} (Naomi's DMs might be closed)`; } + if (error instanceof Error) { + await logger.error("forwardToOwner command", error); + } await interaction.editReply(replyText); } }; -export const forwardOwnerDM = { - data, - execute, -}; +export { forwardToOwner }; diff --git a/src/commands/onboardMentee.ts b/src/commands/onboardMentee.ts index ac62f54..bedd8ed 100644 --- a/src/commands/onboardMentee.ts +++ b/src/commands/onboardMentee.ts @@ -92,7 +92,9 @@ export const onboardMentee = async( content: `✅ Successfully onboarded **${menteeName}**!\nRepository: ${repoUrl}`, }); } catch (error) { - await logger.log("error", `Failed to onboard mentee: ${String(error)}`); + await logger.error("onboardmentee command", error instanceof Error + ? error + : new Error(String(error))); await interaction.editReply({ content: `❌ Failed to onboard mentee: ${String(error)}`, }); diff --git a/src/events/handleInteractionCreate.ts b/src/events/handleInteractionCreate.ts new file mode 100644 index 0000000..c66230b --- /dev/null +++ b/src/events/handleInteractionCreate.ts @@ -0,0 +1,85 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { + MessageFlags, + type ChatInputCommandInteraction, + type Interaction, +} from "discord.js"; +import { createIssue } from "../commands/createIssue.js"; +import { createTask } from "../commands/createTask.js"; +import { forwardToOwner } from "../commands/forwardToOwner.js"; +import { onboardMentee } from "../commands/onboardMentee.js"; +import { ids } from "../config/ids.js"; +import type { Amari } from "../interfaces/amari.js"; + +/** + * Routes a chat input command to the appropriate handler. + * @param amari -- Amari's instance. + * @param interaction -- The incoming slash command to dispatch. + */ +const handleChatInputCommand = ( + amari: Amari, + interaction: ChatInputCommandInteraction, +): void => { + const { commandName } = interaction; + if (commandName === "onboard-mentee") { + void onboardMentee(amari, interaction); + return; + } + if (commandName === "create-task") { + void createTask(interaction); + return; + } + if (commandName === "create-issue") { + void createIssue(interaction); + } +}; + +/** + * Handles the interaction create event from Discord. + * Bootstraps all of our custom interaction logic. + * @param amari -- Amari's instance. + * @param interaction -- The incoming Discord gateway event to dispatch. + */ +export const handleInteractionCreate = ( + amari: Amari, + interaction: Interaction, +): void => { + if ( + interaction.isMessageContextMenuCommand() + && interaction.commandName === "Forward to Naomi" + ) { + void forwardToOwner(interaction); + return; + } + + if (interaction.isButton() && interaction.customId === "resolve") { + if (interaction.user.id !== ids.users.naomi) { + void interaction.reply({ + content: "Who are you????", + flags: [ MessageFlags.Ephemeral ], + }); + return; + } + void interaction.message.delete(); + return; + } + + if (interaction.isChatInputCommand()) { + handleChatInputCommand(amari, interaction); + return; + } + + if (interaction.isAutocomplete()) { + return; + } + + void interaction.reply({ + content: "What?", + flags: [ MessageFlags.Ephemeral ], + }); +}; diff --git a/src/index.ts b/src/index.ts index 69d3d51..ab0d1a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,15 +10,11 @@ import { GatewayIntentBits, Events, Partials, - MessageFlags, } from "discord.js"; import { scheduleJob } from "node-schedule"; import { App } from "octokit"; -import { createIssue } from "./commands/createIssue.js"; -import { createTask } from "./commands/createTask.js"; -import { forwardOwnerDM } from "./commands/forwardToOwner.js"; -import { onboardMentee } from "./commands/onboardMentee.js"; import { ids } from "./config/ids.js"; +import { handleInteractionCreate } from "./events/handleInteractionCreate.js"; import { handleMessageCreate } from "./events/handleMessageCreate.js"; import { cacheData } from "./modules/cacheData.js"; import { checkRetroAchievements } from "./modules/checkAchievements.js"; @@ -117,47 +113,7 @@ amari.discord.on(Events.MessageCreate, (message) => { amari.discord.on(Events.InteractionCreate, (interaction) => { void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); - - if ( - interaction.isMessageContextMenuCommand() - && interaction.commandName === "Forward to Naomi" - ) { - void forwardOwnerDM.execute(interaction); - return; - } - - if (interaction.isButton() && interaction.customId === "resolve") { - if (interaction.user.id !== ids.users.naomi) { - void interaction.reply({ - content: "Who are you????", - flags: [ MessageFlags.Ephemeral ], - }); - return; - } - - void interaction.message.delete(); - return; - } - if (interaction.isChatInputCommand()) { - const { commandName } = interaction; - if (commandName === "onboard-mentee") { - return void onboardMentee(amari, interaction); - } - if (commandName === "create-task") { - return void createTask(interaction); - } - if (commandName === "create-issue") { - return void createIssue(interaction); - } - } - if (interaction.isAutocomplete()) { - void interaction; - return; - } - void interaction.reply({ - content: "What?", - flags: [ MessageFlags.Ephemeral ], - }); + handleInteractionCreate(amari, interaction); }); amari.discord.on(Events.ThreadCreate, (thread) => { diff --git a/src/scripts/deployGlobal.ts b/src/scripts/deployGlobal.ts deleted file mode 100644 index deebd9f..0000000 --- a/src/scripts/deployGlobal.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @copyright NHCarrigan - * @license Naomi's Public License - * @author Teklu Abayneh - */ - -import { REST, Routes } from "discord.js"; -import { forwardOwnerDM } from "../commands/forwardToOwner.js"; -import { logger } from "../utils/logger.js"; - -const commands = [ forwardOwnerDM.data.toJSON() ]; -const token = process.env.BOT_TOKEN; -const clientId = process.env.GH_CLIENT_ID; - -if (token === undefined) { - throw new Error("BOT_TOKEN is missing from environment variables!"); -} -if (clientId === undefined) { - throw new Error("CLIENT_ID is missing from environment variables!"); -} - -const rest = new REST({ version: "10" }).setToken(token); -const requestCommand = async(): Promise => { - try { - await rest.put(Routes.applicationCommands(clientId), { body: commands }); - } catch (error) { - if (error instanceof Error) { - await logger.error("operation", error); - } - } -}; -void requestCommand();