diff --git a/app.js b/app.js index ffd31e0..d65efbe 100644 --- a/app.js +++ b/app.js @@ -1,20 +1,24 @@ -import 'dotenv/config' -import express from 'express' -import axios from 'axios'; -import { InteractionType, InteractionResponseType } from 'discord-interactions'; -import { VerifyDiscordRequest, getRandomEmoji, ComponentType, ButtonStyle, DiscordAPI } from './utils.js'; +import 'dotenv/config'; +import express from 'express'; +import { InteractionType, InteractionResponseType, InteractionResponseFlags } from 'discord-interactions'; +import { + VerifyDiscordRequest, + getRandomEmoji, + ComponentType, + ButtonStyle, + DiscordRequest, +} from './utils.js'; import { getShuffledOptions, getResult } from './game.js'; -import { CHALLENGE_COMMAND, TEST_COMMAND, HasGuildCommands } from './commands.js'; +import { + CHALLENGE_COMMAND, + TEST_COMMAND, + HasGuildCommands, +} from './commands.js'; // Create an express app const app = express(); // Parse request body and verifies incoming requests using discord-interactions package -app.use(express.json({verify: VerifyDiscordRequest(process.env.PUBLIC_KEY)})); - -// Create HTTP client instance with token -const client = axios.create({ - headers: {'Authorization': `Bot ${process.env.DISCORD_TOKEN}`} -}); +app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) })); // Store for in-progress games. In production, you'd want to use a DB const activeGames = {}; @@ -23,141 +27,158 @@ const activeGames = {}; * Interactions endpoint URL where Discord will send HTTP requests */ app.post('/interactions', async function (req, res) { - // Interaction type and data - const { type, id, data } = req.body; + // Interaction type and data + const { type, id, data } = req.body; - /** - * Handle verification requests - */ - if (type === InteractionType.PING) { - return res.send({ "type": InteractionResponseType.PONG }); + /** + * Handle verification requests + */ + if (type === InteractionType.PING) { + return res.send({ type: InteractionResponseType.PONG }); + } + + /** + * Handle slash command requests + * See https://discord.com/developers/docs/interactions/application-commands#slash-commands + */ + if (type === InteractionType.APPLICATION_COMMAND) { + const { name } = data; + + // "test" guild command + if (name === 'test') { + // Send a message into the channel where command was triggered from + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + // Fetches a random emoji to send from a helper function + content: 'hello world ' + getRandomEmoji(), + }, + }); } + // "challenge" guild command + if (name === 'challenge' && id) { + const userId = req.body.member.user.id; + // User's object choice + const objectName = req.body.data.options[0].value; - /** - * Handle slash command requests - * See https://discord.com/developers/docs/interactions/application-commands#slash-commands - */ - if (type === InteractionType.APPLICATION_COMMAND){ - const { name } = data; - - // "test" guild command - if (name === "test") { - // Send a message into the channel where command was triggered from - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { - // Fetches a random emoji to send from a helper function - "content": "hello world " + getRandomEmoji() - } - }); - } - // "challenge" guild command - if (name === "challenge" && id) { - const userId = req.body.member.user.id; - // User's object choice - const objectName = req.body.data.options[0].value; + // Create active game using message ID as the game ID + activeGames[id] = { + id: userId, + objectName: objectName, + }; - // Create active game using message ID as the game ID - activeGames[id] = { - "id": userId, - "objectName": objectName - }; - - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { - // Fetches a random emoji to send from a helper function - "content": `Rock papers scissors challenge from <@${userId}>`, - "components": [{ - "type": ComponentType.ACTION, - "components": [{ - "type": ComponentType.BUTTON, - // Append the game ID to use later on - "custom_id": `accept_button_${req.body.id}`, - "label": "Accept", - "style": ButtonStyle.PRIMARY - }] - }] - } - }); - } + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + // Fetches a random emoji to send from a helper function + content: `Rock papers scissors challenge from <@${userId}>`, + components: [ + { + type: ComponentType.ACTION, + components: [ + { + type: ComponentType.BUTTON, + // Append the game ID to use later on + custom_id: `accept_button_${req.body.id}`, + label: 'Accept', + style: ButtonStyle.PRIMARY, + }, + ], + }, + ], + }, + }); } + } - /** - * Handle requests from interactive components - * See https://discord.com/developers/docs/interactions/message-components#responding-to-a-component-interaction - */ - if (type === InteractionType.MESSAGE_COMPONENT){ - // custom_id set in payload when sending message component - const componentId = data.custom_id; + /** + * Handle requests from interactive components + * See https://discord.com/developers/docs/interactions/message-components#responding-to-a-component-interaction + */ + if (type === InteractionType.MESSAGE_COMPONENT) { + // custom_id set in payload when sending message component + const componentId = data.custom_id; - if (componentId.startsWith('accept_button_')) { - // get the associated game ID - const gameId = componentId.replace('accept_button_', ''); - // Delete message with token in request body - const url = DiscordAPI(`webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`); - try { - await res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { - // Fetches a random emoji to send from a helper function - "content": "What's your object of choice?", - // Indicates it'll be an ephemeral message - "flags": 64, - "components": [{ - "type": ComponentType.ACTION, - "components": [{ - "type": ComponentType.SELECT, - // Append game ID - "custom_id": `select_choice_${gameId}`, - "options": getShuffledOptions() - }] - }] - } - }); + if (componentId.startsWith('accept_button_')) { + // get the associated game ID + const gameId = componentId.replace('accept_button_', ''); + // Delete message with token in request body + const endpoint = `webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`; + try { + await res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + // Fetches a random emoji to send from a helper function + content: 'What is your object of choice?', + // Indicates it'll be an ephemeral message + flags: InteractionResponseFlags.EPHEMERAL, + components: [ + { + type: ComponentType.ACTION, + components: [ + { + type: ComponentType.SELECT, + // Append game ID + custom_id: `select_choice_${gameId}`, + options: getShuffledOptions(), + }, + ], + }, + ], + }, + }); - await client({ url, method: 'delete' }); - } catch (err) { - console.error('Error sending message:', err); - } - } else if (componentId.startsWith('select_choice_')) { - // get the associated game ID - const gameId = componentId.replace('select_choice_', ''); - - if (activeGames[gameId]) { - // Get user ID and object choice for responding user - const userId = req.body.member.user.id; - const objectName = data.values[0]; - // Calculate result from helper function - const resultStr = getResult(activeGames[gameId], {id: userId, objectName}); - - // Remove game from storage - delete activeGames[gameId]; - // Update message with token in request body - const url = DiscordAPI(`webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`); - - try { - // Send results - await res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { "content": resultStr } - }); - - await client({ url, method: 'patch', data: { - "content": "Nice choice " + getRandomEmoji(), - "components": [] - }}); - } catch (err) { - console.error('Error sending message:', err); - } + await DiscordRequest(endpoint, { method: 'DELETE' }); + } catch (err) { + console.error('Error sending message:', err); + } + } else if (componentId.startsWith('select_choice_')) { + // get the associated game ID + const gameId = componentId.replace('select_choice_', ''); + + if (activeGames[gameId]) { + // Get user ID and object choice for responding user + const userId = req.body.member.user.id; + const objectName = data.values[0]; + // Calculate result from helper function + const resultStr = getResult(activeGames[gameId], { + id: userId, + objectName, + }); + + // Remove game from storage + delete activeGames[gameId]; + // Update message with token in request body + const endpoint = `webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`; + + try { + // Send results + await res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { content: resultStr }, + }); + + await DiscordRequest(endpoint, { + method: 'PATCH', + body: { + content: "Nice choice " + getRandomEmoji(), + components: [] } + }); + } catch (err) { + console.error('Error sending message:', err); } + } } + } }); app.listen(3000, () => { - console.log('Listening on port 3000'); + console.log('Listening on port 3000'); - // Check if guild commands from commands.json are installed (if not, install them) - HasGuildCommands(client, process.env.APP_ID, process.env.GUILD_ID, [TEST_COMMAND, CHALLENGE_COMMAND]); + // Check if guild commands from commands.json are installed (if not, install them) + HasGuildCommands(process.env.APP_ID, process.env.GUILD_ID, [ + TEST_COMMAND, + CHALLENGE_COMMAND, + ]); }); diff --git a/commands.js b/commands.js index ed938c8..9331100 100644 --- a/commands.js +++ b/commands.js @@ -1,79 +1,80 @@ -import { getRPSChoices } from "./game.js"; -import { capitalize, DiscordAPI } from "./utils.js"; +import { getRPSChoices } from './game.js'; +import { capitalize, DiscordRequest } from './utils.js'; -export async function HasGuildCommands(client, appId, guildId, commands) { - if (guildId === '' || appId === '') return; +export async function HasGuildCommands(appId, guildId, commands) { + if (guildId === '' || appId === '') return; - commands.forEach(c => HasGuildCommand(client, appId, guildId, c)); + commands.forEach((c) => HasGuildCommand(appId, guildId, c)); } // Checks for a command -async function HasGuildCommand(client, appId, guildId, command) { - // API URL to get and post guild commands - const url = DiscordAPI(`applications/${appId}/guilds/${guildId}/commands`); +async function HasGuildCommand(appId, guildId, command) { + // API endpoint to get and post guild commands + const endpoint = `applications/${appId}/guilds/${guildId}/commands`; - try { - const { data } = await client({ url, method: 'get'}); - if (data) { - const installedNames = data.map((c) => c["name"]); - // This is just matching on the name, so it's not good for updates - if (!installedNames.includes(command["name"])) { - InstallGuildCommand(client, appId, guildId, command); - } else { - console.log(`"${command["name"]}" command already installed`) - } - } - } catch (err) { - console.error('Error installing commands: ', err); + try { + const res = await DiscordRequest(endpoint, { method: 'GET' }); + const data = await res.json(); + if (data) { + const installedNames = data.map((c) => c['name']); + // This is just matching on the name, so it's not good for updates + if (!installedNames.includes(command['name'])) { + InstallGuildCommand(appId, guildId, command); + } else { + console.log(`"${command['name']}" command already installed`); + } } + } catch (err) { + console.error('Error installing commands: ', err); + } } // Installs a command -export async function InstallGuildCommand(client, appId, guildId, command) { - // API URL to get and post guild commands - const url = DiscordAPI(`applications/${appId}/guilds/${guildId}/commands`); - // install command - try { - await client({ url, method: 'post', data: command}); - } catch (err) { - console.error('Error installing commands: ', err); - } +export async function InstallGuildCommand(appId, guildId, command) { + // API endpoint to get and post guild commands + const endpoint = `applications/${appId}/guilds/${guildId}/commands`; + // install command + try { + await DiscordRequest(endpoint, { method: 'POST', body: command }); + } catch (err) { + console.error('Error installing commands: ', err); + } } // Get the game choices from game.js function createCommandChoices() { - const choices = getRPSChoices(); - const commandChoices = []; + const choices = getRPSChoices(); + const commandChoices = []; - for (let choice of choices) { - commandChoices.push({ - "name": capitalize(choice), - "value": choice.toLowerCase() - }); - } + for (let choice of choices) { + commandChoices.push({ + name: capitalize(choice), + value: choice.toLowerCase(), + }); + } - return commandChoices; + return commandChoices; } // Simple test command export const TEST_COMMAND = { - "name": "test", - "description": "Basic guild command", - "type": 1 + name: 'test', + description: 'Basic guild command', + type: 1, }; // Command containing options export const CHALLENGE_COMMAND = { - "name": "challenge", - "description": "Challenge to a match of rock paper scissors", - "options": [ - { - "type": 3, - "name": "object", - "description": "Pick your object", - "required": true, - "choices": createCommandChoices() - } - ], - "type": 1 + name: 'challenge', + description: 'Challenge to a match of rock paper scissors', + options: [ + { + type: 3, + name: 'object', + description: 'Pick your object', + required: true, + choices: createCommandChoices(), + }, + ], + type: 1, }; diff --git a/examples/button.js b/examples/button.js index 0fd7099..2731563 100644 --- a/examples/button.js +++ b/examples/button.js @@ -1,61 +1,65 @@ -import 'dotenv/config' -import express from 'express' +import 'dotenv/config'; +import express from 'express'; import { InteractionType, InteractionResponseType } from 'discord-interactions'; import { VerifyDiscordRequest, ComponentType, ButtonStyle } from '../utils.js'; // Create and configure express app const app = express(); -app.use(express.json({verify: VerifyDiscordRequest(process.env.PUBLIC_KEY)})); +app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) })); app.post('/interactions', function (req, res) { - // Interaction type and data - const { type, data } = req.body; - /** - * Handle slash command requests - */ - if (type === InteractionType.APPLICATION_COMMAND){ - // Slash command with name of "test" - if (data.name === "test") { - // Send a message with a button - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { - "content": 'A message with a button', - // Buttons are inside of action rows - "components": [{ - "type": ComponentType.ACTION, - "components": [{ - "type": ComponentType.BUTTON, - // Value for your app to identify the button - "custom_id": "my_button", - "label": "Click", - "style": ButtonStyle.PRIMARY - }] - }] - } - }); - } + // Interaction type and data + const { type, data } = req.body; + /** + * Handle slash command requests + */ + if (type === InteractionType.APPLICATION_COMMAND) { + // Slash command with name of "test" + if (data.name === 'test') { + // Send a message with a button + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: 'A message with a button', + // Buttons are inside of action rows + components: [ + { + type: ComponentType.ACTION, + components: [ + { + type: ComponentType.BUTTON, + // Value for your app to identify the button + custom_id: 'my_button', + label: 'Click', + style: ButtonStyle.PRIMARY, + }, + ], + }, + ], + }, + }); } + } - /** - * Handle requests from interactive components - */ - if (type === InteractionType.MESSAGE_COMPONENT){ - // custom_id set in payload when sending message component - const componentId = data.custom_id; - // user who clicked button - const userId = req.body.member.user.id; + /** + * Handle requests from interactive components + */ + if (type === InteractionType.MESSAGE_COMPONENT) { + // custom_id set in payload when sending message component + const componentId = data.custom_id; + // user who clicked button + const userId = req.body.member.user.id; - if (componentId === 'my_button') { - console.log(req.body); - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { "content": `<@${userId} clicked the button` } - }); - } + if (componentId === 'my_button') { + console.log(req.body); + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { content: `<@${userId} clicked the button` }, + }); } + } }); app.listen(3000, () => { - console.log('Listening on port 3000'); + console.log('Listening on port 3000'); }); diff --git a/examples/command.js b/examples/command.js index b3166ef..059f972 100644 --- a/examples/command.js +++ b/examples/command.js @@ -1,70 +1,65 @@ -import 'dotenv/config' -import express from 'express' +import express from 'express'; import { InteractionType, InteractionResponseType } from 'discord-interactions'; -import { VerifyDiscordRequest, DiscordAPI } from '../utils.js'; -import axios from 'axios'; +import { VerifyDiscordRequest, DiscordRequest } from '../utils.js'; // Create and configure express app const app = express(); -app.use(express.json({verify: VerifyDiscordRequest(process.env.PUBLIC_KEY)})); +app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) })); app.post('/interactions', function (req, res) { - // Interaction type and data - const { type, data } = req.body; - /** - * Handle slash command requests - */ - if (type === InteractionType.APPLICATION_COMMAND){ - // Slash command with name of "test" - if (data.name === "test") { - // Send a message as response - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { "content": "A wild message appeared" } - }); - } + // Interaction type and data + const { type, data } = req.body; + /** + * Handle slash command requests + */ + if (type === InteractionType.APPLICATION_COMMAND) { + // Slash command with name of "test" + if (data.name === 'test') { + // Send a message as response + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { content: 'A wild message appeared' }, + }); } + } }); -function createCommand() { - const appId = process.env.APP_ID; - const guildId = process.env.GUILD_ID; - - /** - * Globally-scoped slash commands (generally only recommended for production) - * See https://discord.com/developers/docs/interactions/application-commands#create-global-application-command - */ - // const globalUrl = DiscordAPI(`applications/${appId}/commands`); - - /** - * Guild-scoped slash commands - * See https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command - */ - const guildUrl = DiscordAPI(`applications/${appId}/guilds/${guildId}/commands`); - const commandBody = { - "name": "test", - "description": "Just your average command", - // chat command (see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types) - "type": 1 - }; +async function createCommand() { + const appId = process.env.APP_ID; + const guildId = process.env.GUILD_ID; - try { - // Send HTTP request with bot token - let res = await axios({ - url: guildUrl, - method: 'post', - data: commandBody, - headers: {'Authorization': `Bot ${process.env.DISCORD_TOKEN}`} - }); - console.log(res.body); - } catch (err) { - console.error('Error installing commands: ', err); - } - + /** + * Globally-scoped slash commands (generally only recommended for production) + * See https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + */ + // const globalEndpoint = `applications/${appId}/commands`; + + /** + * Guild-scoped slash commands + * See https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + */ + const guildEndpoint = `applications/${appId}/guilds/${guildId}/commands`; + const commandBody = { + name: 'test', + description: 'Just your average command', + // chat command (see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types) + type: 1, + }; + + try { + // Send HTTP request with bot token + const res = await DiscordRequest(guildEndpoint, { + method: 'POST', + body: commandBody, + }); + console.log(await res.json()); + } catch (err) { + console.error('Error installing commands: ', err); + } } app.listen(3000, () => { - console.log('Listening on port 3000'); + console.log('Listening on port 3000'); - createCommand(); + createCommand(); }); diff --git a/examples/modal.js b/examples/modal.js index 512abbb..d16a6b0 100644 --- a/examples/modal.js +++ b/examples/modal.js @@ -1,84 +1,86 @@ -import 'dotenv/config' -import express from 'express' +import 'dotenv/config'; +import express from 'express'; import { InteractionType, InteractionResponseType } from 'discord-interactions'; import { VerifyDiscordRequest, ComponentType } from '../utils.js'; // Create and configure express app const app = express(); -app.use(express.json({verify: VerifyDiscordRequest(process.env.PUBLIC_KEY)})); +app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) })); app.post('/interactions', function (req, res) { - // Interaction type and data - const { type, data } = req.body; - /** - * Handle slash command requests - */ - if (type === InteractionType.APPLICATION_COMMAND){ - // Slash command with name of "test" - if (data.name === "test") { - // Send a modal as response - return res.send({ - "type": InteractionResponseType.APPLICATION_MODAL, - "data": { - "custom_id": "my_modal", - "title": "Modal title", - "components": [ - { - // Text inputs must be inside of an action component - "type": ComponentType.ACTION, - "components": [ - { - // See https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure - "type": ComponentType.INPUT, - "custom_id": "my_text", - "style": 1, - "label": "Type some text" - } - ] - }, - { - "type": ComponentType.ACTION, - "components": [ - { - "type": ComponentType.INPUT, - "custom_id": "my_longer_text", - // Bigger text box for input - "style": 2, - "label": "Type some (longer) text" - } - ] - } - ] - } - }); - } + // Interaction type and data + const { type, data } = req.body; + /** + * Handle slash command requests + */ + if (type === InteractionType.APPLICATION_COMMAND) { + // Slash command with name of "test" + if (data.name === 'test') { + // Send a modal as response + return res.send({ + type: InteractionResponseType.APPLICATION_MODAL, + data: { + custom_id: 'my_modal', + title: 'Modal title', + components: [ + { + // Text inputs must be inside of an action component + type: ComponentType.ACTION, + components: [ + { + // See https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure + type: ComponentType.INPUT, + custom_id: 'my_text', + style: 1, + label: 'Type some text', + }, + ], + }, + { + type: ComponentType.ACTION, + components: [ + { + type: ComponentType.INPUT, + custom_id: 'my_longer_text', + // Bigger text box for input + style: 2, + label: 'Type some (longer) text', + }, + ], + }, + ], + }, + }); } + } - /** - * Handle modal submissions - */ - if (type === InteractionType.APPLICATION_MODAL_SUBMIT) { - // custom_id of modal - const modalId = data.custom_id; - // user ID of member who filled out modal - const userId = req.body.member.user.id; + /** + * Handle modal submissions + */ + if (type === InteractionType.APPLICATION_MODAL_SUBMIT) { + // custom_id of modal + const modalId = data.custom_id; + // user ID of member who filled out modal + const userId = req.body.member.user.id; - if (modalId === 'my_modal') { - let modalValues = ''; - // Get value of text inputs - for (let action of data.components) { - let inputComponent = action.components[0]; - modalValues += `${inputComponent.custom_id}: ${inputComponent.value}\n` - } - - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { "content": `<@${userId}> typed the following (in a modal):\n\n${modalValues}` } - }) - } + if (modalId === 'my_modal') { + let modalValues = ''; + // Get value of text inputs + for (let action of data.components) { + let inputComponent = action.components[0]; + modalValues += `${inputComponent.custom_id}: ${inputComponent.value}\n`; + } + + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `<@${userId}> typed the following (in a modal):\n\n${modalValues}`, + }, + }); } + } }); app.listen(3000, () => { - console.log('Listening on port 3000'); + console.log('Listening on port 3000'); }); diff --git a/examples/selectMenu.js b/examples/selectMenu.js index 8341a26..4fba284 100644 --- a/examples/selectMenu.js +++ b/examples/selectMenu.js @@ -1,76 +1,80 @@ -import 'dotenv/config' -import express from 'express' +import 'dotenv/config'; +import express from 'express'; import { InteractionType, InteractionResponseType } from 'discord-interactions'; import { VerifyDiscordRequest, ComponentType } from '../utils.js'; // Create and configure express app const app = express(); -app.use(express.json({verify: VerifyDiscordRequest(process.env.PUBLIC_KEY)})); +app.use(express.json({ verify: VerifyDiscordRequest(process.env.PUBLIC_KEY) })); app.post('/interactions', function (req, res) { - // Interaction type and data - const { type, data } = req.body; - /** - * Handle slash command requests - */ - if (type === InteractionType.APPLICATION_COMMAND){ - // Slash command with name of "test" - if (data.name === "test") { - // Send a message with a button - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { - "content": 'A message with a button', - // Selects are inside of action rows - "components": [{ - "type": ComponentType.ACTION, - "components": [{ - "type": ComponentType.SELECT, - // Value for your app to identify the select menu interactions - "custom_id": "my_select", - // Select options - see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure - "options": [ - { - "label": "Option #1", - "value": "option_1", - "description": "The very first option" - }, - { - "label": "Second option", - "value": "option_2", - "description": "The second AND last option" - } - ] - }] - }] - } - }); - } + // Interaction type and data + const { type, data } = req.body; + /** + * Handle slash command requests + */ + if (type === InteractionType.APPLICATION_COMMAND) { + // Slash command with name of "test" + if (data.name === 'test') { + // Send a message with a button + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: 'A message with a button', + // Selects are inside of action rows + components: [ + { + type: ComponentType.ACTION, + components: [ + { + type: ComponentType.SELECT, + // Value for your app to identify the select menu interactions + custom_id: 'my_select', + // Select options - see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure + options: [ + { + label: 'Option #1', + value: 'option_1', + description: 'The very first option', + }, + { + label: 'Second option', + value: 'option_2', + description: 'The second AND last option', + }, + ], + }, + ], + }, + ], + }, + }); } + } - /** - * Handle requests from interactive components - */ - if (type === InteractionType.MESSAGE_COMPONENT){ - // custom_id set in payload when sending message component - const componentId = data.custom_id; + /** + * Handle requests from interactive components + */ + if (type === InteractionType.MESSAGE_COMPONENT) { + // custom_id set in payload when sending message component + const componentId = data.custom_id; - if (componentId === 'my_select') { - console.log(req.body); + if (componentId === 'my_select') { + console.log(req.body); - // Get selected option from payload - const selectedOption = data.values[0]; - const userId = req.body.member.user.id; + // Get selected option from payload + const selectedOption = data.values[0]; + const userId = req.body.member.user.id; - // Send results - return res.send({ - "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - "data": { "content": `<@${userId}> selected ${selectedOption}` } - }); - } + // Send results + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { content: `<@${userId}> selected ${selectedOption}` }, + }); } + } }); app.listen(3000, () => { - console.log('Listening on port 3000'); + console.log('Listening on port 3000'); }); diff --git a/game.js b/game.js index b0327d9..e56edf3 100644 --- a/game.js +++ b/game.js @@ -1,92 +1,103 @@ -import { capitalize } from './utils.js' +import { capitalize } from './utils.js'; export function getResult(p1, p2) { - let gameResult; - if (RPSChoices[p1.objectName] && RPSChoices[p1.objectName][p2.objectName]) { - // o1 wins - gameResult = { win: p1, lose: p2, verb: RPSChoices[p1.objectName][p2.objectName] }; - } else if (RPSChoices[p2.objectName] && RPSChoices[p2.objectName][p1.objectName]) { - // o2 wins - gameResult = { win: p2, lose: p1, verb: RPSChoices[p2.objectName][p1.objectName] }; - } else { - // tie -- win/lose don't - gameResult = { win: p1, lose: p2, verb: 'tie' }; - } + let gameResult; + if (RPSChoices[p1.objectName] && RPSChoices[p1.objectName][p2.objectName]) { + // o1 wins + gameResult = { + win: p1, + lose: p2, + verb: RPSChoices[p1.objectName][p2.objectName], + }; + } else if ( + RPSChoices[p2.objectName] && + RPSChoices[p2.objectName][p1.objectName] + ) { + // o2 wins + gameResult = { + win: p2, + lose: p1, + verb: RPSChoices[p2.objectName][p1.objectName], + }; + } else { + // tie -- win/lose don't + gameResult = { win: p1, lose: p2, verb: 'tie' }; + } - return formatResult(gameResult) + return formatResult(gameResult); } function formatResult(result) { - const { win, lose, verb } = result; - return verb === 'tie' ? - `<@${win.id}> and <@${lose.id}> draw with **${win.objectName}**` : - `<@${win.id}>'s **${win.objectName}** ${verb} <@${lose.id}>'s **${lose.objectName}**`; + const { win, lose, verb } = result; + return verb === 'tie' + ? `<@${win.id}> and <@${lose.id}> draw with **${win.objectName}**` + : `<@${win.id}>'s **${win.objectName}** ${verb} <@${lose.id}>'s **${lose.objectName}**`; } // this is just to figure out winner + verb const RPSChoices = { - "rock": { - "description": "sedimentary, igneous, or perhaps even metamorphic", - "virus": "outwaits", - "computer": "smashes", - "scissors": "crushes" - }, - "cowboy": { - "description": "yeehaw~", - "scissors": "puts away", - "wumpus": "lassos", - "rock": "steel-toe kicks" - }, - "scissors": { - "description": "careful ! sharp ! edges !!", - "paper": "cuts", - "computer": "cuts cord of", - "virus": "cuts DNA of" - }, - "virus": { - "description": "genetic mutation, malware, or something inbetween", - "cowboy": "infects", - "computer": "corrupts", - "wumpus": "infects" - }, - "computer": { - "description": "beep boop beep bzzrrhggggg", - "cowboy": "overwhelms", - "paper": "uninstalls firmware for", - "wumpus": "deletes assets for" - }, - "wumpus": { - "description": "the purple Discord fella", - "paper": "draws picture on", - "rock": "paints cute face on", - "scissors": "admires own reflection in" - }, - "paper": { - "description": "versatile and iconic", - "virus": "ignores", - "cowboy": "gives papercut to", - "rock": "covers" - } + rock: { + description: 'sedimentary, igneous, or perhaps even metamorphic', + virus: 'outwaits', + computer: 'smashes', + scissors: 'crushes', + }, + cowboy: { + description: 'yeehaw~', + scissors: 'puts away', + wumpus: 'lassos', + rock: 'steel-toe kicks', + }, + scissors: { + description: 'careful ! sharp ! edges !!', + paper: 'cuts', + computer: 'cuts cord of', + virus: 'cuts DNA of', + }, + virus: { + description: 'genetic mutation, malware, or something inbetween', + cowboy: 'infects', + computer: 'corrupts', + wumpus: 'infects', + }, + computer: { + description: 'beep boop beep bzzrrhggggg', + cowboy: 'overwhelms', + paper: 'uninstalls firmware for', + wumpus: 'deletes assets for', + }, + wumpus: { + description: 'the purple Discord fella', + paper: 'draws picture on', + rock: 'paints cute face on', + scissors: 'admires own reflection in', + }, + paper: { + description: 'versatile and iconic', + virus: 'ignores', + cowboy: 'gives papercut to', + rock: 'covers', + }, }; export function getRPSChoices() { - return Object.keys(RPSChoices); + return Object.keys(RPSChoices); } // Function to fetch shuffled options for select menu export function getShuffledOptions() { - const allChoices = getRPSChoices(); - const options = []; + const allChoices = getRPSChoices(); + const options = []; - for (let c of allChoices) { - // Formatted for select menus - // https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure - options.push({ - "label": capitalize(c), - "value": c.toLowerCase(), - "description": RPSChoices[c]["description"] - }); - } + for (let c of allChoices) { + // Formatted for select menus + // https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure + options.push({ + label: capitalize(c), + value: c.toLowerCase(), + description: RPSChoices[c]['description'], + }); + } - return options.sort(() => Math.random() - 0.5); + return options.sort(() => Math.random() - 0.5); } diff --git a/package.json b/package.json index 08113b7..8f457a8 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,10 @@ "author": "Shay DeWael", "license": "MIT", "dependencies": { - "axios": "^0.26.1", "discord-interactions": "^3.1.0", "dotenv": "^16.0.0", - "express": "^4.17.3" + "express": "^4.17.3", + "node-fetch": "^3.2.3" }, "devDependencies": { "nodemon": "^2.0.15" diff --git a/utils.js b/utils.js index 52eb3a8..fe11506 100644 --- a/utils.js +++ b/utils.js @@ -1,3 +1,5 @@ +import 'dotenv/config'; +import fetch from 'node-fetch'; import { verifyKey } from 'discord-interactions'; export function VerifyDiscordRequest(clientKey) { @@ -10,32 +12,45 @@ export function VerifyDiscordRequest(clientKey) { res.status(401).send('Bad request signature'); throw new Error('Bad request signature'); } - } + }; } -export function DiscordAPI(url) { return 'https://discord.com/api/v9/' + url }; +export function DiscordRequest(endpoint, options) { + // append endpoint to root API URL + const url = 'https://discord.com/api/v9/' + endpoint; + // Stringify payloads + if (options.body) options.body = JSON.stringify(options.body); + // Use node-fetch to make requests + return fetch(url, { + headers: { + Authorization: `Bot ${process.env.DISCORD_TOKEN}`, + 'Content-Type': 'application/json; charset=UTF-8', + }, + ...options + }); +} // Simple method that returns a random emoji from list export function getRandomEmoji() { - const emojiList = ['😭', 'πŸ˜„', '😌', 'πŸ€“', '😎', '😀', 'πŸ€–', 'πŸ˜Άβ€πŸŒ«οΈ', '🌏', 'πŸ“Έ', 'πŸ’Ώ', 'πŸ‘‹', '🌊', '✨']; + const emojiList = ['😭','πŸ˜„','😌','πŸ€“','😎','😀','πŸ€–','πŸ˜Άβ€πŸŒ«οΈ','🌏','πŸ“Έ','πŸ’Ώ','πŸ‘‹','🌊','✨']; return emojiList[Math.floor(Math.random() * emojiList.length)]; } export function capitalize(str) { - return str.charAt(0).toUpperCase() + str.slice(1); + return str.charAt(0).toUpperCase() + str.slice(1); } export const ComponentType = { - ACTION: 1, - BUTTON: 2, - SELECT: 3, - INPUT: 4 -} + ACTION: 1, + BUTTON: 2, + SELECT: 3, + INPUT: 4, +}; export const ButtonStyle = { - PRIMARY: 1, - SECONDARY: 2, - SUCCESS: 3, - DANGER: 4, - LINK: 5 -} + PRIMARY: 1, + SECONDARY: 2, + SUCCESS: 3, + DANGER: 4, + LINK: 5, +};