mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-01-18 16:37:40 +01:00
more
This commit is contained in:
@@ -11,6 +11,7 @@ Below is a basic overview of the project structure:
|
||||
|
||||
```
|
||||
├── examples -> short, feature-specific sample apps
|
||||
│ ├── app.js -> finished app.js code
|
||||
│ ├── button.js
|
||||
│ ├── command.js
|
||||
│ ├── modal.js
|
||||
@@ -72,6 +73,8 @@ node app.js
|
||||
|
||||
> ⚙️ A package [like `nodemon`](https://github.com/remy/nodemon), which watches for local changes and restarts your app, may be helpful while locally developing.
|
||||
|
||||
If you aren't following the [getting started guide](https://discord.com/developers/docs/getting-started), you can move the contents of `examples/app.js` (the finished `app.js` file) to the top-level `app.js`.
|
||||
|
||||
### Set up interactivity
|
||||
|
||||
The project needs a public endpoint where Discord can send requests. To develop and test locally, you can use something like [`ngrok`](https://ngrok.com/) to tunnel HTTP traffic.
|
||||
|
||||
115
app.js
115
app.js
@@ -52,121 +52,6 @@ app.post('/interactions', async function (req, res) {
|
||||
},
|
||||
});
|
||||
}
|
||||
// "challenge" 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,
|
||||
};
|
||||
|
||||
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: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
// Append the game ID to use later on
|
||||
custom_id: `accept_button_${req.body.id}`,
|
||||
label: 'Accept',
|
||||
style: ButtonStyleTypes.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 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: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.STRING_SELECT,
|
||||
// Append game ID
|
||||
custom_id: `select_choice_${gameId}`,
|
||||
options: getShuffledOptions(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// Delete previous message
|
||||
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 },
|
||||
});
|
||||
// Update ephemeral message
|
||||
await DiscordRequest(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
content: 'Nice choice ' + getRandomEmoji(),
|
||||
components: [],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error sending message:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
175
examples/app.js
Normal file
175
examples/app.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import {
|
||||
InteractionType,
|
||||
InteractionResponseType,
|
||||
InteractionResponseFlags,
|
||||
MessageComponentTypes,
|
||||
ButtonStyleTypes,
|
||||
} from 'discord-interactions';
|
||||
import { VerifyDiscordRequest, getRandomEmoji, DiscordRequest } from './utils.js';
|
||||
import { getShuffledOptions, getResult } from './game.js';
|
||||
|
||||
// Create an express app
|
||||
const app = express();
|
||||
// Get port, or default to 3000
|
||||
const PORT = process.env.PORT || 3000;
|
||||
// Parse request body and verifies incoming requests using discord-interactions package
|
||||
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 = {};
|
||||
|
||||
/**
|
||||
* 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" 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" 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,
|
||||
};
|
||||
|
||||
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: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
// Append the game ID to use later on
|
||||
custom_id: `accept_button_${req.body.id}`,
|
||||
label: 'Accept',
|
||||
style: ButtonStyleTypes.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 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: MessageComponentTypes.ACTION_ROW,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.STRING_SELECT,
|
||||
// Append game ID
|
||||
custom_id: `select_choice_${gameId}`,
|
||||
options: getShuffledOptions(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// Delete previous message
|
||||
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 },
|
||||
});
|
||||
// Update ephemeral message
|
||||
await DiscordRequest(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
content: 'Nice choice ' + getRandomEmoji(),
|
||||
components: [],
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error sending message:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('Listening on port', PORT);
|
||||
});
|
||||
Reference in New Issue
Block a user