generated from nhcarrigan/template
refactor: align commands and interaction handler with project style
- Replace forwardToOwner embed approach with getComponentsForNaomi + Components V2 - Extract InteractionCreate handler to src/events/handleInteractionCreate.ts - Replace deployGlobal.ts script with static commands.json payload - Fix minor error handling in createTask and onboardMentee
This commit is contained in:
+60
-59
@@ -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",
|
"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": [
|
"options": [
|
||||||
{
|
{
|
||||||
"name": "owner",
|
"name": "owner",
|
||||||
"description": "The repository owner",
|
"description": "The owner of the repository.",
|
||||||
"type": 3,
|
"type": 3,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "repo",
|
"name": "repo",
|
||||||
"description": "The repository name",
|
"description": "The name of the repository.",
|
||||||
"type": 3,
|
"type": 3,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "title",
|
"name": "title",
|
||||||
"description": "The issue title",
|
"description": "The issue title.",
|
||||||
"type": 3,
|
"type": 3,
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"description": "Additional context for the issue (AI will expand this)",
|
"description": "Optional additional context for the issue body.",
|
||||||
"type": 3,
|
"type": 3,
|
||||||
"required": false
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const generateTaskDescription = async(
|
|||||||
system: taskSystemPrompt,
|
system: taskSystemPrompt,
|
||||||
});
|
});
|
||||||
const [ firstContent ] = aiResponse.content;
|
const [ firstContent ] = aiResponse.content;
|
||||||
return firstContent.type === "text"
|
return firstContent?.type === "text"
|
||||||
? firstContent.text
|
? firstContent.text
|
||||||
: description;
|
: description;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,47 +5,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContextMenuCommandBuilder,
|
|
||||||
ApplicationCommandType,
|
|
||||||
type MessageContextMenuCommandInteraction,
|
|
||||||
DiscordAPIError,
|
DiscordAPIError,
|
||||||
ButtonBuilder,
|
MessageFlags,
|
||||||
EmbedBuilder,
|
type MessageContextMenuCommandInteraction,
|
||||||
ActionRowBuilder,
|
|
||||||
ButtonStyle,
|
|
||||||
type Message,
|
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { ids } from "../config/ids.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().
|
* Forwards a message to Naomi via DM using a context menu command.
|
||||||
setColor(0x58_65_F2).
|
* @param interaction -- The message context menu interaction.
|
||||||
setTitle(`Message from ${String(message.author.tag)}!`).
|
*/
|
||||||
setDescription(
|
const forwardToOwner = async(
|
||||||
`${(message.attachments.size > 0
|
interaction: MessageContextMenuCommandInteraction,
|
||||||
? `**Attachments:** ${String(message.attachments.size)}
|
): Promise<void> => {
|
||||||
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<void> => {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
if (interaction.user.id !== ids.users.naomi) {
|
if (interaction.user.id !== ids.users.naomi) {
|
||||||
@@ -63,33 +37,26 @@ Promise<void> => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const naomi = await interaction.client.users.fetch(ids.users.naomi);
|
const naomi = await interaction.client.users.fetch(ids.users.naomi);
|
||||||
const forwardedEmbed = buildForwardedEmbed(message);
|
|
||||||
const viewButton = buildViewButtonFunction(message);
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
||||||
viewButton,
|
|
||||||
);
|
|
||||||
await naomi.send({
|
await naomi.send({
|
||||||
components: [ row ],
|
components: getComponentsForNaomi(
|
||||||
embeds: [ forwardedEmbed ],
|
message.author,
|
||||||
files: message.attachments.map((att) => {
|
message.content,
|
||||||
return att.url;
|
message.url,
|
||||||
}),
|
),
|
||||||
});
|
flags: [ MessageFlags.IsComponentsV2 ],
|
||||||
|
|
||||||
await interaction.editReply({
|
|
||||||
content: "✅ Forwarded to your DMs!",
|
|
||||||
});
|
});
|
||||||
|
await logger.metric("forwarded_message", 1, { user: message.author.id });
|
||||||
|
await interaction.editReply({ content: "✅ Forwarded to your DMs!" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let replyText = "❌ Failed to forward message.";
|
let replyText = "❌ Failed to forward message.";
|
||||||
if (error instanceof DiscordAPIError && error.code === 50_007) {
|
if (error instanceof DiscordAPIError && error.code === 50_007) {
|
||||||
replyText = `${replyText} (Naomi's DMs might be closed)`;
|
replyText = `${replyText} (Naomi's DMs might be closed)`;
|
||||||
}
|
}
|
||||||
|
if (error instanceof Error) {
|
||||||
|
await logger.error("forwardToOwner command", error);
|
||||||
|
}
|
||||||
await interaction.editReply(replyText);
|
await interaction.editReply(replyText);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const forwardOwnerDM = {
|
export { forwardToOwner };
|
||||||
data,
|
|
||||||
execute,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ export const onboardMentee = async(
|
|||||||
content: `✅ Successfully onboarded **${menteeName}**!\nRepository: ${repoUrl}`,
|
content: `✅ Successfully onboarded **${menteeName}**!\nRepository: ${repoUrl}`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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({
|
await interaction.editReply({
|
||||||
content: `❌ Failed to onboard mentee: ${String(error)}`,
|
content: `❌ Failed to onboard mentee: ${String(error)}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 ],
|
||||||
|
});
|
||||||
|
};
|
||||||
+2
-46
@@ -10,15 +10,11 @@ import {
|
|||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
Events,
|
Events,
|
||||||
Partials,
|
Partials,
|
||||||
MessageFlags,
|
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { scheduleJob } from "node-schedule";
|
import { scheduleJob } from "node-schedule";
|
||||||
import { App } from "octokit";
|
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 { ids } from "./config/ids.js";
|
||||||
|
import { handleInteractionCreate } from "./events/handleInteractionCreate.js";
|
||||||
import { handleMessageCreate } from "./events/handleMessageCreate.js";
|
import { handleMessageCreate } from "./events/handleMessageCreate.js";
|
||||||
import { cacheData } from "./modules/cacheData.js";
|
import { cacheData } from "./modules/cacheData.js";
|
||||||
import { checkRetroAchievements } from "./modules/checkAchievements.js";
|
import { checkRetroAchievements } from "./modules/checkAchievements.js";
|
||||||
@@ -117,47 +113,7 @@ amari.discord.on(Events.MessageCreate, (message) => {
|
|||||||
|
|
||||||
amari.discord.on(Events.InteractionCreate, (interaction) => {
|
amari.discord.on(Events.InteractionCreate, (interaction) => {
|
||||||
void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction });
|
void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction });
|
||||||
|
handleInteractionCreate(amari, 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 ],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
amari.discord.on(Events.ThreadCreate, (thread) => {
|
amari.discord.on(Events.ThreadCreate, (thread) => {
|
||||||
|
|||||||
@@ -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<void> => {
|
|
||||||
try {
|
|
||||||
await rest.put(Routes.applicationCommands(clientId), { body: commands });
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
await logger.error("operation", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void requestCommand();
|
|
||||||
Reference in New Issue
Block a user