Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anya discord bot #69

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.sample

This file was deleted.

199 changes: 176 additions & 23 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,210 @@ import express from 'express';
import {
InteractionType,
InteractionResponseType,
verifyKeyMiddleware,
MessageComponentTypes,
ButtonStyleTypes,
InteractionResponseFlags,
verifyKeyMiddleware
} from 'discord-interactions';
import { getRandomEmoji } from './utils.js';
import { getRandomEmoji, DiscordRequest } from './utils.js';
import { getResult, getRPSChoices, getShuffledOptions } from './game.js';

// Create an express app
const app = express();
// Get port, or default to 3000
const PORT = process.env.PORT || 3000;
const activeGames = {};

/**
* Interactions endpoint URL where Discord will send HTTP requests
* Parse request body and verifies incoming requests using discord-interactions package
*/
app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async function (req, res) {
// Interaction type and data
const { type, 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()}`,
},
});
}

console.error(`unknown command: ${name}`);
return res.status(400).json({ error: 'unknown command' });
if (name === 'challenge') {
const userId = req.body.member?.user?.id || req.body.user?.id;
const objectName = data.options[0].value;
const gameId = req.body.id;

activeGames[gameId] = {
challengerId: userId,
challengerChoice: objectName,
timestamp: Date.now()
};

return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `<@${userId}> challenges you to Rock Paper Scissors! ${getRandomEmoji()}`,
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
custom_id: `accept_button_${gameId}`,
label: 'Accept Challenge',
style: ButtonStyleTypes.PRIMARY,
},
],
},
],
},
});
}
}

console.error('unknown interaction type', type);
return res.status(400).json({ error: 'unknown interaction type' });
if (type === InteractionType.MESSAGE_COMPONENT) {
const componentId = data.custom_id;

if (componentId.startsWith('accept_button_')) {
const gameId = componentId.replace('accept_button_', '');
const game = activeGames[gameId];

if (!game) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'This challenge is no longer valid.',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}

const userId = req.body.member?.user?.id || req.body.user?.id;

try {
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Choose your object! 🎮',
flags: InteractionResponseFlags.EPHEMERAL,
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.STRING_SELECT,
custom_id: `select_choice_${gameId}`,
options: getShuffledOptions(),
},
],
},
],
},
});

const endpoint = `webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`;
await DiscordRequest(endpoint, { method: 'DELETE' });
} catch (err) {
console.error('Error handling button interaction:', err);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'An error occurred while processing the challenge.',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
} else if (componentId.startsWith('select_choice_')) {
const gameId = componentId.replace('select_choice_', '');
const game = activeGames[gameId];

if (!game) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'This game is no longer valid.',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}

const userId = req.body.member?.user?.id || req.body.user?.id;
const objectName = data.values[0];

try {
const result = getResult(
{ id: game.challengerId, objectName: game.challengerChoice },
{ id: userId, objectName: objectName }
);

// Log both players' choices and the result
console.log(
`Game ID: ${gameId}, Challenger: <@${game.challengerId}> (${game.challengerChoice}), Opponent: <@${userId}> (${objectName}), Result: ${result}`
);

let winnerMessage;
const winnerId = getWinnerId(game.challengerChoice, objectName, game.challengerId, userId);

if (winnerId === game.challengerId) {
winnerMessage = `<@${game.challengerId}> wins! They chose ${game.challengerChoice}. ${result}`;
} else if (winnerId === userId) {
winnerMessage = `<@${userId}> wins! They chose ${objectName}. ${result}`;
} else {
winnerMessage = "It's a draw! Both players chose the same object.";
}

delete activeGames[gameId];

await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `${winnerMessage} ${getRandomEmoji()}`,
},
});

const endpoint = `webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`;
await DiscordRequest(endpoint, {
method: 'PATCH',
body: {
content: `You chose ${objectName}! ${getRandomEmoji()}`,
components: [],
},
});
} catch (err) {
console.error('Error handling select menu interaction:', err);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'An error occurred while processing your choice.',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
}
}
});

function getWinnerId(challengerChoice, opponentChoice, challengerId, opponentId) {
const rules = {
rock: 'scissors',
paper: 'rock',
scissors: 'paper'
};

if (challengerChoice === opponentChoice) {
return null; // It's a draw
}

if (rules[challengerChoice] === opponentChoice) {
return challengerId; // Challenger wins
} else {
return opponentId; // Opponent wins
}
}

app.listen(PORT, () => {
console.log('Listening on port', PORT);
});
9 changes: 1 addition & 8 deletions commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dotenv/config';
import { getRPSChoices } from './game.js';
import { capitalize, InstallGlobalCommands } from './utils.js';

// Get the game choices from game.js
function createCommandChoices() {
const choices = getRPSChoices();
const commandChoices = [];
Expand All @@ -17,16 +16,12 @@ function createCommandChoices() {
return commandChoices;
}

// Simple test command
const TEST_COMMAND = {
name: 'test',
description: 'Basic command',
type: 1,
integration_types: [0, 1],
contexts: [0, 1, 2],
};

// Command containing options
const CHALLENGE_COMMAND = {
name: 'challenge',
description: 'Challenge to a match of rock paper scissors',
Expand All @@ -40,10 +35,8 @@ const CHALLENGE_COMMAND = {
},
],
type: 1,
integration_types: [0, 1],
contexts: [0, 2],
};

const ALL_COMMANDS = [TEST_COMMAND, CHALLENGE_COMMAND];

InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS);
75 changes: 36 additions & 39 deletions game.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,5 @@
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' };
}

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}**`;
}

// this is just to figure out winner + verb
const RPSChoices = {
rock: {
description: 'sedimentary, igneous, or perhaps even metamorphic',
Expand Down Expand Up @@ -80,18 +45,50 @@ const RPSChoices = {
},
};

export function getResult(p1, p2) {
if (p1.id === p2.id && p1.objectName === p2.objectName) {
return `<@${p1.id}> tied with themselves using **${p1.objectName}**!`;
}

let gameResult;
if (RPSChoices[p1.objectName] && RPSChoices[p1.objectName][p2.objectName]) {
gameResult = {
win: p1,
lose: p2,
verb: RPSChoices[p1.objectName][p2.objectName],
};
} else if (RPSChoices[p2.objectName] && RPSChoices[p2.objectName][p1.objectName]) {
gameResult = {
win: p2,
lose: p1,
verb: RPSChoices[p2.objectName][p1.objectName],
};
} else {
gameResult = { win: p1, lose: p2, verb: 'tie' };
}

return formatResult(gameResult);
}

function formatResult(result) {
const { win, lose, verb } = result;
if (win.id === lose.id) {
return `<@${win.id}> played against themselves! **${win.objectName}** vs **${lose.objectName}**${verb === 'tie' ? " - It's a tie!" : ` - ${win.objectName} ${verb} ${lose.objectName}`}`;
}
return verb === 'tie'
? `<@${win.id}> and <@${lose.id}> draw with **${win.objectName}**`
: `<@${win.id}>'s **${win.objectName}** ${verb} <@${lose.id}>'s **${lose.objectName}**`;
}

export function getRPSChoices() {
return Object.keys(RPSChoices);
}

// Function to fetch shuffled options for select menu
export function getShuffledOptions() {
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(),
Expand All @@ -100,4 +97,4 @@ export function getShuffledOptions() {
}

return options.sort(() => Math.random() - 0.5);
}
}
Loading