Skip to content

Commit

Permalink
add support for modern Discord polls
Browse files Browse the repository at this point in the history
  • Loading branch information
Bongo9911 committed Sep 30, 2024
1 parent 4e253fa commit 15395f6
Show file tree
Hide file tree
Showing 7 changed files with 675 additions and 1,731 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# BongoBotSlash
Bongo Bot Slash is a remake of a Discord Bot that I made which improves greatly on lots of the issues present in the original version of the bot. The main purpose of the bot is for playing a game known as "Give & Take", a game in which players are presented with a list of items (usually all being a part of a category of some sort such as colors, fruits, Taylor Swift albums, etc.) each with a set amount of points at the start. When the player is not on cooldown, they may take an action to take 1 point away from one item and give it to another. Once an item reaches 0 points, it is removed from the game entirely and cannot be brought back. This continues until a certain number of items are left, at which point a final vote is started which polls users on which of the remaining items they want to win. Once the vote has completed, the item with the most votes is crowned the winner.

## Running the Bot

To run the bot follow these steps:

- Install node depenencies with `npm install`
- Run the bot by running `node index.js`
7 changes: 4 additions & 3 deletions commands/themes/createtheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ async function getEmojis(message, themeInfo) {
getColors(message, themeInfo);
}
else {
//TODO: validate these are valid emojis
themeInfo.emojis = message.content.split(/\r?\n/).filter(m => m.length);
if (themeInfo.emojis.length === themeInfo.items.length) {
getColors(message, themeInfo);
Expand Down Expand Up @@ -142,11 +143,11 @@ async function getColors(message, themeInfo) {
rgbcolors = [];
let valid = true;
for (let i = 0; i < hexcolors.length; ++i) {
if(!/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(hexcolors[i])) {
if (!/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(hexcolors[i])) {
message.reply("Invalid hex code: " + hexcolors[i] + " please make sure to include the # at the front of the hex code");
getColors(message, themeInfo);
}

}
if (valid) {
themeInfo.colors = rgbcolors;
Expand All @@ -167,7 +168,7 @@ async function getColors(message, themeInfo) {

async function finishCreateTheme(message, themeInfo) {

const theme = await Themes.create({
const theme = await Themes.create({
guild_id: message.guildId,
name: themeInfo.name,
created_user: message.author.id,
Expand Down
1 change: 1 addition & 0 deletions commands/themes/suggesttheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ async function getEmojis(message, themeInfo) {
getColors(message, themeInfo);
}
else {
//TODO: validate these are valid emojis
themeInfo.emojis = message.content.split(/\r?\n/).filter(m => m.length);
if (themeInfo.emojis.length === themeInfo.items.length) {
getColors(message, themeInfo);
Expand Down
9 changes: 5 additions & 4 deletions databaseModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const Games = sequelize.define('games', {
turns: Sequelize.INTEGER, //The number of turns in the game
active: Sequelize.BOOLEAN,
voting_message: Sequelize.STRING,
voting_type: Sequelize.INTEGER,
end_time: Sequelize.DATE
},
{
Expand Down Expand Up @@ -311,13 +312,13 @@ GameHistory.belongsTo(Games, { foreignKey: 'game_id' });
ItemInteractions.belongsTo(Games, { foreignKey: 'game_id' });
ItemInteractions.belongsTo(GameItems, { foreignKey: 'item_id' });

UserBadges.hasOne(Badges, { sourceKey: "badge_id", foreignKey: 'id' });
Badges.hasMany(UserBadges, { sourceKey: "id", foreignKey: 'badge_id' })

ThemeItems.belongsTo(Themes, { foreignKey: "theme_id" });

GuildSettings.hasOne(SettingsConfig, { sourceKey: "setting_id", foreignKey: 'id' });
ChannelSettings.hasOne(SettingsConfig, { sourceKey: "setting_id", foreignKey: 'id' });
GameSettings.hasOne(SettingsConfig, { sourceKey: "setting_id", foreignKey: 'id' });
SettingsConfig.hasMany(GuildSettings, { sourceKey: "id", foreignKey: 'setting_id' });
SettingsConfig.hasMany(ChannelSettings, { sourceKey: "id", foreignKey: 'setting_id' });
SettingsConfig.hasMany(GameSettings, { sourceKey: "id", foreignKey: 'setting_id' });

exports.sequelize = sequelize;
exports.Games = Games;
Expand Down
216 changes: 151 additions & 65 deletions giveandtake/giveandtakefunctions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { GameItems, ItemInteractions, GameHistory, Games, UserBadges, sequelize, Badges, Themes, ThemeVotes, ThemeVoteThemes, ThemeItems } = require('../databaseModels.js');
const { Op } = require("sequelize");
const { Channel, EmbedBuilder, messageLink } = require('discord.js');
const { Channel, EmbedBuilder, messageLink, Poll, PollLayoutType, PollAnswer, Guild, PermissionFlagsBits } = require('discord.js');
const { client } = require('../client');
const fs = require('node:fs');
const { GetGameSettingValue, GetChannelSettingValue } = require('./settingsService.js');
Expand All @@ -18,6 +18,15 @@ const itemsForFinalVote = 2;
const themeVotingEnabled = true;
const themesPerVote = 10;

/**
* Possible types of voting at the end of a game
* @enum {number}
*/
const VotingTypes = {
Reaction: 0,
Poll: 1
}

async function MakeMove(guildId, channelId, userId, giveName, takeName) {
const game = await Games.findOne({
where: {
Expand Down Expand Up @@ -462,39 +471,105 @@ async function CheckGameStatus(game) {
const endTime = new Date();
endTime.setHours(endTime.getHours() + 12);

let description = "**Voting ends** <t:" + Math.ceil(endTime.getTime() / 1000) + ":R>\n";
const guild = client.guilds.cache.get(game.guild_id);
if (guild.members.me.permissions.has(PermissionFlagsBits.SendPolls)) {
BuildPoll(game, guild, items, endTime);
}
else {
BuildLegacyPoll(game, items, endTime);
}
}
}

description += "**Options**\n";
/**
*
* @param {Games} game
* @param {Guild} guild
* @param {*} items
* @param {Date} endTime
*/
async function BuildPoll(game, guild, items, endTime) {
const emoteIDRegex = /<a?:.+:(\d+)>/

for (let i = 0; i < items.length; ++i) {
description += reactionEmojis[i] + " - " + (items[i].emoji ? items[i].emoji + " " : "") + items[i].name + ((i + 1) < items.length ? "\n" : "");
let answers = [];

for (let i = 0; i < items.length; ++i) {
/** @type {PollAnswer} */
const answer = {
text: items[i].name
}

const pollEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle("📊 FINAL " + itemsForFinalVote + " - React to vote")
.setDescription(description);
if (emoteID = emoteIDRegex.exec(items[i].emoji)) {
const guildEmoji = guild.emojis.cache?.find(emoji => emoji.id === emoteID);
if (guildEmoji) {
answer.emoji = guildEmoji;
} else {
answer.emoji = reactionEmojis[i % reactionEmojis.length];
}
} else {
answer.emoji = items[i].emoji
}

const channel = client.channels.cache.get(game.channel_id);
answers.push(answer);
}

if (channel) {
let content = "";
const giveAndTakeRoleID = await GetChannelSettingValue("GiveAndTakeRoleID", channel.guildId, channel.id)
if (giveAndTakeRoleID.length) {
content += "<@&" + giveAndTakeRoleID + ">"
const channel = client.channels.cache.get(game.channel_id);

if (channel) {
const message = await channel.send({
/** @type {Poll} */
poll: {
question: { text: "Vote for the winner of " + game.theme_name },
answers: answers,
allowMultiselect: true,
expiresTimestamp: endTime.getTime(),
layoutType: PollLayoutType.Default
}
});

//Switch to the voting stage
game.status = "VOTING";
game.end_time = endTime;
game.voting_message = message.id;
game.voting_type = VotingTypes.Poll;
await game.save();
}
}

const message = await channel.send({ content: content, embeds: [pollEmbed] });
async function BuildLegacyPoll(game, items, endTime) {
let description = "**Voting ends** <t:" + Math.ceil(endTime.getTime() / 1000) + ":R>\n";

//Switch to the voting stage
game.status = "VOTING";
game.end_time = endTime;
game.voting_message = message.id;
await game.save();
description += "**Options**\n";

for (let i = 0; i < items.length; ++i) {
await message.react(reactionEmojis[i]);
}
for (let i = 0; i < items.length; ++i) {
description += reactionEmojis[i] + " - " + (items[i].emoji ? items[i].emoji + " " : "") + items[i].name + ((i + 1) < items.length ? "\n" : "");
}

const pollEmbed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle("📊 FINAL " + itemsForFinalVote + " - React to vote")
.setDescription(description);

const channel = client.channels.cache.get(game.channel_id);

if (channel) {
let content = "";
const giveAndTakeRoleID = await GetChannelSettingValue("GiveAndTakeRoleID", channel.guildId, channel.id)
if (giveAndTakeRoleID.length) {
content += "<@&" + giveAndTakeRoleID + ">"
}

const message = await channel.send({ content: content, embeds: [pollEmbed] });

//Switch to the voting stage
game.status = "VOTING";
game.end_time = endTime;
game.voting_message = message.id;
game.voting_type = VotingTypes.Reaction;
await game.save();

for (let i = 0; i < items.length; ++i) {
await message.react(reactionEmojis[i]);
}
}
}
Expand Down Expand Up @@ -591,52 +666,25 @@ async function CheckGameVoteStatus() {
if (gameItems.length > 0) {
const channel = client.channels.cache.get(game.channel_id);

let messageDeleted;
let voteMessage;
try {
voteMessage = await channel.messages.fetch(game.voting_message);
messageDeleted = voteMessage.deleted;
}
catch {
messageDeleted = true;
}

if (!messageDeleted) {
let reactionCount = [];

for (let i = 0; i < gameItems.length; ++i) {
reactionCount.push(voteMessage.reactions.cache.get(reactionEmojis[i]).count - 1);
if (game.voting_type === VotingTypes.Reaction) {
let messageDeleted;
let voteMessage;
try {
voteMessage = await channel.messages.fetch(game.voting_message);
messageDeleted = voteMessage.deleted;
}

let maxReactionCount = Math.max(...reactionCount);

let winningItems = gameItems.filter((item, i) => reactionCount[i] === maxReactionCount);

let message = "";

if (winningItems.length === 1) {
message = "**" + winningItems[0].name + "** has won the game with " + maxReactionCount + " vote(s)!";
catch {
messageDeleted = true;
}
else if (winningItems.length === gameItems.length) {
message = "The game has ended in a tie with all items receiving " + maxReactionCount + " vote(s)!";

if (!messageDeleted) {
CheckReactionVoteWinner(channel, gameItems);
}
else {
for (let i = 0; i < winningItems.length; ++i) {
if (i !== winningItems.length - 1) {
message += "**" + winningItems[i].name + "**, ";
}
else {
message += "and **" + winningItems[i].name + "** have tied with " + maxReactionCount + " vote(s)!";
}
}
console.log("Final vote message was deleted");
let message = "Final vote message has been deleted, could not determine results. Ending game...";
await channel.send(message);
}

await channel.send(message);
}
else {
console.log("Final vote message was deleted");
let message = "Final vote message has been deleted, could not determine results. Ending game...";
await channel.send(message);
}

game.active = false;
Expand All @@ -651,6 +699,44 @@ async function CheckGameVoteStatus() {
});
}

/**
* Checks the winner of a reaction vote
* @param {Channel} channel
* @param {GameItems[]} gameItems
*/
async function CheckReactionVoteWinner(channel, gameItems) {
let reactionCount = [];

for (let i = 0; i < gameItems.length; ++i) {
reactionCount.push(voteMessage.reactions.cache.get(reactionEmojis[i]).count - 1);
}

let maxReactionCount = Math.max(...reactionCount);

let winningItems = gameItems.filter((item, i) => reactionCount[i] === maxReactionCount);

let message = "";

if (winningItems.length === 1) {
message = "**" + winningItems[0].name + "** has won the game with " + maxReactionCount + " vote(s)!";
}
else if (winningItems.length === gameItems.length) {
message = "The game has ended in a tie with all items receiving " + maxReactionCount + " vote(s)!";
}
else {
for (let i = 0; i < winningItems.length; ++i) {
if (i !== winningItems.length - 1) {
message += "**" + winningItems[i].name + "**, ";
}
else {
message += "and **" + winningItems[i].name + "** have tied with " + maxReactionCount + " vote(s)!";
}
}
}

await channel.send(message);
}

/**
* Starts a vote for the next game theme
* @param {Channel} channel
Expand Down
Loading

0 comments on commit 15395f6

Please sign in to comment.