mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
replace axios with node-fetch and change code formatting
This commit is contained in:
289
app.js
289
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,
|
||||
]);
|
||||
});
|
||||
|
||||
109
commands.js
109
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,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
155
game.js
155
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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
45
utils.js
45
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user