From 5c01bacf8591f89070bc076e2fe61f7c37f75874 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Sun, 24 Nov 2024 13:38:32 -0500 Subject: [PATCH 1/8] added two commands to test actionrows and implemented an embed for clarify --- src/discordApp.ts | 6 +- src/firebase.ts | 4 +- src/interactions.ts | 202 ++++++++++++++++++++++++++++++++++++++-- src/registerCommands.ts | 12 ++- src/tones.json | 123 ++++++++++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 336 insertions(+), 13 deletions(-) create mode 100644 src/tones.json diff --git a/src/discordApp.ts b/src/discordApp.ts index a44ab4a..57e6940 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import analyzeTone from "./gptRequests"; import { Client, GatewayIntentBits, Events, ClientUser } from "discord.js"; -import { clarify, embed, ping, tone, mood } from "./interactions" +import { action, clarify, embed, ping, tone, getTones /*mood*/ } from "./interactions" // define a bunch of emojis we'll use frequently here. either unicode character or just the id const reactions = { @@ -80,7 +80,9 @@ async function launchBot(): Promise { if (interaction.isChatInputCommand()) { if (interaction.commandName === "ping") await ping(interaction); if (interaction.commandName === "embed") await embed(interaction); - if (interaction.commandName === "mood") await mood(interaction); + if (interaction.commandName === "action") await action(interaction); + if (interaction.commandName === "list-tones") await getTones(interaction); + /*if (interaction.commandName === "mood") await mood(interaction);*/ } else if (interaction.isMessageContextMenuCommand()) { if (interaction.commandName === "Tone") await tone(interaction); if (interaction.commandName === "Clarify") await clarify(interaction); diff --git a/src/firebase.ts b/src/firebase.ts index d8f4bef..ef0a0aa 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -1,4 +1,4 @@ -import * as dotenv from 'dotenv'; +/*import * as dotenv from 'dotenv'; dotenv.config(); // Load environment variables from .env @@ -22,4 +22,4 @@ const app = initializeApp(firebaseConfig); // Initialize Realtime Database and get a reference to the service const database = getDatabase(app); -export default database; // Export the Firestore instance \ No newline at end of file +export default database; // Export the Firestore instance*/ \ No newline at end of file diff --git a/src/interactions.ts b/src/interactions.ts index 5ee221c..7e9f28a 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -1,10 +1,22 @@ -import { CacheType, ChatInputCommandInteraction, EmbedBuilder, - MessageContextMenuCommandInteraction, SlashCommandBuilder } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, + ChatInputCommandInteraction, ComponentType, EmbedBuilder, MessageContextMenuCommandInteraction, + SlashCommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import analyzeTone from "./gptRequests"; -import db from './firebase'; // Import from your firebase.ts file +//import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; +import toneJSON from "./tones.json" assert { type: "json"}; -export async function mood(interaction: ChatInputCommandInteraction): Promise { +interface Tone { + name: string; + description: string; + indicator: string; +} + +interface ToneList { + tones: Tone[]; +} + +/*export async function mood(interaction: ChatInputCommandInteraction): Promise { var currentMood = interaction.options.get('currentmood')?.value!.toString(); var oldMood = ""; @@ -58,7 +70,7 @@ export async function mood(interaction: ChatInputCommandInteraction): ephemeral: true, content: "Thanks for updating your mood!" }) -} +}*/ // Example: Add a document to a collection @@ -91,6 +103,147 @@ export async function embed(interaction: ChatInputCommandInteraction) interaction.editReply({ embeds: [embed1, embed2] }); } +export async function action(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply(); + + //Builds the menu + const select = new StringSelectMenuBuilder() + .setCustomId('example') + .setPlaceholder('select an option') + .addOptions( + //Build the option: + //Has a label (setLabel), description (setDescription), value (setValue), emoji (setEmoji), and can be set to selected by default (setDefault) + new StringSelectMenuOptionBuilder() + .setLabel('Option 1 Label') + .setDescription('Option 1 Description') + .setValue('Option 1 Value'), + new StringSelectMenuOptionBuilder() + .setLabel('Option 2 Label') + .setDescription('Option 2 Description') + .setValue('Option 2 Value'), + new StringSelectMenuOptionBuilder() + .setLabel('Option 3 Label') + .setDescription('Option 3 Description') + .setValue('Option 3 Value'), + ); + + //Buttons have five different styles, primary, secondary, success, danger, and link + //Each button has a customId, label, style, link, and emoji (SKUid doesn't really apply to our project) + //A button can also be set to disabled by default + const primary = new ButtonBuilder() + .setCustomId('primary') + .setLabel('Primary Button') + .setStyle(ButtonStyle.Primary); + + const secondary = new ButtonBuilder() + .setCustomId('secondary') + .setLabel('Secondary Button') + .setStyle(ButtonStyle.Secondary); + + const success = new ButtonBuilder() + .setCustomId('success') + .setLabel('Success Button') + .setStyle(ButtonStyle.Success); + + const danger = new ButtonBuilder() + .setCustomId('danger') + .setLabel('Danger Button') + .setStyle(ButtonStyle.Danger); + + //Important! URL and CustomID are mutually exclusive + const link = new ButtonBuilder() + .setLabel('Link Button') + .setURL('https://discordjs.guide/message-components/buttons.html#button-styles') + .setStyle(ButtonStyle.Link); + + //row1 and row2 are the rows of a message, there can be up to five rows, each with five maximum elements + //Select menus have a width value of 5, while buttons have a width of 1 + const row1 = new ActionRowBuilder() + .addComponents(select); + + const row2 = new ActionRowBuilder() + .addComponents(primary, secondary, success, danger, link); + + const response = await interaction.editReply({ + content: 'An example select menu', + components: [row1, row2], + }); + + //This filter ensures that only the user who issued the command can press the buttons + const collectorFilter = (i: { user: { id: string; }; }) => i.user.id === interaction.user.id; + + //this approach works to collect only one interaction + /*try { + const confirmation = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000}); + + switch (confirmation.customId) { + case 'primary': + await confirmation.update({ content: `${interaction.user.displayName} has pressed the primary button`, components: [] }); + break; + case 'secondary': + await confirmation.update({ content: `${interaction.user.displayName} has pressed the secondary button`, components: [] }); + break; + case 'success': + await confirmation.update({ content: `${interaction.user.displayName} has pressed the success button`, components: [] }); + break; + case 'danger': + await confirmation.update({ content: `${interaction.user.displayName} has pressed the danger button`, components: [] }); + break; + } + } catch (e) { + await interaction.editReply({ content: 'No interaction for the last minute, cancelling interaction', components: [] }); + }*/ + + const collector = response.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.Button, time: 180_000}); + + //For each interaction with the components of componentType, do the following + collector.on('collect', async i => { + const selection = i.customId; + await i.reply(`${i.user} has selected ${selection}!\n`) + }); +} + +export async function getTones(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply(); + + const toneMenu = new StringSelectMenuBuilder() + .setCustomId('tone select menu') + .setPlaceholder('Select a tone') + .setMinValues(1) + .setMaxValues(5); + + toneJSON.tones.forEach((tone: Tone) => { + toneMenu.addOptions( + new StringSelectMenuOptionBuilder() + .setValue(` ${tone.name} (${tone.indicator})`) + .setLabel(`${tone.name}: ${tone.indicator}`) + .setDescription(`${tone.description}`), + ); + }); + + const row = new ActionRowBuilder() + .addComponents(toneMenu); + + const response = await interaction.editReply({ + content: 'Here is a list of tones: ', + components: [row], + }); + + const collectorFilter = (i: {user: {id: string}; }) => i.user.id === interaction.user.id; + + const collector = response.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.StringSelect, time: 180_000}); + + let selection: string[] = []; + + collector.on('collect', async i => { + selection = i.values; + await interaction.editReply({ + content: `${interaction.user.displayName}, you have selected the following tones: ${selection}`, + components: [], + }); + }); +} + export async function tone(interaction: MessageContextMenuCommandInteraction): Promise { await interaction.deferReply(); @@ -107,13 +260,48 @@ export async function clarify(interaction: MessageContextMenuCommandInteraction< ephemeral: true, content: "Thanks for pointing that out, I'll ask for you!" }) + + const toneMenu = new StringSelectMenuBuilder() + .setCustomId('tone select menu') + .setPlaceholder('Select a tone') + .setMinValues(1) + .setMaxValues(5); + + toneJSON.tones.forEach((tone: Tone) => { + toneMenu.addOptions( + new StringSelectMenuOptionBuilder() + .setValue(` ${tone.name} (${tone.indicator})`) + .setLabel(`${tone.name}: ${tone.indicator}`) + .setDescription(`${tone.description}`), + ); + }); + + const row = new ActionRowBuilder() + .addComponents(toneMenu); + if (interaction.channel?.isSendable()) { - interaction.channel.send(`Hey there, ${interaction.targetMessage.author}! It seems I wasn't able to understand the tone in one of your messages: + const request = await interaction.channel.send({ + content: `Hey there, ${interaction.targetMessage.author}! It seems I wasn't able to understand the tone in one of your messages: > ${interaction.targetMessage.content.split('\n').join("\n> ")} To help me learn, I was hoping you could clarify the tone of your message. -Here's a short list of tones: \`\` (***TODO***)`); +Here's a short list of tones, select up to five that apply:`, + components: [row]}); + + const collectorFilter = (i: {user: {id: string}; }) => i.user.id === interaction.targetMessage.author.id; + + const collector = request.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.StringSelect, time: 180_000}); + + let selection: string[] = []; + + collector.on('collect', async i => { + selection = i.values; + await interaction.targetMessage.reply({ + content: `This message was marked with the following tones: ${selection}` + }); + await request.delete(); + }); } } \ No newline at end of file diff --git a/src/registerCommands.ts b/src/registerCommands.ts index 898102f..7a0a115 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -48,6 +48,16 @@ updateCommands([ type: 1, }, { + name: "action", + description: "tests the action row feature of discord", + type: 1, + }, + { + name: "list-tones", + description: "lists a sample of tones and their description", + type: 1, + }, + /*{ name: "mood", description: "Sets the user's current mood", options: [ @@ -59,7 +69,7 @@ updateCommands([ } ], type: 1 - }, + },*/ { name: "embed", description: "test embed feature of discord", diff --git a/src/tones.json b/src/tones.json new file mode 100644 index 0000000..9475764 --- /dev/null +++ b/src/tones.json @@ -0,0 +1,123 @@ +{ + "tones": [{ + "name": "Joking", + "description": "A tone used to indicate a message was a joke.", + "indicator": "\\j" + }, + { + "name": "Half-Joking", + "description": "Used to indicate a message was said in a joke-like manner, but is semi-serious.", + "indicator": "\\hj" + }, + { + "name": "Sarcastic", + "description": "A tone used to indicate a message was sarcastic or includes sarcasm.", + "indicator": "\\s" + }, + { + "name": "Genuine", + "description": "A tone used to indicate a message was made with genuine intentions.", + "indicator": "\\g" + }, + { + "name": "Serious", + "description": "A tone used to indicate a message is serious and is intended to be taken seriously.", + "indicator": "\\srs" + }, + { + "name": "Non-Serious", + "description": "A tone used to indicate a message is not serious and shouldn't be taken seriously.", + "indicator": "\\nsrs" + }, + { + "name": "Positive Connotation", + "description": "A tone used to indicate a message has a positive connotation.", + "indicator": "\\pos" + }, + { + "name": "Neutral Connotation", + "description": "A tone indicating a message has a neutral connotation.", + "indicator": "\\neu" + }, + { + "name": "Negative Connotation", + "description": "A tone used to indicate a message has a negative connotation.", + "indicator": "\\neg" + }, + { + "name": "Platonic", + "description": "A tone used when a message is meant to be taken platonically", + "indicator": "\\p" + }, + { + "name": "Romantic", + "description": "A tone used when a message is meant to be taken romantically.", + "indicator": "\\r" + }, + { + "name": "Light-Hearted", + "description": "A tone used to indicate a message is light-hearted, carefree, or cheerful.", + "indicator": "\\lh" + }, + { + "name": "Little Upset", + "description": "A tone used when the sender wants to indicate they're a little upset.", + "indicator": "\\lu" + }, + { + "name": "Nobody Here", + "description": "A tone used when someone wants to indicate their post isn't directed at any recipient.", + "indicator": "\\nbh" + }, + { + "name": "Rhetorical", + "description": "A tone used when a question is rhetorical and not meant to be answered.", + "indicator": "\\rh" + }, + { + "name": "Teasing", + "description": "A tone used when a user intends to tease.", + "indicator": "\\t" + }, + { + "name": "Inside Joke", + "description": "A tone used to mark any inside joke that other recipients might not get.", + "indicator": "\\ij" + }, + { + "name": "Metaphorical", + "description": "A tone used to mark a message as metaphorical.", + "indicator": "\\m" + }, + { + "name": "Literal", + "description": "A tone used to indicate a message is literal.", + "indicator": "\\li" + }, + { + "name": "Hyperbole", + "description": "A tone used to indicate a message is hyperbole (an exaggeration)", + "indicator": "\\hyb" + }, + { + "name": "Fake", + "description": "A tone used to mark something as fake or a lie.", + "indicator": "\\f" + }, + { + "name": "Not Mad", + "description": "A tone used when a sender wants to indicate they're not mad.", + "indicator": "\\nm" + }, + { + "name": "Copypasta", + "description": "A tone used to indicate a message is a copypasta (usually humorous copy-pasted texts).", + "indicator": "\\c" + }, + { + "name": "Lyrics", + "description": "A tone used to indicate a message contains lyrics to a song.", + "indicator": "\\l" + }] + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 242963d..ec5aedc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "module": "es2022", + "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, From 330b2495968caa3f35016939af49d1fa7bdf99d6 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Sun, 24 Nov 2024 14:13:17 -0500 Subject: [PATCH 2/8] added comments, minor cleanup --- src/interactions.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/interactions.ts b/src/interactions.ts index 7e9f28a..7695c7e 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -4,6 +4,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, import analyzeTone from "./gptRequests"; //import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; +//getTones and Clarify rely on toneJSON. Implementing it in firebase would be better import toneJSON from "./tones.json" assert { type: "json"}; interface Tone { @@ -12,10 +13,6 @@ interface Tone { indicator: string; } -interface ToneList { - tones: Tone[]; -} - /*export async function mood(interaction: ChatInputCommandInteraction): Promise { var currentMood = interaction.options.get('currentmood')?.value!.toString(); var oldMood = ""; @@ -203,19 +200,22 @@ export async function action(interaction: ChatInputCommandInteraction }); } +//A function which creates a select menu from the elements in tones.json for users to select export async function getTones(interaction: ChatInputCommandInteraction): Promise { await interaction.deferReply(); + //create the tone menu which allows a user to select 1-5 tones const toneMenu = new StringSelectMenuBuilder() .setCustomId('tone select menu') .setPlaceholder('Select a tone') .setMinValues(1) .setMaxValues(5); + //For each tone in toneJson.tones, we create a new option for our tone menu toneJSON.tones.forEach((tone: Tone) => { toneMenu.addOptions( new StringSelectMenuOptionBuilder() - .setValue(` ${tone.name} (${tone.indicator})`) + .setValue(`${tone.name} (${tone.indicator})`) .setLabel(`${tone.name}: ${tone.indicator}`) .setDescription(`${tone.description}`), ); @@ -224,19 +224,22 @@ export async function getTones(interaction: ChatInputCommandInteraction() .addComponents(toneMenu); + //await the promise and edit the reply with the following + //row is the actionRow which hold the select menu const response = await interaction.editReply({ content: 'Here is a list of tones: ', components: [row], }); + //creates a filter that only allows the user who sent the interaction to edit it const collectorFilter = (i: {user: {id: string}; }) => i.user.id === interaction.user.id; + //creates a collector with the filter above that times out in three minutes const collector = response.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.StringSelect, time: 180_000}); - let selection: string[] = []; - + //edit the reply to say what the user selected collector.on('collect', async i => { - selection = i.values; + const selection = i.values; await interaction.editReply({ content: `${interaction.user.displayName}, you have selected the following tones: ${selection}`, components: [], @@ -261,6 +264,7 @@ export async function clarify(interaction: MessageContextMenuCommandInteraction< content: "Thanks for pointing that out, I'll ask for you!" }) + //create the tone menu and add the options from toneJSON const toneMenu = new StringSelectMenuBuilder() .setCustomId('tone select menu') .setPlaceholder('Select a tone') @@ -290,14 +294,14 @@ To help me learn, I was hoping you could clarify the tone of your message. Here's a short list of tones, select up to five that apply:`, components: [row]}); + //create the filter and the collector const collectorFilter = (i: {user: {id: string}; }) => i.user.id === interaction.targetMessage.author.id; const collector = request.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.StringSelect, time: 180_000}); - let selection: string[] = []; - + //Get the response from the, reply to the target message the tones, and delete the request collector.on('collect', async i => { - selection = i.values; + const selection = i.values; await interaction.targetMessage.reply({ content: `This message was marked with the following tones: ${selection}` }); From 2451d8151bdc412c8cab38ffa3e55115461d4430 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Sun, 24 Nov 2024 14:23:01 -0500 Subject: [PATCH 3/8] removed firebase-related comments --- src/discordApp.ts | 4 ++-- src/firebase.ts | 4 ++-- src/interactions.ts | 6 +++--- src/registerCommands.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/discordApp.ts b/src/discordApp.ts index 57e6940..df36323 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import analyzeTone from "./gptRequests"; import { Client, GatewayIntentBits, Events, ClientUser } from "discord.js"; -import { action, clarify, embed, ping, tone, getTones /*mood*/ } from "./interactions" +import { action, clarify, embed, ping, tone, getTones, mood } from "./interactions" // define a bunch of emojis we'll use frequently here. either unicode character or just the id const reactions = { @@ -82,7 +82,7 @@ async function launchBot(): Promise { if (interaction.commandName === "embed") await embed(interaction); if (interaction.commandName === "action") await action(interaction); if (interaction.commandName === "list-tones") await getTones(interaction); - /*if (interaction.commandName === "mood") await mood(interaction);*/ + if (interaction.commandName === "mood") await mood(interaction); } else if (interaction.isMessageContextMenuCommand()) { if (interaction.commandName === "Tone") await tone(interaction); if (interaction.commandName === "Clarify") await clarify(interaction); diff --git a/src/firebase.ts b/src/firebase.ts index ef0a0aa..d8f4bef 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -1,4 +1,4 @@ -/*import * as dotenv from 'dotenv'; +import * as dotenv from 'dotenv'; dotenv.config(); // Load environment variables from .env @@ -22,4 +22,4 @@ const app = initializeApp(firebaseConfig); // Initialize Realtime Database and get a reference to the service const database = getDatabase(app); -export default database; // Export the Firestore instance*/ \ No newline at end of file +export default database; // Export the Firestore instance \ No newline at end of file diff --git a/src/interactions.ts b/src/interactions.ts index 7695c7e..93e4ad8 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -2,7 +2,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, ChatInputCommandInteraction, ComponentType, EmbedBuilder, MessageContextMenuCommandInteraction, SlashCommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import analyzeTone from "./gptRequests"; -//import db from './firebase'; // Import from your firebase.ts file +import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; //getTones and Clarify rely on toneJSON. Implementing it in firebase would be better import toneJSON from "./tones.json" assert { type: "json"}; @@ -13,7 +13,7 @@ interface Tone { indicator: string; } -/*export async function mood(interaction: ChatInputCommandInteraction): Promise { +export async function mood(interaction: ChatInputCommandInteraction): Promise { var currentMood = interaction.options.get('currentmood')?.value!.toString(); var oldMood = ""; @@ -67,7 +67,7 @@ interface Tone { ephemeral: true, content: "Thanks for updating your mood!" }) -}*/ +} // Example: Add a document to a collection diff --git a/src/registerCommands.ts b/src/registerCommands.ts index 7a0a115..0d8fc8e 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -57,7 +57,7 @@ updateCommands([ description: "lists a sample of tones and their description", type: 1, }, - /*{ + { name: "mood", description: "Sets the user's current mood", options: [ @@ -69,7 +69,7 @@ updateCommands([ } ], type: 1 - },*/ + }, { name: "embed", description: "test embed feature of discord", From 09831655a39cf2f4e52fb0cea4cf3de47c8680ab Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Mon, 25 Nov 2024 16:27:14 -0500 Subject: [PATCH 4/8] Added a proto add-tone function using reactions --- src/discordApp.ts | 5 +++-- src/gptRequests.ts | 47 +++++++++++++++++++++++++++++++++++++++-- src/registerCommands.ts | 4 ++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/discordApp.ts b/src/discordApp.ts index df36323..9e8dd02 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,7 +1,7 @@ import "dotenv/config"; -import analyzeTone from "./gptRequests"; +import { analyzeTone } from "./gptRequests"; import { Client, GatewayIntentBits, Events, ClientUser } from "discord.js"; -import { action, clarify, embed, ping, tone, getTones, mood } from "./interactions" +import { action, clarify, embed, ping, tone, getTones, mood, postemptiveToneAdd } from "./interactions" // define a bunch of emojis we'll use frequently here. either unicode character or just the id const reactions = { @@ -85,6 +85,7 @@ async function launchBot(): Promise { if (interaction.commandName === "mood") await mood(interaction); } else if (interaction.isMessageContextMenuCommand()) { if (interaction.commandName === "Tone") await tone(interaction); + if (interaction.commandName === "Add Tone") await postemptiveToneAdd(interaction); if (interaction.commandName === "Clarify") await clarify(interaction); } else { console.log(interaction); diff --git a/src/gptRequests.ts b/src/gptRequests.ts index f7b8245..3534af8 100644 --- a/src/gptRequests.ts +++ b/src/gptRequests.ts @@ -3,7 +3,7 @@ import { OpenAI } from "openai"; const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY}); -async function analyzeTone(userText: string): Promise { +export async function analyzeTone(userText: string): Promise { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ @@ -50,4 +50,47 @@ async function analyzeTone(userText: string): Promise { } } -export default analyzeTone \ No newline at end of file +export async function emojiRepresentation(userText: string): Promise { + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": ` + Humans are unpredictable beings. A text message by someone can be interpreted as passive aggressive + or cheerful depending on how the person reads it in their minds. This can lead to misunderstandings. + However, assume you are an expert in understanding human emotions when they send text messages. + I want you to reply to the following texts by giving your best guess about what the person might be + feeling when they wrote it. You are to figure out whether they are sad, mad, happy, neutral, or any + other emotion that the person is conveying. Assume the texter is familiar with the modern day texting conventions. + Sometimes, the text will contain "@vibecheque". You are to discard that, and only analyze + the rest of the text. + + I want you to tune into the tone behind any messages you receive and reply only with the emoji which best fits. + If you are unable to determine an emoji for the message, respond only with this emoji: 🛒 + ` + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": userText + } + ] + } + ] + }); + + if (response.choices[0].message.content !== null){ + return response.choices[0].message.content; + } + else{ + return "Unknown error - can't generate the tone at the moment" + } +} \ No newline at end of file diff --git a/src/registerCommands.ts b/src/registerCommands.ts index 0d8fc8e..be684a2 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -79,6 +79,10 @@ updateCommands([ name: "Tone", type: 3, }, + { + name: "Add Tone", + type: 3, + }, { name: "Clarify", type: 3, From ebaed072ffa50fc4d8f117ff45ce8a7346ceeb89 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Sat, 7 Dec 2024 23:34:49 -0500 Subject: [PATCH 5/8] added in-depth analysis --- src/discordApp.ts | 3 +- src/gptRequests.ts | 54 +++++++++++++++++++++++++++++++++ src/interactions.ts | 66 ++++++++++++++++++++++++++++++++++++++++- src/registerCommands.ts | 4 +++ 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/discordApp.ts b/src/discordApp.ts index 9e8dd02..34fc481 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import { analyzeTone } from "./gptRequests"; import { Client, GatewayIntentBits, Events, ClientUser } from "discord.js"; -import { action, clarify, embed, ping, tone, getTones, mood, postemptiveToneAdd } from "./interactions" +import { action, clarify, embed, ping, tone, getTones, mood, postemptiveToneAdd, inDepthClarification } from "./interactions" // define a bunch of emojis we'll use frequently here. either unicode character or just the id const reactions = { @@ -87,6 +87,7 @@ async function launchBot(): Promise { if (interaction.commandName === "Tone") await tone(interaction); if (interaction.commandName === "Add Tone") await postemptiveToneAdd(interaction); if (interaction.commandName === "Clarify") await clarify(interaction); + if (interaction.commandName === "In-Depth Clarification") await inDepthClarification(interaction); } else { console.log(interaction); } diff --git a/src/gptRequests.ts b/src/gptRequests.ts index 3534af8..8170845 100644 --- a/src/gptRequests.ts +++ b/src/gptRequests.ts @@ -50,6 +50,60 @@ export async function analyzeTone(userText: string): Promise { } } +export async function explanationOfTone(userText: string): Promise { + const response = await openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + "role": "system", + "content": [ + { + "type": "text", + "text": ` + Humans are unpredictable beings. A text message by someone can be interpreted as passive aggressive + or cheerful depending on how the person reads it in their minds. This can lead to misunderstandings. + However, assume you are an expert in understanding human emotions when they send text messages. + I want you to reply to the following texts by giving your best guess about what the person might be + feeling when they wrote it. You are to figure out whether they are sad, mad, happy, neutral, or any + other emotion that the person is conveying. Assume the texter is familiar with the modern day texting conventions. + Sometimes, the text will contain "@vibecheque". You are to discard that, and only analyze + the rest of the text. + + I want you to analyze the text with the intention of clarifying the message to solve the aformentioned issues. + For any tone in the message, I want you to format your explanation as following: + + > "text" + + If the message has fragments with different tone, I want you to use the following format instead: + + > "text fragment" + + After the tone, I want you to explain how the tone applies to respective fragment in a single sentence. + After the analysis, I want you to include some variation of "Hope that clears things up!" + ` + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": userText + } + ] + } + ] + }); + + if (response.choices[0].message.content !== null){ + return response.choices[0].message.content; + } + else{ + return "Unknown error - can't generate the tone at the moment" + } +} + export async function emojiRepresentation(userText: string): Promise { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", diff --git a/src/interactions.ts b/src/interactions.ts index 93e4ad8..b3e45f8 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, ChatInputCommandInteraction, ComponentType, EmbedBuilder, MessageContextMenuCommandInteraction, SlashCommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; -import analyzeTone from "./gptRequests"; +import { analyzeTone, emojiRepresentation, explanationOfTone } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; //getTones and Clarify rely on toneJSON. Implementing it in firebase would be better @@ -200,7 +200,58 @@ export async function action(interaction: ChatInputCommandInteraction }); } +//This should be an application command (need to select message to add tone to) +//This kinda adds tone, but it's super lame as it's only a reaction +export async function postemptiveToneAdd(interaction: MessageContextMenuCommandInteraction): Promise { + await interaction.deferReply(); + + //copy-paste select menu for tones: + //create the tone menu which allows a user to select 1-5 tones + const toneMenu = new StringSelectMenuBuilder() + .setCustomId('tone select menu') + .setPlaceholder('Select a tone') + .setMinValues(1) + .setMaxValues(5); + + //For each tone in toneJson.tones, we create a new option for our tone menu + toneJSON.tones.forEach((tone: Tone) => { + toneMenu.addOptions( + new StringSelectMenuOptionBuilder() + .setValue(`${tone.name} (${tone.indicator})`) + .setLabel(`${tone.name}: ${tone.indicator}`) + .setDescription(`${tone.description}`), + ); + }); + + const row = new ActionRowBuilder() + .addComponents(toneMenu); + + //await the promise and edit the reply with the following + //row is the actionRow which hold the select menu + const response = await interaction.editReply({ + content: 'Here is a list of tones: ', + components: [row], + }); + + //creates a filter that only allows the user who sent the interaction to edit it + //Need to make sure the author is the only one who can add tones + const collectorFilter = (i: {user: {id: string}; }) => i.user.id === interaction.targetMessage.author.id; + + //creates a collector with the filter above that times out in three minutes + const collector = response.createMessageComponentCollector({ filter: collectorFilter, componentType: ComponentType.StringSelect, time: 180_000}); + + //edit the reply to say what the user selected + collector.on('collect', async i => { + const selection = i.values; + //rather than form a reply, the bot should try to edit the user's message + //TODO: make selection a string instead of a string[] using map/filter + selection.forEach(async (currentValue: string) => {interaction.targetMessage.react(await emojiRepresentation(currentValue));}); + await response.delete(); + }); +} + //A function which creates a select menu from the elements in tones.json for users to select +//This already covers the preexisting tone add export async function getTones(interaction: ChatInputCommandInteraction): Promise { await interaction.deferReply(); @@ -258,6 +309,19 @@ export async function tone(interaction: MessageContextMenuCommandInteraction): Promise { + //is this seriously all? + interaction.reply({ + ephemeral: true, + content: await explanationOfTone(interaction.targetMessage.content) + }); +} + export async function clarify(interaction: MessageContextMenuCommandInteraction): Promise { interaction.reply({ ephemeral: true, diff --git a/src/registerCommands.ts b/src/registerCommands.ts index be684a2..c67eb98 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -87,4 +87,8 @@ updateCommands([ name: "Clarify", type: 3, }, + { + name: "In-Depth Clarification", + type: 3, + } ]); \ No newline at end of file From 85a5d7ea95b56bd60e17e13baef7161398d9bc45 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Mon, 9 Dec 2024 12:38:28 -0500 Subject: [PATCH 6/8] Updated interaction.ts and tsconfig.json to not rely on esnext --- src/interactions.ts | 15 +++++++++------ tsconfig.json | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/interactions.ts b/src/interactions.ts index b3e45f8..42c2cf9 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -1,18 +1,21 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CacheType, ChatInputCommandInteraction, ComponentType, EmbedBuilder, MessageContextMenuCommandInteraction, SlashCommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js"; import { analyzeTone, emojiRepresentation, explanationOfTone } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; //getTones and Clarify rely on toneJSON. Implementing it in firebase would be better -import toneJSON from "./tones.json" assert { type: "json"}; +//import toneJSON from "./tones.json" assert { type: "json"}; -interface Tone { +export interface Tone { name: string; description: string; indicator: string; } +//Import tones from tones.json +const tones: Tone[] = require("./tones.json").tones; + export async function mood(interaction: ChatInputCommandInteraction): Promise { var currentMood = interaction.options.get('currentmood')?.value!.toString(); var oldMood = ""; @@ -214,7 +217,7 @@ export async function postemptiveToneAdd(interaction: MessageContextMenuCommandI .setMaxValues(5); //For each tone in toneJson.tones, we create a new option for our tone menu - toneJSON.tones.forEach((tone: Tone) => { + tones.forEach((tone: Tone) => { toneMenu.addOptions( new StringSelectMenuOptionBuilder() .setValue(`${tone.name} (${tone.indicator})`) @@ -263,7 +266,7 @@ export async function getTones(interaction: ChatInputCommandInteraction { + tones.forEach((tone: Tone) => { toneMenu.addOptions( new StringSelectMenuOptionBuilder() .setValue(`${tone.name} (${tone.indicator})`) @@ -335,7 +338,7 @@ export async function clarify(interaction: MessageContextMenuCommandInteraction< .setMinValues(1) .setMaxValues(5); - toneJSON.tones.forEach((tone: Tone) => { + tones.forEach((tone: Tone) => { toneMenu.addOptions( new StringSelectMenuOptionBuilder() .setValue(` ${tone.name} (${tone.indicator})`) diff --git a/tsconfig.json b/tsconfig.json index ec5aedc..242963d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, - "module": "esnext", + "module": "es2022", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, From 03b7eae7e19ac210aa2222cbd59c28fb1b31b995 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Mon, 9 Dec 2024 12:40:24 -0500 Subject: [PATCH 7/8] added tests for action and list-tones --- src/interactions.test.ts | 66 ++++++++++++++++++++++++++++++-- src/testing/mocks/mockDiscord.ts | 16 +++++++- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/interactions.test.ts b/src/interactions.test.ts index 4677ead..9c3c1ab 100644 --- a/src/interactions.test.ts +++ b/src/interactions.test.ts @@ -1,7 +1,9 @@ -import { ChatInputCommandInteraction, EmbedBuilder, Message, MessageComponentBuilder, MessageContextMenuCommandInteraction, MessagePayload } from "discord.js"; -import { embed, ping, tone } from "./interactions"; +import { ChatInputCommandInteraction, EmbedBuilder, Message, MessageComponentBuilder, + MessageContextMenuCommandInteraction, MessagePayload, ActionRowBuilder, + StringSelectMenuComponent, ActionRow, ButtonComponent } from "discord.js"; +import { embed, ping, tone, action, getTones, Tone } from "./interactions"; import MockDiscord from "./testing/mocks/mockDiscord"; -import analyzeTone from "./gptRequests"; +import { analyzeTone } from "./gptRequests"; jest.mock("./gptRequests") describe("Testing slash commands", ()=>{ @@ -28,6 +30,8 @@ describe("Testing slash commands", ()=>{ await embed(interaction); + //console.log("interaction: ", interaction); + const reply = discord.getInteractionReply() as any; expect(reply).toHaveProperty("embeds"); @@ -60,4 +64,60 @@ describe("Testing slash commands", ()=>{ expect(spyDeferReply).toHaveBeenCalled(); expect(spyEditReply).not.toHaveBeenCalledWith("Something went wrong."); }); + + //Best coverage I can do here, I have no clue how to implement the collector as it requires a lot of set up + test("`action` function replies with two action rows, one a menu, and the other five buttons.", async () => { + const discord = new MockDiscord({ command: "/action"}); + + const message = discord.createMockMessage({}); + + const interaction = discord.getInteraction() as ChatInputCommandInteraction; + + await action(interaction); + + let reply: any = discord.getInteractionReply() as any; + + expect(reply).toHaveProperty("components"); + expect(reply.components).toHaveProperty("length"); + expect(reply.components.length).toBe(2); + expect(reply.components[0] instanceof ActionRowBuilder).toBeTruthy(); + expect(reply.components[1] instanceof ActionRowBuilder).toBeTruthy(); + + const actionRow1 = reply.components[0]; + const actionRow2 = reply.components[1]; + + expect(actionRow1.components[0].options[0].data.label).toMatch('Option 1 Label'); + expect(actionRow1.components[0].options[1].data.label).toMatch('Option 2 Label'); + expect(actionRow1.components[0].options[2].data.label).toMatch('Option 3 Label'); + + expect(actionRow2.components[0].data.label).toMatch('Primary Button'); + expect(actionRow2.components[1].data.label).toMatch('Secondary Button'); + expect(actionRow2.components[2].data.label).toMatch('Success Button'); + expect(actionRow2.components[3].data.label).toMatch('Danger Button'); + expect(actionRow2.components[4].data.label).toMatch('Link Button'); + }); + + //Can only test that the action row builder contains the items we expect + test("`list-tones` returns a message containing a StringSelectMenu of tones", async () => { + const discord = new MockDiscord({ command: "/list-tones"}); + + const message = discord.createMockMessage({}); + + const interaction = discord.getInteraction() as ChatInputCommandInteraction; + + await getTones(interaction); + + let reply: any = discord.getInteractionReply() as any; + + expect(reply).toHaveProperty("components"); + expect(reply.components).toHaveProperty("length"); + expect(reply.components.length).toBe(1); + expect(reply.components[0] instanceof ActionRowBuilder).toBeTruthy(); + + const actionRow = reply.components[0].components[0].options; + + const tones: Tone[] = require("./tones.json").tones; + + tones.forEach((tone: Tone, value: number) => {expect(actionRow[value].data.label).toMatch(`${tone.name}: ${tone.indicator}`)}); + }); }); \ No newline at end of file diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 7f0f18f..e0765a4 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -7,13 +7,14 @@ import { Message, MessageContextMenuCommandInteraction, MessageCreateOptions, + ComponentType, + InteractionCollector, } from "discord.js"; type MockDiscordOptions = { command: string } - export default class MockDiscord { private client!: Client; private user!: User; @@ -58,7 +59,8 @@ export default class MockDiscord { id: BigInt(1), // reply: jest.fn((text: string) => this.interactionReply = text), deferReply: jest.fn(), - editReply: jest.fn((reply: string | MessagePayload | InteractionEditReplyOptions) => this.interactionReply = reply), + editReply: jest.fn((reply: string | MessagePayload | InteractionEditReplyOptions) => { this.interactionReply = reply; return this.createMockMessage(reply as MessageCreateOptions); }), + //reply: jest.fn((reply: string | MessagePayload | InteractionEditReplyOptions) => this.interactionReply = reply), // fetchReply: jest.fn(), isRepliable: jest.fn(() => true) } as unknown as CommandInteraction; @@ -77,7 +79,17 @@ export default class MockDiscord { client: this.client, author: this.user, content: "MESSAGE CONTENT", + createMessageComponentCollector: jest.fn((filter: Function, componentType: ComponentType, time: number | undefined) => {return this.createMockCollector(filter, componentType, time)}), ...options } as unknown as Message; } + + public createMockCollector(filter: Function, componentType: ComponentType, time: number | undefined): InteractionCollector { + return { + filter: filter, + componentType: componentType, + time: time, + on: jest.fn((event: "collect" | "dispose" | "ignore", listener: any) => {/* Do nothing */}), + } as unknown as InteractionCollector; + } } \ No newline at end of file From d699fd2a699bceba77539a435db10f64da29e3a4 Mon Sep 17 00:00:00 2001 From: Aiden McGlauflin Date: Mon, 9 Dec 2024 19:25:07 -0500 Subject: [PATCH 8/8] Fixed tests and await issues --- src/interactions.ts | 17 ++++++++++++----- src/registerCommands.ts | 16 ++++++++-------- src/testing/mocks/mockDiscord.ts | 4 ++-- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/interactions.ts b/src/interactions.ts index ac53ffb..97efa5a 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -8,9 +8,6 @@ import { updateOldRoleInServer, updateNewRoleInServer} from "./helpers" //getTones and Clarify rely on toneJSON. Implementing it in firebase would be better //import tonesData from "./tones.json" assert { type: "json"}; import { readFile } from 'fs/promises'; -const tonesData = JSON.parse( - await readFile("./src/tones.json", "utf8") -); //console.log(tonesData.tones); @@ -20,8 +17,12 @@ export interface Tone { indicator: string; } -//Import tones from tones.json -const tones: Tone[] = tonesData.tones; +async function initializeTones(): Promise { + let tonesData: Tone[]; + return tonesData = JSON.parse( + await readFile("./src/tones.json", "utf8") + ).tones; +} export async function ping(interaction: ChatInputCommandInteraction): Promise { await interaction.deferReply(); @@ -156,6 +157,8 @@ export async function action(interaction: ChatInputCommandInteraction export async function postemptiveToneAdd(interaction: MessageContextMenuCommandInteraction): Promise { await interaction.deferReply(); + const tones: Tone[] = await initializeTones(); + //copy-paste select menu for tones: //create the tone menu which allows a user to select 1-5 tones const toneMenu = new StringSelectMenuBuilder() @@ -206,6 +209,8 @@ export async function postemptiveToneAdd(interaction: MessageContextMenuCommandI export async function getTones(interaction: ChatInputCommandInteraction): Promise { await interaction.deferReply(); + const tones: Tone[] = await initializeTones(); + //create the tone menu which allows a user to select 1-5 tones const toneMenu = new StringSelectMenuBuilder() .setCustomId('tone select menu') @@ -279,6 +284,8 @@ export async function clarify(interaction: MessageContextMenuCommandInteraction< content: "Thanks for pointing that out, I'll ask for you!" }) + const tones: Tone[] = await initializeTones(); + //create the tone menu and add the options from toneJSON const toneMenu = new StringSelectMenuBuilder() .setCustomId('tone select menu') diff --git a/src/registerCommands.ts b/src/registerCommands.ts index 28aee5a..cddd06a 100644 --- a/src/registerCommands.ts +++ b/src/registerCommands.ts @@ -42,7 +42,7 @@ async function updateCommands(commands: command[]): Promise { } updateCommands([ - { + /*{ name: "ping", description: 'test bot and return "pong"', type: 1, @@ -51,13 +51,13 @@ updateCommands([ name: "action", description: "tests the action row feature of discord", type: 1, - }, + },*/ { name: "list-tones", description: "lists a sample of tones and their description", type: 1, }, - { + /*{ name: "mood", description: "Sets the user's current mood", options: [ @@ -78,21 +78,21 @@ updateCommands([ { name: "Tone", type: 3, - }, + },*/ { name: "Add Tone", type: 3, }, - { + /*{ name: "Clarify", type: 3, - }, + },*/ { name: "In-Depth Clarification", type: 3, }, - { + /*{ name:"Request Anonymous Clarification", type: 3, - } + },*/ ]); \ No newline at end of file diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 91f579b..b7fed97 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -208,12 +208,12 @@ export default class MockDiscord { id: BigInt(1), reply: jest.fn((replyOptions: string | MessagePayload | InteractionReplyOptions) => { this.interactionReply = replyOptions; - return Promise.resolve(); + return Promise.resolve(this.createMockMessage(replyOptions as MessageCreateOptions)); }), deferReply: jest.fn(), editReply: jest.fn((reply: string | MessagePayload | InteractionEditReplyOptions) => { this.interactionReply = reply; - return Promise.resolve(); + return Promise.resolve(this.createMockMessage(reply as MessageCreateOptions)); }), isRepliable: jest.fn(() => true), member: mockMember