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 { getShuffledOptions, getResult } from './game.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}`} }); // Store for in-progress games. In production, you'd want to use a DB 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; /** * 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; // 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 }] }] } }); } } /** * 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() }] }] } }); await client({ url, method: 'delete' }); return; } 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": [] }}); return; } catch (err) { console.error(`Error sending message: ${err}`); } } } } }); app.listen(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]); });