Compare commits

..

5 commits

25 changed files with 1435 additions and 49 deletions

View file

@ -11,12 +11,23 @@ module.exports = {
async execute(interaction, client) {
try {
// Check if the user has the Manage Roles permission
const isMod = interaction.member.permissions.has(
PermissionsBitField.Flags.ManageRoles
);
const serverName = interaction.guild.name;
const generalCommands = [];
const modCommands = [];
// Categorize commands
client.commands.forEach((command) => {
const commandLine = `/${command.data.name} - ${command.data.description}`;
if (!command.isModOnly) {
generalCommands.push(commandLine);
} else if (isMod) {
modCommands.push(`${commandLine} (Mods only)`);
}
});
const helpEmbed = new EmbedBuilder()
.setColor("#0099ff")
@ -30,39 +41,46 @@ module.exports = {
iconURL: client.user.displayAvatarURL(),
});
// Group commands into general and mod-only
const generalCommands = [];
const modCommands = [];
// Function to split commands into fields under 1024 characters
const addCommandFields = (embed, commands, title) => {
let commandChunk = "";
let chunkCount = 1;
client.commands.forEach((command) => {
const commandLine = `/${command.data.name} - ${command.data.description}`;
if (!command.isModOnly) {
generalCommands.push(commandLine);
} else if (isMod) {
modCommands.push(`${commandLine} (Mods only)`);
}
});
helpEmbed.addFields({
name: `General Commands (${generalCommands.length} available)`,
value:
generalCommands.length > 0
? generalCommands.join("\n")
: "No general commands available.",
inline: false,
});
if (isMod) {
helpEmbed.addFields({
name: `Mod-Only Commands (${modCommands.length} available)`,
value:
modCommands.length > 0
? modCommands.join("\n")
: "No mod-only commands available.",
inline: false,
commands.forEach((command) => {
// Check if adding this command will exceed the 1024 character limit
if ((commandChunk + command).length > 1024) {
// Add current chunk as a new field
embed.addFields({
name: `${title} (Part ${chunkCount})`,
value: commandChunk,
});
commandChunk = ""; // Reset chunk for new field
chunkCount += 1;
}
// Append command to the current chunk
commandChunk += command + "\n";
});
// Add any remaining commands in the last chunk
if (commandChunk) {
embed.addFields({
name: `${title} (Part ${chunkCount})`,
value: commandChunk,
});
}
};
// Add general commands in fields
if (generalCommands.length > 0) {
addCommandFields(helpEmbed, generalCommands, "General Commands");
}
// Add mod-only commands in fields, if user is a mod
if (isMod && modCommands.length > 0) {
addCommandFields(helpEmbed, modCommands, "Mod-Only Commands");
}
// Send the single embed
await interaction.reply({
embeds: [helpEmbed],
});

34
commands/core/invite.js Normal file
View file

@ -0,0 +1,34 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("invite")
.setDescription("Provides an invite link to add the bot to your server."),
async execute(interaction, client) {
try {
const botInviteLink = `https://discord.com/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`;
const inviteEmbed = new EmbedBuilder()
.setColor("#0099ff")
.setTitle("Invite the Bot to Your Server!")
.setDescription(`**[Click here to invite the bot!](${botInviteLink})**`)
.setTimestamp()
.setFooter({
text: `Requested by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({
embeds: [inviteEmbed],
ephemeral: true,
});
} catch (error) {
console.error("Error executing the invite command:", error);
await interaction.reply({
content: "There was an error while executing this command!",
ephemeral: true,
});
}
},
};

View file

@ -0,0 +1,113 @@
const { SlashCommandBuilder } = require("discord.js");
const Event = require("../../models/Event");
const Participant = require("../../models/Participant");
const moment = require("moment");
module.exports = {
data: new SlashCommandBuilder()
.setName("createevent")
.setDescription("Create a new event.")
.addStringOption((option) =>
option
.setName("name")
.setDescription("Name of the event")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("description")
.setDescription("Description of the event")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("category")
.setDescription("Category of the event")
.addChoices(
{ name: "Tournament", value: "tournament" },
{ name: "Meeting", value: "meeting" },
{ name: "Giveaway", value: "giveaway" },
{ name: "Other", value: "other" }
)
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("location")
.setDescription("Location of the event (default is Online)")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("startdatetime")
.setDescription("Start date and time of the event (YYYY-MM-DD HH:mm)")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("enddatetime")
.setDescription("End date and time of the event (YYYY-MM-DD HH:mm)")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("recurrence")
.setDescription("Recurrence of the event")
.addChoices(
{ name: "None", value: "none" },
{ name: "Daily", value: "daily" },
{ name: "Weekly", value: "weekly" },
{ name: "Monthly", value: "monthly" }
)
),
async execute(interaction) {
const name = interaction.options.getString("name");
const description = interaction.options.getString("description");
const category = interaction.options.getString("category");
const location = interaction.options.getString("location") || "Online";
const startDate = moment(
interaction.options.getString("startdatetime"),
"YYYY-MM-DD HH:mm"
);
const endDate = moment(
interaction.options.getString("enddatetime"),
"YYYY-MM-DD HH:mm"
);
if (!startDate.isValid() || !endDate.isValid()) {
return await interaction.reply(
"Invalid date format! Use YYYY-MM-DD HH:mm."
);
}
if (startDate.isAfter(endDate)) {
return await interaction.reply("Start date must be before the end date.");
}
// Create new event
const event = new Event({
name,
description,
category,
location,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
organizerId: interaction.user.id,
});
try {
await event.save();
await interaction.reply(
`Event "${name}" created! Starts at ${startDate.format(
"MMMM Do YYYY, h:mm a"
)} and ends at ${endDate.format("MMMM Do YYYY, h:mm a")}.`
);
} catch (error) {
if (error.code === 11000) {
await interaction.reply("An event with that name already exists.");
} else {
await interaction.reply("Error creating event. Please try again.");
}
}
},
};

View file

@ -0,0 +1,150 @@
// commands/editevent.js
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const Event = require("../../models/Event");
const moment = require("moment");
module.exports = {
data: new SlashCommandBuilder()
.setName("editevent")
.setDescription("Edit an existing event.")
.addStringOption((option) =>
option
.setName("name")
.setDescription("Name of the event to edit")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("description")
.setDescription("New description of the event")
)
.addStringOption((option) =>
option
.setName("category")
.setDescription("New category of the event")
.addChoices(
{ name: "Tournament", value: "tournament" },
{ name: "Meeting", value: "meeting" },
{ name: "Giveaway", value: "giveaway" },
{ name: "Other", value: "other" }
)
)
.addStringOption((option) =>
option.setName("location").setDescription("New location of the event")
)
.addStringOption((option) =>
option
.setName("startdatetime")
.setDescription(
"New start date and time of the event (YYYY-MM-DD HH:mm)"
)
)
.addStringOption((option) =>
option
.setName("enddatetime")
.setDescription("New end date and time of the event (YYYY-MM-DD HH:mm)")
)
.addStringOption((option) =>
option
.setName("recurrence")
.setDescription("New recurrence of the event")
.addChoices(
{ name: "None", value: "none" },
{ name: "Daily", value: "daily" },
{ name: "Weekly", value: "weekly" },
{ name: "Monthly", value: "monthly" }
)
),
async execute(interaction) {
const name = interaction.options.getString("name");
const event = await Event.findOne({ name });
if (!event) {
return await interaction.reply("Event not found!");
}
if (interaction.options.getString("description")) {
event.description = interaction.options.getString("description");
}
if (interaction.options.getString("category")) {
event.category = interaction.options.getString("category");
}
if (interaction.options.getString("location")) {
event.location = interaction.options.getString("location");
}
if (interaction.options.getString("startdatetime")) {
const newStartDate = moment(
interaction.options.getString("startdatetime"),
"YYYY-MM-DD HH:mm"
);
if (!newStartDate.isValid()) {
return await interaction.reply(
"Invalid start date format! Use YYYY-MM-DD HH:mm."
);
}
event.startDate = newStartDate.toISOString();
}
if (interaction.options.getString("enddatetime")) {
const newEndDate = moment(
interaction.options.getString("enddatetime"),
"YYYY-MM-DD HH:mm"
);
if (!newEndDate.isValid()) {
return await interaction.reply(
"Invalid end date format! Use YYYY-MM-DD HH:mm."
);
}
if (moment(event.startDate).isAfter(newEndDate)) {
return await interaction.reply(
"End date must be after the start date."
);
}
event.endDate = newEndDate.toISOString();
}
if (interaction.options.getString("recurrence")) {
event.recurrence = interaction.options.getString("recurrence");
}
await event.save();
const embed = new EmbedBuilder()
.setTitle(`Event "${event.name}" Updated Successfully`)
.setColor("#00FF00")
.addFields(
{
name: "Description",
value: event.description || "No description provided",
inline: true,
},
{
name: "Category",
value: event.category || "Not specified",
inline: true,
},
{
name: "Location",
value: event.location || "Not specified",
inline: true,
},
{
name: "Start Date",
value: moment(event.startDate).format("YYYY-MM-DD HH:mm"),
inline: true,
},
{
name: "End Date",
value: moment(event.endDate).format("YYYY-MM-DD HH:mm"),
inline: true,
},
{ name: "Recurrence", value: event.recurrence || "None", inline: true }
)
.setFooter({
text: "Event updated successfully",
iconURL: interaction.user.displayAvatarURL(),
})
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,92 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const Event = require("../../models/Event");
const Participant = require("../../models/Participant");
const moment = require("moment");
module.exports = {
data: new SlashCommandBuilder()
.setName("joinevent")
.setDescription("Join an event.")
.addStringOption((option) =>
option
.setName("event_name")
.setDescription("Name of the event to join")
.setRequired(true)
),
async execute(interaction) {
const eventName = interaction.options.getString("event_name");
const event = await Event.findOne({ name: eventName });
if (!event) {
return interaction.reply({
content: `Event "${eventName}" not found.`,
ephemeral: false,
});
}
let participant = await Participant.findOne({
userId: interaction.user.id,
});
if (!participant) {
participant = new Participant({
userId: interaction.user.id,
username: interaction.user.username,
});
await participant.save();
}
if (!event.participants.includes(participant._id)) {
event.participants.push(participant._id);
await event.save();
}
const embed = new EmbedBuilder()
.setTitle(`Successfully Joined Event: ${event.name}`)
.setColor("#3498db")
.addFields(
{
name: "Category",
value: event.category ? String(event.category) : "Not specified",
inline: true,
},
{
name: "Location",
value: event.location ? String(event.location) : "Virtual",
inline: true,
},
{
name: "Start Date",
value: moment(event.startDate).isValid()
? moment(event.startDate).format("YYYY-MM-DD HH:mm")
: "N/A",
inline: true,
},
{
name: "End Date",
value: moment(event.endDate).isValid()
? moment(event.endDate).format("YYYY-MM-DD HH:mm")
: "N/A",
inline: true,
},
{
name: "Recurrence",
value: event.recurrence ? String(event.recurrence) : "None",
inline: true,
},
{
name: "Participants",
value: String(event.participants.length),
inline: true,
}
)
.setTimestamp()
.setFooter({
text: `Joined by ${interaction.user.username}`,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,72 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const Event = require("../../models/Event");
const Participant = require("../../models/Participant");
module.exports = {
data: new SlashCommandBuilder()
.setName("leaveevent")
.setDescription("Leave an event.")
.addStringOption((option) =>
option
.setName("event_name")
.setDescription("Name of the event to leave")
.setRequired(true)
),
async execute(interaction) {
const eventName = interaction.options.getString("event_name");
const event = await Event.findOne({ name: eventName });
if (!event) {
return await interaction.reply({
content: `Event "${eventName}" not found.`,
ephemeral: false,
});
}
const participant = await Participant.findOne({
userId: interaction.user.id,
});
if (!participant) {
return await interaction.reply({
content: `You are not a participant of this event.`,
ephemeral: false,
});
}
event.participants.pull(participant._id);
await event.save();
const isInOtherEvents = await Event.exists({
participants: participant._id,
});
if (!isInOtherEvents) {
await Participant.deleteOne({ userId: interaction.user.id });
}
const embed = new EmbedBuilder()
.setTitle(`Left Event: ${event.name}`)
.setColor("#e74c3c")
.addFields(
{
name: "Category",
value: event.category || "Not specified",
inline: true,
},
{ name: "Location", value: event.location || "Virtual", inline: true },
{
name: "Participants Remaining",
value: String(event.participants.length),
inline: true,
}
)
.setFooter({
text: `Left by ${interaction.user.username}`,
iconURL: interaction.user.displayAvatarURL(),
})
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,51 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const Event = require("../../models/Event");
const moment = require("moment");
module.exports = {
data: new SlashCommandBuilder()
.setName("listevents")
.setDescription("List all upcoming events."),
async execute(interaction) {
const { user } = interaction;
const upcomingEvents = await Event.find({ status: "upcoming" }).populate(
"participants"
);
if (upcomingEvents.length === 0) {
return interaction.reply("There are no upcoming events.");
}
const embed = new EmbedBuilder()
.setTitle("Upcoming Events")
.setColor("#00FF00")
.setTimestamp()
.setFooter({
text: `Requested by ${user.username}`,
iconURL: user.displayAvatarURL(),
});
upcomingEvents.forEach((event) => {
const participantsList =
event.participants.map((p) => p.username).join(", ") ||
"No Particiapnts yet";
embed.addFields({
name: event.name,
value: `**Description:** ${event.description}\n**Category:** ${
event.category
}\n**Location:** ${event.location}\n**Start Date:** ${moment(
event.startDate
).format("YYYY-MM-DD HH:mm")}\n**End Date:** ${moment(
event.endDate
).format("YYYY-MM-DD HH:mm")}\n**Recurrence:** ${
event.recurrence
}\n**Participants:** ${participantsList}`,
inline: false,
});
});
await interaction.reply({ embeds: [embed] });
},
};

View file

@ -0,0 +1,12 @@
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("coinflip")
.setDescription("Flip a coin!"),
async execute(interaction) {
const result = Math.random() < 0.5 ? "Heads" : "Tails";
await interaction.reply(`🪙 It's ${result}!`);
},
};

View file

@ -0,0 +1,23 @@
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("roll")
.setDescription("Roll a dice!")
.addIntegerOption((option) =>
option
.setName("sides")
.setDescription("Number of sides on the dice")
.setRequired(false)
.setMinValue(2)
.setMaxValue(100)
),
async execute(interaction) {
const sides = interaction.options.getInteger("sides") || 6;
const result = Math.floor(Math.random() * sides) + 1;
await interaction.reply(
`🎲 You rolled a ${result} on a ${sides}-sided dice!`
);
},
};

View file

@ -0,0 +1,41 @@
const { SlashCommandBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("rps")
.setDescription("Play Rock Paper Scissors!")
.addStringOption((option) =>
option
.setName("choice")
.setDescription("Choose rock, paper, or scissors")
.setRequired(true)
.addChoices(
{ name: "Rock", value: "rock" },
{ name: "Paper", value: "paper" },
{ name: "Scissors", value: "scissors" }
)
),
async execute(interaction) {
const userChoice = interaction.options.getString("choice");
const choices = ["rock", "paper", "scissors"];
const botChoice = choices[Math.floor(Math.random() * choices.length)];
let result;
if (userChoice === botChoice) {
result = "It's a draw!";
} else if (
(userChoice === "rock" && botChoice === "scissors") ||
(userChoice === "paper" && botChoice === "rock") ||
(userChoice === "scissors" && botChoice === "paper")
) {
result = "You win!";
} else {
result = "You lose!";
}
await interaction.reply(
`You chose ${userChoice}. I chose ${botChoice}. ${result}`
);
},
};

401
commands/games/spyfall.js Normal file
View file

@ -0,0 +1,401 @@
const {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
} = require("discord.js");
const SpyfallGame = require("../../models/SpyfallGame");
const SpyfallLocation = require("../../models/SpyfallLocation");
const { v4: uuidv4 } = require("uuid");
module.exports = {
data: new SlashCommandBuilder()
.setName("spyfall")
.setDescription("Start a game of Spyfall."),
async execute(interaction) {
const guildId = interaction.guild.id;
const gameId = uuidv4();
const existingGame = await SpyfallGame.findOne({ gameId });
if (existingGame && existingGame.status === "ongoing") {
return interaction.reply(
"A game is already in progress! Please finish the current game first."
);
}
try {
const locations = await SpyfallLocation.find({});
if (!locations || locations.length === 0) {
return interaction.reply("No locations found. Please try again later.");
}
const selectedLocation =
locations[Math.floor(Math.random() * locations.length)].name;
const players = new Set();
const joinEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Join the Spyfall Game!")
.setDescription("Click the button below to join the game.")
.addFields({ name: "Current Players:", value: "None yet." })
.setFooter({ text: "You need at least 2 players to start!" })
.setTimestamp();
const joinButton = new ButtonBuilder()
.setCustomId("join_game")
.setLabel("Join Game")
.setStyle(ButtonStyle.Success);
const leaveButton = new ButtonBuilder()
.setCustomId("leave_game")
.setLabel("Leave Game")
.setStyle(ButtonStyle.Danger);
const startButton = new ButtonBuilder()
.setCustomId("start_game")
.setLabel("Start Game")
.setStyle(ButtonStyle.Primary)
.setDisabled(true);
const buttonRow = new ActionRowBuilder().addComponents(
joinButton,
leaveButton,
startButton
);
const joinMessage = await interaction.reply({
embeds: [joinEmbed],
components: [buttonRow],
fetchReply: true,
});
const filter = (i) =>
i.customId === "join_game" ||
i.customId === "leave_game" ||
i.customId === "start_game";
const collector = joinMessage.createMessageComponentCollector({ filter });
collector.on("collect", async (i) => {
try {
if (!players.has(i.user.id) && i.customId !== "join_game") {
return i.reply({
content: "You cannot interact with this button.",
ephemeral: true,
});
}
if (i.customId === "join_game") {
if (players.has(i.user.id)) {
return i.reply({
content: "You are already in the game!",
ephemeral: true,
});
}
players.add(i.user.id);
const currentPlayers =
Array.from(players)
.map(
(id) => interaction.guild.members.cache.get(id).user.username
)
.join(", ") || "None yet.";
joinEmbed
.setDescription("Click the button below to join the game.")
.setFields([{ name: "Current Players:", value: currentPlayers }]);
if (players.size >= 2) {
startButton.setDisabled(false);
}
await i.update({ embeds: [joinEmbed], components: [buttonRow] });
} else if (i.customId === "leave_game") {
if (players.has(i.user.id)) {
players.delete(i.user.id);
const currentPlayers =
Array.from(players)
.map(
(id) =>
interaction.guild.members.cache.get(id).user.username
)
.join(", ") || "None yet.";
joinEmbed
.setDescription("Click the button below to join the game.")
.setFields([
{ name: "Current Players:", value: currentPlayers },
]);
await i.update({ embeds: [joinEmbed], components: [buttonRow] });
} else {
await i.reply({
content: "You are not part of the game.",
ephemeral: true,
});
}
} else if (i.customId === "start_game") {
if (players.size < 2) {
return i.reply({
content: "You need at least 2 players to start the game.",
ephemeral: true,
});
}
const spyIndex = Math.floor(Math.random() * players.size);
const spy = Array.from(players)[spyIndex];
const newGame = new SpyfallGame({
gameId,
guildId,
location: selectedLocation,
spy,
players: Array.from(players),
status: "ongoing",
});
await newGame.save();
const locationMessage = `The game has started! The location is **${selectedLocation}**.`;
for (const playerId of players) {
const player = await interaction.guild.members.cache.get(playerId)
.user;
if (playerId !== spy) {
await player.send(locationMessage);
} else {
await player.send(
"The game has started! You are the spy! Try to blend in!"
);
}
}
const embed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Spyfall Game Started!")
.setDescription("The game has started! One of you is the spy!")
.setFooter({
text: "The spy must figure out the location without revealing themselves!",
})
.setTimestamp();
await interaction.followUp({ embeds: [embed], components: [] });
collector.stop();
await interaction.channel.send({
content: "The game has started! Use `/stopspyfall` to end it.",
});
await startQuestioningPhase(
interaction,
spy,
players,
selectedLocation,
gameId
);
return;
}
} catch (error) {
console.error("Error during button interaction:", error);
await i.reply({
content:
"There was an error processing your request. Please try again later.",
ephemeral: true,
});
}
});
} catch (error) {
console.error("Error fetching locations or starting the game:", error);
await interaction.reply(
"There was an error fetching locations or starting the game. Please try again later."
);
}
},
};
async function startQuestioningPhase(
interaction,
spy,
players,
selectedLocation,
gameId
) {
const ongoingGame = await SpyfallGame.findOne({ gameId });
if (!ongoingGame || ongoingGame.status !== "ongoing") {
return;
}
const playerArray = Array.from(players);
let currentIndex = 0;
async function askQuestion() {
if (currentIndex >= playerArray.length) {
return startVotingPhase(interaction, spy, playerArray, gameId);
}
const currentPlayerId = playerArray[currentIndex];
const currentPlayer =
interaction.guild.members.cache.get(currentPlayerId).user;
const questionEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Your Turn to Ask a Question!")
.setDescription(
`It's **${currentPlayer.username}**'s turn to ask a question about the location.`
)
.setFooter({ text: "Click 'Finish Turn' when you're done." });
const finishButton = new ButtonBuilder()
.setCustomId(`finish_turn_${currentPlayerId}`)
.setLabel("Finish Turn")
.setStyle(ButtonStyle.Primary);
const buttonRow = new ActionRowBuilder().addComponents(finishButton);
await interaction.channel.send({
embeds: [questionEmbed],
components: [buttonRow],
});
const filter = (i) =>
i.customId === `finish_turn_${currentPlayerId}` &&
i.user.id === currentPlayerId;
const collector = interaction.channel.createMessageComponentCollector({
filter,
time: 30000,
});
collector.on("collect", async (i) => {
await i.reply({ content: "Your turn has ended!", ephemeral: true });
collector.stop();
});
collector.on("end", async () => {
currentIndex++;
await askQuestion();
});
}
askQuestion();
}
async function startVotingPhase(
interaction,
spy,
players,
gameId,
selectedLocation
) {
// Ensure players is an array
if (!Array.isArray(players)) {
console.error("Players is not an array:", players);
return;
}
const ongoingGame = await SpyfallGame.findOne({ gameId });
if (!ongoingGame || ongoingGame.status !== "ongoing") {
return;
}
const votes = new Map();
const voteEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Voting Phase")
.setDescription(
"Vote for who you think the spy is! (You cannot vote for yourself)"
)
.setFooter({ text: "Click the buttons below to vote." });
const voteButtons = players.map((playerId) =>
new ButtonBuilder()
.setCustomId(`vote_${playerId}`)
.setLabel(interaction.guild.members.cache.get(playerId).user.username)
.setStyle(ButtonStyle.Primary)
);
const voteRow = new ActionRowBuilder().addComponents(voteButtons);
const voteMessage = await interaction.channel.send({
embeds: [voteEmbed],
components: [voteRow],
});
const voteFilter = (i) =>
i.customId.startsWith("vote_") && players.includes(i.user.id);
const voteCollector = voteMessage.createMessageComponentCollector({
filter: voteFilter,
});
voteCollector.on("collect", async (i) => {
const votedPlayerId = i.customId.split("_")[1];
if (i.user.id === votedPlayerId) {
await i.reply({
content: "You cannot vote for yourself.",
ephemeral: true,
});
return;
}
// Add the vote
votes.set(votedPlayerId, (votes.get(votedPlayerId) || 0) + 1);
await i.reply({
content: `You voted for ${
interaction.guild.members.cache.get(votedPlayerId).user.username
}.`,
ephemeral: true,
});
if (votes.size === players.length) {
voteCollector.stop();
await revealSpy(interaction, spy, votes, selectedLocation, gameId);
}
});
voteCollector.on("end", async () => {
if (votes.size > 0) {
await revealSpy(interaction, spy, votes, selectedLocation, gameId);
} else {
await interaction.channel.send("Voting timed out! No votes were cast.");
}
});
}
async function revealSpy(interaction, spy, votes, selectedLocation, gameId) {
const voteEntries = Array.from(votes.entries());
const highestVote = Math.max(...voteEntries.map(([_, v]) => v));
const suspectedSpy = voteEntries.find(([_, v]) => v === highestVote)[0];
const resultEmbed = new EmbedBuilder()
.setColor("#ffcc00")
.setTitle("Voting Results")
.addFields(
{
name: "Suspected Spy:",
value: interaction.guild.members.cache.get(suspectedSpy).user.username,
},
{
name: "Actual Spy:",
value: interaction.guild.members.cache.get(spy).user.username,
}
)
.setFooter({
text:
suspectedSpy === spy
? "The spy has been caught!"
: "The spy has escaped!",
})
.setTimestamp();
const result = await SpyfallGame.updateOne({ gameId }, { status: "ended" });
if (result.modifiedCount === 0) {
console.log("No game found or status was already 'ended'.");
}
await interaction.channel.send({ embeds: [resultEmbed] });
}

View file

@ -0,0 +1,38 @@
const { SlashCommandBuilder } = require("discord.js");
const SpyfallGame = require("../../models/SpyfallGame");
module.exports = {
data: new SlashCommandBuilder()
.setName("stopspyfall")
.setDescription("Stop the current Spyfall game in this server."),
async execute(interaction) {
try {
const guildId = interaction.guild.id;
const ongoingGame = await SpyfallGame.findOne({
guildId: guildId,
status: "ongoing",
});
if (!ongoingGame) {
return interaction.reply({
content: "No Spyfall game is currently in progress in this server!",
ephemeral: true,
});
}
ongoingGame.status = "ended";
await ongoingGame.save();
await interaction.reply("The Spyfall game has been stopped!");
} catch (error) {
console.error("Error stopping the game:", error);
await interaction.reply({
content:
"There was an error trying to stop the game. Please try again later.",
ephemeral: true,
});
}
},
};

View file

@ -0,0 +1,61 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
const axios = require("axios");
module.exports = {
data: new SlashCommandBuilder()
.setName("thisdayinhistory")
.setDescription("Shows historical events that happened on this day."),
async execute(interaction) {
try {
const today = new Date();
const month = today.getMonth() + 1;
const day = today.getDate();
const response = await axios.get(
`https://en.wikipedia.org/api/rest_v1/feed/onthisday/events/${month}/${day}`
);
if (response.data.events.length === 0) {
return interaction.reply("No significant events found for today.");
}
const events = response.data.events
.map((event) => `${event.year}: ${event.text}`)
.join("\n");
const maxDescriptionLength = 4096;
const truncatedEvents =
events.length > maxDescriptionLength
? events.slice(0, maxDescriptionLength - 3) + "..."
: events;
const historyEmbed = new EmbedBuilder()
.setColor("#0099ff")
.setTitle(`This Day in History: ${month}/${day}`)
.setDescription(truncatedEvents)
.setTimestamp()
.setFooter({
text: `Requested by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({ embeds: [historyEmbed] });
} catch (error) {
console.error("Error executing the thisdayinhistory command:", error);
if (error.response) {
await interaction.reply({
content: `API returned an error: ${error.response.status} - ${error.response.data.title}`,
ephemeral: true,
});
} else {
await interaction.reply({
content:
"There was an error while executing this command! Please try again later.",
ephemeral: true,
});
}
}
},
};

View file

@ -0,0 +1,64 @@
const {
SlashCommandBuilder,
EmbedBuilder,
PermissionsBitField,
} = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("servers")
.setDescription("Displays a list of servers the bot is currently in"),
isModOnly: true,
async execute(interaction) {
try {
// Check if the user has the Manage Server permission
if (
!interaction.member.permissions.has(
PermissionsBitField.Flags.ManageGuild
)
) {
await interaction.reply({
content: "You do not have permission to use this command!",
ephemeral: false,
});
return;
}
const guilds = interaction.client.guilds.cache.map((guild) => ({
name: guild.name,
memberCount: guild.memberCount,
id: guild.id,
}));
const serversEmbed = new EmbedBuilder()
.setColor("#0099ff")
.setTitle("Servers the Bot is In")
.setDescription(`Currently in ${guilds.length} servers`)
.setTimestamp()
.setFooter({
text: `Requested by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
guilds.forEach((guild) => {
serversEmbed.addFields({
name: guild.name,
value: `ID: ${guild.id}\nMembers: ${guild.memberCount}`,
inline: true,
});
});
await interaction.reply({
embeds: [serversEmbed],
ephemeral: false,
});
} catch (error) {
console.error("Error executing servers command:", error);
await interaction.reply({
content: "There was an error while executing this command!",
ephemeral: false,
});
}
},
};

View file

@ -1,4 +1,5 @@
const { SlashCommandBuilder } = require("discord.js");
const ServerSettings = require("../../models/ServerSettings");
module.exports = {
data: new SlashCommandBuilder()
@ -16,11 +17,23 @@ module.exports = {
),
async execute(interaction) {
// Check if the command is used in the allowed channel
const allowedChannelId = "1299134735330836521";
if (interaction.channelId !== allowedChannelId) {
const serverSettings = await ServerSettings.findOne({
guildId: interaction.guild.id,
});
if (!serverSettings) {
return interaction.reply({
content: `This command can only be used in the designated channel.`,
content:
"Server settings are not configured. Please run the setup command.",
ephemeral: true,
});
}
const actionItemChannelId = serverSettings.actionItemsChannelId;
if (interaction.channelId !== actionItemChannelId) {
return interaction.reply({
content: `This command can only be used in the <#${actionItemChannelId}> channel.`,
ephemeral: true,
});
}

View file

@ -34,6 +34,14 @@ module.exports = {
.setName("emaildomains")
.setDescription("Comma-separated list of allowed email domains.")
.setRequired(true)
)
.addChannelOption((option) =>
option
.setName("actionitemschannel")
.setDescription(
"Select the allowed channel for action items. (Optional)"
)
.setRequired(false)
),
async execute(interaction) {
@ -55,7 +63,10 @@ module.exports = {
const verifiedRole = interaction.options.getRole("verifiedrole");
const emailDomains = interaction.options
.getString("emaildomains")
.split(",");
.split(",")
.map((domain) => domain.trim());
const actionitemschannel =
interaction.options.getChannel("actionitemschannel");
try {
// Store the channel IDs instead of names
@ -63,11 +74,14 @@ module.exports = {
{ guildId: interaction.guild.id },
{
guildId: interaction.guild.id,
logChannelId: logChannel.id, // Store log channel ID
logChannelId: logChannel.id,
verifiedRoleName: verifiedRole.name,
verificationChannelId: verificationChannel.id, // Store verification channel ID
generalChannelId: generalChannel.id, // Store general channel ID
verificationChannelId: verificationChannel.id,
generalChannelId: generalChannel.id,
emailDomains: emailDomains,
actionItemsChannelId: actionitemschannel
? actionitemschannel.id
: null,
},
{ upsert: true, new: true }
);
@ -78,7 +92,10 @@ module.exports = {
**General Channel**: <#${generalChannel.id}>\n
**Verification Channel**: <#${verificationChannel.id}>\n
**Verified Role**: ${verifiedRole.name}\n
**Allowed Email Domains**: ${emailDomains.join(", ")}`,
**Allowed Email Domains**: ${emailDomains.join(", ")}\n
**Action Item Channel**: ${
actionitemschannel ? `<#${actionitemschannel.id}>` : "None"
}`,
ephemeral: true,
});
} catch (error) {

63
commands/utils/stats.js Normal file
View file

@ -0,0 +1,63 @@
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
module.exports = {
data: new SlashCommandBuilder()
.setName("stats")
.setDescription("Displays server statistics."),
async execute(interaction) {
try {
const totalMembers = interaction.guild.memberCount;
const onlineMembers = interaction.guild.members.cache.filter(
(member) => member.presence?.status !== "offline"
).size;
const offlineMembers = totalMembers - onlineMembers;
const humanMembers = interaction.guild.members.cache.filter(
(member) => !member.user.bot
).size;
const botMembers = totalMembers - humanMembers;
const statsEmbed = new EmbedBuilder()
.setColor("#0099ff")
.setTitle("Server Statistics")
.addFields(
{
name: "Total Members",
value: totalMembers.toString(),
inline: true,
},
{
name: "Online Members",
value: onlineMembers.toString(),
inline: true,
},
{
name: "Offline Members",
value: offlineMembers.toString(),
inline: true,
},
{
name: "Human Members",
value: humanMembers.toString(),
inline: true,
},
{ name: "Bot Members", value: botMembers.toString(), inline: true }
)
.setTimestamp()
.setFooter({
text: `Requested by ${interaction.user.tag}`,
iconURL: interaction.user.displayAvatarURL(),
});
await interaction.reply({ embeds: [statsEmbed] });
} catch (error) {
console.error("Error executing the stats command:", error);
await interaction.reply({
content: "There was an error while executing this command!",
ephemeral: true,
});
}
},
};

View file

@ -5,12 +5,14 @@ const {
Collection,
REST,
Routes,
PresenceUpdateStatus,
} = require("discord.js");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const ServerSettings = require("./models/ServerSettings");
const seedShopItems = require("./utils/seedShopItems");
const seedSpyfallLocations = require("./utils/seedSpyfallLocations");
const client = new Client({
intents: [
@ -65,19 +67,18 @@ client.once("ready", async () => {
// Register commands for all existing guilds
const guilds = client.guilds.cache.map((guild) => guild.id);
// Seed the shop items
for (const guildId of guilds) {
await seedShopItems(guildId); // Pass guildId to seedShopItems
}
for (const guildId of guilds) {
await registerCommands(guildId);
}
await Promise.all(
guilds.map(async (guildId) => {
await seedShopItems(guildId);
await seedSpyfallLocations(guildId);
await registerCommands(guildId);
})
);
// Set bot status and activity
client.user.setPresence({
activities: [{ name: "Degenerate Gamers!", type: 3 }],
status: "online",
status: PresenceUpdateStatus.Online,
});
console.log(`\n==============================\n`);
@ -93,6 +94,9 @@ client.on("guildCreate", async (guild) => {
// seed items for new guild with guildId
await seedShopItems(guild.id);
// Seed spyfall locations for the new guild
await seedSpyfallLocations(guild.id);
// Register slash commands for the new guild
await registerCommands(guild.id);
} catch (error) {

30
models/Event.js Normal file
View file

@ -0,0 +1,30 @@
const mongoose = require("mongoose");
const Participant = require("./Participant");
const eventSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
description: { type: String, required: true },
category: {
type: String,
enum: ["tournamets", "meeting", "giveaway", "other"],
default: "other",
},
location: { type: String, default: "Online" },
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
organizerId: { type: String, required: true },
participants: [{ type: mongoose.Schema.Types.ObjectId, ref: "Participant" }],
recurrence: {
type: String,
enum: ["none", "daily", "weekly", "monthly"],
default: "none",
},
status: {
type: String,
enum: ["upcoming", "completed", "cancelled"],
default: "upcoming",
},
});
module.exports = mongoose.model("Event", eventSchema);

8
models/Participant.js Normal file
View file

@ -0,0 +1,8 @@
const mongoose = require("mongoose");
const participantSchema = new mongoose.Schema({
userId: { type: String, required: true },
username: { type: String, required: true },
});
module.exports = mongoose.model("Participant", participantSchema);

View file

@ -7,7 +7,9 @@ const ServerSettingsSchema = new mongoose.Schema({
verificationChannelId: { type: String, required: false },
generalChannelId: { type: String, required: false },
emailDomains: { type: [String], required: false },
actionItemsChannelId: { type: String, required: false },
});
const ServerSettings = mongoose.model("ServerSettings", ServerSettingsSchema);
module.exports = ServerSettings;

18
models/SpyfallGame.js Normal file
View file

@ -0,0 +1,18 @@
const mongoose = require("mongoose");
const { v4: uuidv4 } = require("uuid");
const gameStatusEnum = {
values: ["ongoing", "ended"],
message: 'Status must be either "ongoing" or "ended"',
};
const spyfallGameSchema = new mongoose.Schema({
gameId: { type: String, required: true, unique: true, default: uuidv4 },
guildId: { type: String, required: true },
location: { type: String, required: true },
spy: { type: String, required: true },
players: { type: [String], required: true },
status: { type: String, enum: gameStatusEnum, default: "ongoing" },
});
module.exports = mongoose.model("SpyfallGame", spyfallGameSchema);

View file

@ -0,0 +1,7 @@
const mongoose = require("mongoose");
const spyfallLocationSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
});
module.exports = mongoose.model("SpyfallLocation", spyfallLocationSchema);

View file

@ -16,9 +16,11 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"html-entities": "^2.5.2",
"moment": "^2.30.1",
"mongoose": "^8.6.0",
"nodemailer": "^6.9.14",
"owoify-js": "^2.0.0",
"puppeteer": "^23.4.1"
"puppeteer": "^23.4.1",
"uuid": "^11.0.0"
}
}

View file

@ -0,0 +1,52 @@
const SpyfallLocation = require("../models/SpyfallLocation");
async function seedSpyfallLocations(guildId) {
const locations = [
{ name: "Beach" },
{ name: "Casino" },
{ name: "Circus" },
{ name: "Cruise Ship" },
{ name: "Hospital" },
{ name: "Hotel" },
{ name: "Military Base" },
{ name: "Movie Studio" },
{ name: "Pirate Ship" },
{ name: "Polar Station" },
{ name: "Police Station" },
{ name: "Restaurant" },
{ name: "School" },
{ name: "Space Station" },
{ name: "Submarine" },
{ name: "Supermarket" },
{ name: "Theater" },
{ name: "University" },
{ name: "Zoo" },
{ name: "Airplane" },
{ name: "Bank" },
{ name: "Cathedral" },
{ name: "Corporate Party" },
{ name: "Crusader Army" },
{ name: "Day Spa" },
{ name: "Embassy" },
{ name: "Jail" },
{ name: "Museum" },
{ name: "Passenger Train" },
{ name: "Service Station" },
{ name: "Space Station" },
{ name: "Subway" },
{ name: "The U.N." },
{ name: "World Cup Final" },
];
for (const location of locations) {
await SpyfallLocation.updateOne(
{ name: location.name },
{ $set: location },
{ upsert: true }
);
}
console.log(`✅ Spyfall Locations seeded for guild: ${guildId}`);
}
module.exports = seedSpyfallLocations;