diff --git a/src/commands/clear.ts b/src/commands/clear.ts new file mode 100644 index 0000000..e013359 --- /dev/null +++ b/src/commands/clear.ts @@ -0,0 +1,24 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { + ApplicationIntegrationType, + SlashCommandBuilder, + InteractionContextType, +} from "discord.js"; + +const command = new SlashCommandBuilder(). + setContexts( + InteractionContextType.BotDM, + InteractionContextType.Guild, + InteractionContextType.PrivateChannel, + ). + setIntegrationTypes(ApplicationIntegrationType.UserInstall). + setName("clear"). + setDescription("Clear your current conversation so you can start a new one!"); + +// eslint-disable-next-line no-console -- We don't need our logger here as this never runs in production. +console.log(JSON.stringify(command.toJSON())); diff --git a/src/events/message.ts b/src/events/message.ts index 0caeb2c..303640e 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -14,13 +14,14 @@ import { personality } from "../config/personality.js"; import { ai } from "../utils/ai.js"; import { calculateCost } from "../utils/calculateCost.js"; import { isSubscribedMessage } from "../utils/isSubscribed.js"; +import { logger } from "../utils/logger.js"; import type { MessageParam } from "@anthropic-ai/sdk/resources/index.js"; /** * Handles the Discord message event. * @param message - The message payload from Discord. */ -// eslint-disable-next-line max-lines-per-function -- We're off by one bloody line. +// eslint-disable-next-line max-lines-per-function, max-statements -- We're off by one bloody line. export const onMessage = async(message: Message): Promise => { try { if (message.channel.type !== ChannelType.DM) { @@ -33,17 +34,29 @@ export const onMessage = async(message: Message): Promise => { if (!subbed) { return; } - const history = await message.channel.messages.fetch({ limit: 6 }); - const context: Array - = history.reverse().map((messageInner) => { - return { - content: messageInner.content, - role: - messageInner.author.id === message.client.user.id - ? "assistant" - : "user", - }; + const historyRequest = await message.channel.messages.fetch({ limit: 20 }); + const history = [ ...historyRequest.values() ]; + const clearMessageIndex = history.findIndex((messageInner) => { + return ( + messageInner.content === "" + && messageInner.author.id === message.client.user.id + ); }); + if (clearMessageIndex !== -1) { + // Remove the clear message and everything sent before it, which means everything after in the array because the array is backwards + history.splice(clearMessageIndex, history.length - clearMessageIndex); + } + const context: Array = history. + reverse(). + map((messageInner) => { + return { + content: messageInner.content, + role: + messageInner.author.id === message.client.user.id + ? "assistant" + : "user", + }; + }); const messages = await ai.messages.create({ // eslint-disable-next-line @typescript-eslint/naming-convention -- Required key format for SDK. max_tokens: 3000, @@ -61,8 +74,13 @@ export const onMessage = async(message: Message): Promise => { response?.text ?? "There was an error. Please try again later.", ); + if (!response) { + await logger.log("info", `No response from AI, here's the payload: ${JSON.stringify(messages)}`); + } + await calculateCost(messages.usage, message.author.username); } catch (error) { + await logger.error("message event", error as Error); const button = new ButtonBuilder(). setLabel("Need help?"). setStyle(ButtonStyle.Link). diff --git a/src/index.ts b/src/index.ts index d9df46e..ead2758 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { Client, Events, GatewayIntentBits, Partials } from "discord.js"; import { onMessage } from "./events/message.js"; import { about } from "./modules/about.js"; +import { clear } from "./modules/clear.js"; import { instantiateServer } from "./server/serve.js"; import { logger } from "./utils/logger.js"; @@ -45,6 +46,9 @@ client.on(Events.InteractionCreate, (interaction) => { ephemeral: true, }); break; + case "clear": + void clear(interaction); + break; default: void interaction.reply({ content: `I'm sorry, I don't know the ${interaction.commandName} command.`, diff --git a/src/modules/clear.ts b/src/modules/clear.ts new file mode 100644 index 0000000..8e7eb8b --- /dev/null +++ b/src/modules/clear.ts @@ -0,0 +1,47 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { + MessageFlags, + type ChatInputCommandInteraction, +} from "discord.js"; +import { isSubscribedInteraction } from "../utils/isSubscribed.js"; +import { logger } from "../utils/logger.js"; +import { replyToError } from "../utils/replyToError.js"; + +/** + * Sends a clear message in the DMs. + * @param interaction -- The interaction payload from Discord. + */ +export const clear = async( + interaction: ChatInputCommandInteraction, +): Promise => { + try { + await interaction.deferReply({ flags: [ MessageFlags.Ephemeral ] }); + + const subbed = await isSubscribedInteraction(interaction); + if (!subbed) { + return; + } + const sent = await interaction.user.send({ + content: "", + }).catch(() => { + return null; + }); + + await interaction.editReply({ + content: sent + ? "I have added a clear history marker to your DMs." + // eslint-disable-next-line stylistic/max-len -- This is a long string. + : "I was unable to send you a DM. Please ensure your privacy settings allow direct messages.", + }); + } catch (error) { + await replyToError(interaction); + if (error instanceof Error) { + await logger.error("about command", error); + } + } +};