Skip to content

Instantly share code, notes, and snippets.

@RH2
Created April 13, 2025 07:19
Show Gist options
  • Save RH2/6eb2048b5f4f0f940e72b8eb5e000503 to your computer and use it in GitHub Desktop.
Save RH2/6eb2048b5f4f0f940e72b8eb5e000503 to your computer and use it in GitHub Desktop.
smcman _with polling
// Load environment variables
require('dotenv').config();
const { Client, GatewayIntentBits, Partials, PermissionsBitField, EmbedBuilder, AttachmentBuilder } = require('discord.js');
const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');
const createCsvWriter = require('csv-writer').createObjectCsvWriter;
// Create a new Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildScheduledEvents,
GatewayIntentBits.GuildMessageReactions,
],
partials: [Partials.Channel, Partials.Message, Partials.Reaction],
});
// Fixed channel IDs
const CHALLENGE_CHANNEL_ID = '1360730337998668031';
const GALLERY_CHANNEL_ID = '1360730371037331526';
const STATS_CHANNEL_ID = '1360869138154786978';
// CSV file path
const USER_STATS_FILE = path.join(__dirname, 'user_stats.csv');
// User stats storage
const userStats = new Map();
// Competition state storage
const competitions = new Map();
// Goading messages for participants
const joinMessages = [
"Oh look who decided to join! Think you can handle the pressure?",
"Welcome to the arena! Hope your creative muscles are warmed up!",
"Another challenger appears! Let's see what you've got!",
"Bold of you to join! The clock is ticking, better bring your A-game!",
"Brave of you to step up! May your skills be faster than the timer!",
"Look who's feeling confident today! Can you back it up with skill?",
"Fresh meat for the competition! Show us what you're made of!"
];
const leaveMessages = [
"Giving up already? I thought you had more fight in you!",
"Running away with your tail between your legs? The competition will miss your... 'talent'.",
"Another one bites the dust! The pressure got to you, huh?",
"Retreating to safety? Smart move if you can't handle the heat!",
"Backing out? Probably for the best - the others were looking too strong anyway!",
"Abandoning ship! We'll pour one out for your lost potential.",
"Taking the easy way out, eh? See you in the next competition... maybe!"
];
// Initialize user stats
async function loadUserStats() {
try {
const statsChannel = await client.channels.fetch(STATS_CHANNEL_ID);
const messages = await statsChannel.messages.fetch({ limit: 50 });
const statsMessage = messages.find(m =>
m.author.id === client.user.id &&
m.content.startsWith('```csv\nUSER STATS DATA')
);
if (statsMessage) {
// Extract CSV data from message
const csvData = statsMessage.content
.replace(/```csv\nUSER STATS DATA - Last Updated: .+\n/, '')
.replace(/\n```$/, '');
// Parse CSV
const rows = csvData.split('\n');
const header = rows.shift(); // Skip header
for (const row of rows) {
if (!row.trim()) continue;
const [username, userId, totalWords, messages, participations, wins] = row.split(',');
userStats.set(userId, {
username,
userId,
totalWords: parseInt(totalWords) || 0,
messages: parseInt(messages) || 0,
participations: parseInt(participations) || 0,
wins: parseInt(wins) || 0
});
}
console.log('User stats loaded from Discord message');
}
} catch (error) {
console.error('Error loading user stats from Discord:', error);
}
}
// Load user stats from CSV
function loadUserStats() {
fs.createReadStream(USER_STATS_FILE)
.pipe(csv())
.on('data', (data) => {
userStats.set(data.userId, {
username: data.username,
userId: data.userId,
totalWords: parseInt(data.totalWords) || 0,
messages: parseInt(data.messages) || 0,
participations: parseInt(data.participations) || 0,
wins: parseInt(data.wins) || 0
});
})
.on('end', () => {
console.log('User stats loaded');
});
}
async function saveUserStats() {
try {
// Convert user stats to CSV format
const csvHeader = 'Username,UserID,TotalWords,Messages,Participations,Wins\n';
const csvRows = Array.from(userStats.values())
.map(user => `${user.username},${user.userId},${user.totalWords},${user.messages},${user.participations},${user.wins}`)
.join('\n');
const csvContent = csvHeader + csvRows;
// Get the stats channel
const statsChannel = await client.channels.fetch(STATS_CHANNEL_ID);
if (!statsChannel) throw new Error('Stats channel not found');
// Find the last bot message with stats (to update it)
const messages = await statsChannel.messages.fetch({ limit: 50 });
const lastStatsMessage = messages.find(m =>
m.author.id === client.user.id &&
m.content.startsWith('```csv\nUSER STATS DATA')
);
// Create or update the stats message
const messageContent = `\`\`\`csv\nUSER STATS DATA - Last Updated: ${new Date().toISOString()}\n${csvContent}\n\`\`\``;
if (lastStatsMessage) {
await lastStatsMessage.edit(messageContent);
console.log('Updated user stats message in Discord');
} else {
await statsChannel.send(messageContent);
console.log('Created new user stats message in Discord');
}
} catch (error) {
console.error('Error saving user stats to Discord:', error);
}
}
// Get or create user stats
function getUserStats(userId, username) {
if (!userStats.has(userId)) {
userStats.set(userId, {
username: username,
userId: userId,
totalWords: 0,
messages: 0,
participations: 0,
wins: 0
});
} else {
// Update username in case it changed
const stats = userStats.get(userId);
stats.username = username;
}
return userStats.get(userId);
}
// Competition class to manage each competition
class Competition {
constructor(channelId, initiator) {
this.channelId = channelId;
this.initiator = initiator;
this.participants = new Set();
this.topic = 'Drawing Challenge';
this.timeLimit = 30 * 60 * 1000; // Default: 30 minutes
this.startTime = null;
this.timer = null;
this.submissions = new Map();
this.active = false;
this.reminders = [];
this.guildEvent = null;
this.participantRoleName = 'Speed Competitor';
this.participantRole = null;
this.pollMessage = null;
this.hasEnded = false;
}
start() {
this.startTime = Date.now();
this.active = true;
// Set reminders at various intervals
this.setReminders();
// Set the main timer
this.timer = setTimeout(() => this.end(), this.timeLimit);
// Create server event
this.createServerEvent();
// Update participant stats
for (const participantId of this.participants) {
try {
const user = client.users.cache.get(participantId);
if (user) {
const stats = getUserStats(participantId, user.username);
stats.participations++;
}
} catch (error) {
console.error(`Error updating stats for participant ${participantId}:`, error);
}
}
// Save updated stats
saveUserStats();
}
async createServerEvent() {
try {
const channel = await client.channels.fetch(this.channelId);
const guild = channel.guild;
const startTime = new Date();
const endTime = new Date(startTime.getTime() + this.timeLimit);
// Create the scheduled event
const event = await guild.scheduledEvents.create({
name: `Speed Competition: ${this.topic}`,
description: `A timed art competition on the topic: ${this.topic}. Use !in to join and !upload to submit your work.`,
scheduledStartTime: startTime,
scheduledEndTime: endTime,
privacyLevel: 2, // GUILD_ONLY
entityType: 3, // EXTERNAL
entityMetadata: {
location: `#${channel.name}`
}
});
this.guildEvent = event;
channel.send(`πŸ“… Server event created! Check the events tab to see the competition schedule.`);
} catch (error) {
console.error('Error creating server event:', error);
// Continue even if event creation fails
}
}
async createParticipantRole(guild) {
if (this.participantRole) return this.participantRole;
// Check if role already exists
let role = guild.roles.cache.find(r => r.name === this.participantRoleName);
if (!role) {
// Create a new role
role = await guild.roles.create({
name: this.participantRoleName,
color: '#FF5733',
hoist: true, // Shows members with this role separately
reason: 'Role for speed competition participants'
});
}
this.participantRole = role;
return role;
}
async addParticipantRole(member) {
try {
const role = await this.createParticipantRole(member.guild);
await member.roles.add(role);
} catch (error) {
console.error('Error adding participant role:', error);
}
}
async removeParticipantRole(member) {
try {
if (this.participantRole) {
await member.roles.remove(this.participantRole);
}
} catch (error) {
console.error('Error removing participant role:', error);
}
}
setReminders() {
// Clear any existing reminders
this.clearReminders();
// Remaining time reminders
const reminderTimes = [
{ time: this.timeLimit * 0.5, message: 'Halfway there! 50% of time remaining.' },
{ time: this.timeLimit * 0.75, message: '25% of time remaining!' },
{ time: this.timeLimit * 0.9, message: '10% of time remaining! Finish up soon!' },
{ time: this.timeLimit - (60 * 1000), message: '1 minute remaining!' },
{ time: this.timeLimit - (30 * 1000), message: '30 seconds remaining!' },
{ time: this.timeLimit - (10 * 1000), message: '10 seconds remaining!' }
];
// Set all reminders
for (const reminder of reminderTimes) {
const timeToReminder = reminder.time;
if (timeToReminder > 0) {
const timerId = setTimeout(() => {
this.sendMessage(reminder.message);
}, timeToReminder);
this.reminders.push(timerId);
}
}
}
clearReminders() {
for (const timerId of this.reminders) {
clearTimeout(timerId);
}
this.reminders = [];
}
async end() {
if (this.hasEnded) return;
this.hasEnded = true;
this.active = false;
clearTimeout(this.timer);
this.clearReminders();
const channel = await client.channels.fetch(this.channelId);
channel.send(`🏁 The competition has ended! Final submissions are now locked.`);
// End the server event if it exists
if (this.guildEvent) {
try {
await this.guildEvent.setStatus(4); // COMPLETED
} catch (error) {
console.error('Error ending server event:', error);
}
}
// Remove participant roles
const guild = channel.guild;
for (const participantId of this.participants) {
try {
const member = await guild.members.fetch(participantId);
await this.removeParticipantRole(member);
} catch (error) {
console.error(`Error removing role from user ${participantId}:`, error);
}
}
// Create competition summary
setTimeout(() => this.createSummary(), 2000);
}
async createSummary() {
const channel = await client.channels.fetch(this.channelId);
if (this.submissions.size === 0) {
channel.send('No submissions were received during this competition.');
return;
}
// Create results embed
const resultsEmbed = new EmbedBuilder()
.setTitle(`Speed Competition Results: ${this.topic}`)
.setDescription(`Competition has completed! Here's a summary:`)
.setColor('#FF5733')
.addFields(
{ name: 'Participants', value: `${this.participants.size} participants` },
{ name: 'Submissions', value: `${this.submissions.size} submissions` },
{ name: 'Time Limit', value: `${this.timeLimit / 60000} minutes` }
)
.setTimestamp();
await channel.send({ embeds: [resultsEmbed] });
// Post summary to gallery channel using the specific Channel ID
try {
// Use the specific gallery channel ID instead of searching by name
const galleryChannel = await client.channels.fetch(GALLERY_CHANNEL_ID);
if (!galleryChannel) {
throw new Error('Gallery channel not found');
}
// Post summary to gallery channel
await galleryChannel.send(`# Speed Competition Results: ${this.topic}`);
await galleryChannel.send({ embeds: [resultsEmbed] });
// Create poll to vote for winner if there are at least 2 submissions
if (this.submissions.size >= 2) {
await this.createVotingPoll(galleryChannel);
}
// Post all submissions
for (const [userId, submission] of this.submissions.entries()) {
try {
const user = await client.users.fetch(userId);
const submissionEmbed = new EmbedBuilder()
.setTitle(`Submission by ${user.username}`)
.setImage(submission.url)
.setColor('#3498DB');
await galleryChannel.send({ embeds: [submissionEmbed] });
// Add a small delay between messages to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error(`Error posting submission for user ${userId}:`, error);
continue; // Continue with other submissions even if one fails
}
}
// Send confirmation in the original channel
channel.send(`βœ… All results have been posted to <#${GALLERY_CHANNEL_ID}>`);
} catch (error) {
console.error('Error posting to gallery channel:', error);
channel.send(`Error posting to gallery channel: ${error.message}. Please check bot permissions.`);
}
}
async createVotingPoll(galleryChannel) {
try {
// Create voting embed
const votingEmbed = new EmbedBuilder()
.setTitle(`Vote for the Winner: ${this.topic}`)
.setDescription(`React to vote for your favorite submission!\nVoting will be open for 24 hours.`)
.setColor('#9B59B6')
.addFields(
{ name: 'How to Vote', value: 'React with the number corresponding to the submission you want to vote for. One vote per person.' }
);
// Add numbered entries for each submission
let index = 1;
const reactionNumbers = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', 'πŸ”Ÿ'];
const submissionIds = Array.from(this.submissions.keys());
const submissionDesc = [];
for (const userId of submissionIds) {
if (index > reactionNumbers.length) break; // Only support up to 10 submissions for now
try {
const user = await client.users.fetch(userId);
submissionDesc.push(`${reactionNumbers[index-1]} - ${user.username}`);
} catch (error) {
console.error(`Error getting user ${userId} for poll:`, error);
submissionDesc.push(`${reactionNumbers[index-1]} - Unknown Artist`);
}
index++;
}
votingEmbed.addFields({ name: 'Submissions', value: submissionDesc.join('\n') });
// Send the voting message
const pollMessage = await galleryChannel.send({ embeds: [votingEmbed] });
this.pollMessage = pollMessage;
// Add reaction options
for (let i = 0; i < Math.min(submissionIds.length, reactionNumbers.length); i++) {
await pollMessage.react(reactionNumbers[i]);
// Add a small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 300));
}
// Set timer to end poll after 24 hours
setTimeout(() => this.endVotingPoll(galleryChannel, submissionIds, reactionNumbers), 24 * 60 * 60 * 1000);
} catch (error) {
console.error('Error creating voting poll:', error);
galleryChannel.send('Error creating voting poll. Please vote manually by replying to your favorite submission.');
}
}
async endVotingPoll(galleryChannel, submissionIds, reactionNumbers) {
try {
if (!this.pollMessage) return;
// Fetch the message with reactions
const pollMessage = await galleryChannel.messages.fetch(this.pollMessage.id);
// Count votes
const votes = [];
for (let i = 0; i < Math.min(submissionIds.length, reactionNumbers.length); i++) {
const reaction = pollMessage.reactions.cache.get(reactionNumbers[i]);
if (reaction) {
// Subtract 1 to not count the bot's own reaction
const count = Math.max(0, reaction.count - 1);
votes.push({
userId: submissionIds[i],
count: count
});
} else {
votes.push({
userId: submissionIds[i],
count: 0
});
}
}
// Sort by votes (highest first)
votes.sort((a, b) => b.count - a.count);
// Check if there's a winner (votes > 0 and more than second place)
let winnerAnnouncement = '';
if (votes.length > 0 && votes[0].count > 0 && (votes.length === 1 || votes[0].count > votes[1].count)) {
try {
const winner = await client.users.fetch(votes[0].userId);
winnerAnnouncement = `πŸ† The winner is **${winner.username}** with ${votes[0].count} votes!`;
// Update winner stats
const stats = getUserStats(winner.id, winner.username);
stats.wins++;
await saveUserStats();
} catch (error) {
console.error('Error fetching winner:', error);
winnerAnnouncement = 'πŸ† We have a winner, but couldn\'t fetch their username.';
}
} else if (votes.length > 1 && votes[0].count > 0 && votes[0].count === votes[1].count) {
// Tie
const tiedUsers = [];
for (const vote of votes) {
if (vote.count === votes[0].count) {
try {
const user = await client.users.fetch(vote.userId);
tiedUsers.push(user.username);
} catch (error) {
console.error('Error fetching tied user:', error);
}
}
}
if (tiedUsers.length > 0) {
winnerAnnouncement = `πŸ† We have a tie between **${tiedUsers.join('** and **')}** with ${votes[0].count} votes each!`;
} else {
winnerAnnouncement = `πŸ† We have a tie, but couldn't fetch the usernames.`;
}
} else {
winnerAnnouncement = 'No votes were cast in this competition.';
}
// Announce results
const resultsEmbed = new EmbedBuilder()
.setTitle(`Voting Results: ${this.topic}`)
.setDescription(winnerAnnouncement)
.setColor('#2ECC71')
.setTimestamp();
// Add vote counts for all participants
const voteResults = [];
for (const vote of votes) {
try {
const user = await client.users.fetch(vote.userId);
voteResults.push(`${user.username}: ${vote.count} vote${vote.count === 1 ? '' : 's'}`);
} catch (error) {
console.error('Error fetching user for vote results:', error);
voteResults.push(`Unknown Artist: ${vote.count} vote${vote.count === 1 ? '' : 's'}`);
}
}
if (voteResults.length > 0) {
resultsEmbed.addFields({ name: 'Vote Counts', value: voteResults.join('\n') });
}
await galleryChannel.send({ embeds: [resultsEmbed] });
// Send announcement to stats channel too
try {
const statsChannel = await client.channels.fetch(STATS_CHANNEL_ID);
await statsChannel.send({ embeds: [resultsEmbed] });
} catch (error) {
console.error('Error sending to stats channel:', error);
}
} catch (error) {
console.error('Error ending voting poll:', error);
galleryChannel.send('Error calculating voting results.');
}
}
getRemainingTime() {
if (!this.active) return 0;
const elapsed = Date.now() - this.startTime;
const remaining = Math.max(0, this.timeLimit - elapsed);
return remaining;
}
formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
async sendMessage(message) {
try {
const channel = await client.channels.fetch(this.channelId);
await channel.send(message);
} catch (error) {
console.error('Error sending message:', error);
}
}
}
// Bot commands handler
const PREFIX = '!';
client.on('ready', () => {
console.log(`Logged in as ${client.user.tag}!`);
client.user.setActivity('Speed Competitions', { type: 'COMPETING' });
// Initialize user stats
initUserStats();
// Verify channel IDs exist on startup
try {
client.channels.fetch(CHALLENGE_CHANNEL_ID)
.then(channel => console.log(`Challenge channel verified: ${channel.name}`))
.catch(err => console.error('Challenge channel not found! Please check the channel ID.'));
client.channels.fetch(GALLERY_CHANNEL_ID)
.then(channel => console.log(`Gallery channel verified: ${channel.name}`))
.catch(err => console.error('Gallery channel not found! Please check the channel ID.'));
client.channels.fetch(STATS_CHANNEL_ID)
.then(channel => console.log(`Stats channel verified: ${channel.name}`))
.catch(err => console.error('Stats channel not found! Please check the channel ID.'));
} catch (error) {
console.error('Error verifying channels:', error);
}
});
client.on('messageCreate', async (message) => {
// Ignore messages from bots
if (message.author.bot) return;
// Update message stats for the user
if (!message.content.startsWith(PREFIX)) {
const stats = getUserStats(message.author.id, message.author.username);
stats.messages++;
stats.totalWords += message.content.split(/\s+/).length;
// Save periodically (every 10th message)
if (stats.messages % 10 === 0) {
saveUserStats();
}
}
// Ignore messages that don't start with the prefix
if (!message.content.startsWith(PREFIX)) return;
// Parse command and arguments
const args = message.content.slice(PREFIX.length).trim().split(/\s+/);
const command = args.shift().toLowerCase();
// Get the channel ID
const channelId = message.channel.id;
// Get or create competition for this channel
let competition = competitions.get(channelId);
// Handle commands
switch (command) {
case 'topic':
handleTopic(message, args, competition);
break;
case 'limit':
handleLimit(message, args, competition);
break;
case 'timeleft':
handleTimeLeft(message, competition);
break;
case 'in':
handleJoin(message, competition);
break;
case 'out':
handleLeave(message, competition);
break;
case 'upload':
handleUpload(message, competition);
break;
case 'start':
handleStart(message, competition);
break;
case 'end':
handleEnd(message, competition);
break;
case 'help':
handleHelp(message);
break;
case 'stats':
handleStats(message, args);
break;
case 'leaderboard':
handleLeaderboard(message);
break;
case 'test-gallery': // Testing command for gallery posting
testGalleryPosting(message);
break;
}
});
// Stats command handler
async function handleStats(message, args) {
// If no username provided, show stats for the message author
let targetUser = message.author;
// If a username is provided, try to find that user
if (args.length > 0) {
const username = args.join(' ');
try {
// Try to find by mention
if (message.mentions.users.size > 0) {
targetUser = message.mentions.users.first();
} else {
// Try to find by username
const guildMembers = await message.guild.members.fetch();
const foundMember = guildMembers.find(member =>
member.user.username.toLowerCase() === username.toLowerCase() ||
(member.nickname && member.nickname.toLowerCase() === username.toLowerCase())
);
if (foundMember) {
targetUser = foundMember.user;
} else {
return message.reply(`Could not find user "${username}". Please use a valid username or mention.`);
}
}
} catch (error) {
console.error('Error finding user:', error);
return message.reply('Error finding user. Please try again.');
}
}
// Get stats for the user
const stats = getUserStats(targetUser.id, targetUser.username);
// Create stats embed
const statsEmbed = new EmbedBuilder()
.setTitle(`Stats for ${targetUser.username}`)
.setThumbnail(targetUser.displayAvatarURL())
.setColor('#00FF00')
.addFields(
{ name: 'Total Messages', value: stats.messages.toString(), inline: true },
{ name: 'Total Words', value: stats.totalWords.toString(), inline: true },
{ name: 'Competition Participations', value: stats.participations.toString(), inline: true },
{ name: 'Competition Wins', value: stats.wins.toString(), inline: true },
{ name: 'Win Rate', value: stats.participations > 0
? `${Math.round((stats.wins / stats.participations) * 100)}%`
: 'N/A',
inline: true
}
)
.setFooter({ text: 'Stats tracked since bot implementation' });
message.channel.send({ embeds: [statsEmbed] });
}
// Leaderboard command handler
async function handleLeaderboard(message) {
// Create sorted arrays of users for different categories
const allUsers = Array.from(userStats.values());
// Top Participants
const topParticipants = [...allUsers]
.filter(user => user.participations > 0)
.sort((a, b) => b.participations - a.participations)
.slice(0, 5);
// Top Winners
const topWinners = [...allUsers]
.filter(user => user.wins > 0)
.sort((a, b) => b.wins - a.wins)
.slice(0, 5);
// Top Win Rate (min 3 participations)
const topWinRate = [...allUsers]
.filter(user => user.participations >= 3)
.sort((a, b) => (b.wins / b.participations) - (a.wins / a.participations))
.slice(0, 5);
// Create leaderboard embed
const leaderboardEmbed = new EmbedBuilder()
.setTitle('Competition Leaderboards')
.setColor('#FFD700')
.setDescription('Top competitors in the server!')
.setTimestamp();
// Add top participants field
if (topParticipants.length > 0) {
const participantsText = topParticipants
.map((user, index) => `${index + 1}. **${user.username}** - ${user.participations} competitions`)
.join('\n');
leaderboardEmbed.addFields({ name: 'Most Active Competitors', value: participantsText });
} else {
leaderboardEmbed.addFields({ name: 'Most Active Competitors', value: 'No data yet' });
}
// Add top winners field
if (topWinners.length > 0) {
const winnersText = topWinners
.map((user, index) => `${index + 1}. **${user.username}** - ${user.wins} wins`)
.join('\n');
leaderboardEmbed.addFields({ name: 'Most Wins', value: winnersText });
} else {
leaderboardEmbed.addFields({ name: 'Most Wins', value: 'No data yet' });
}
// Add top win rate field
if (topWinRate.length > 0) {
const winRateText = topWinRate
.map((user, index) => {
const rate = Math.round((user.wins / user.participations) * 100);
return `${index + 1}. **${user.username}** - ${rate}% (${user.wins}/${user.participations})`;
})
.join('\n');
leaderboardEmbed.addFields({ name: 'Best Win Rate (min. 3 competitions)', value: winRateText });
} else {
leaderboardEmbed.addFields({ name: 'Best Win Rate (min. 3 competitions)', value: 'No data yet' });
}
message.channel.send({ embeds: [leaderboardEmbed] });
}
// Testing function to verify gallery posting
async function testGalleryPosting(message) {
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return message.reply('This test command is for administrators only.');
}
try {
message.channel.send('Testing gallery posting...');
const galleryChannel = await client.channels.fetch(GALLERY_CHANNEL_ID);
if (!galleryChannel) {
return message.reply(`Gallery channel with ID ${GALLERY_CHANNEL_ID} not found!`);
}
const testEmbed = new EmbedBuilder()
.setTitle('Gallery Test Post')
.setDescription('This is a test post to verify gallery posting functionality.')
.setColor('#00FF00')
.setTimestamp();
await galleryChannel.send({ embeds: [testEmbed] });
message.reply(`βœ… Successfully posted test message to gallery channel: <#${GALLERY_CHANNEL_ID}>`);
} catch (error) {
console.error('Error testing gallery posting:', error);
message.reply(`❌ Error posting to gallery: ${error.message}`);
}
}
// Command handlers
async function handleTopic(message, args, competition) {
if (!competition) {
competition = new Competition(message.channel.id, message.author.id);
competitions.set(message.channel.id, competition);
}
if (competition.active) {
return message.reply('Cannot change topic during an active competition.');
}
const topic = args.join(' ');
if (!topic) {
return message.reply('Please provide a topic. Usage: `!topic Drawing Subject`');
}
competition.topic = topic;
const embed = new EmbedBuilder()
.setTitle('Speed Competition Topic Set')
.setDescription(`Topic: **${topic}**`)
.setColor('#00FF00')
.setFooter({ text: 'Use !in to join the competition' });
message.channel.send({ embeds: [embed] });
}
async function handleLimit(message, args, competition) {
if (!competition) {
competition = new Competition(message.channel.id, message.author.id);
competitions.set(message.channel.id, competition);
}
if (competition.active) {
return message.reply('Cannot change time limit during an active competition.');
}
const minutes = parseInt(args[0]);
if (isNaN(minutes) || minutes <= 0 || minutes > 180) {
return message.reply('Please provide a valid time limit in minutes (1-180). Usage: `!limit 30`');
}
competition.timeLimit = minutes * 60 * 1000;
message.channel.send(`⏱️ Time limit set to ${minutes} minute${minutes === 1 ? '' : 's'}.`);
}
async function handleTimeLeft(message, competition) {
if (!competition || !competition.active) {
return message.reply('No active competition in this channel.');
}
const remainingTime = competition.getRemainingTime();
const formattedTime = competition.formatTime(remainingTime);
message.channel.send(`⏳ Time remaining: ${formattedTime}`);
}
async function handleJoin(message, competition) {
if (!competition) {
competition = new Competition(message.channel.id, message.author.id);
competitions.set(message.channel.id, competition);
}
if (competition.participants.has(message.author.id)) {
return message.reply('You are already in the competition!');
}
competition.participants.add(message.author.id);
// Add participant role
await competition.addParticipantRole(message.member);
// Get a random goading join message
const goadingMessage = joinMessages[Math.floor(Math.random() * joinMessages.length)];
message.reply(`You've joined the speed competition! Topic: **${competition.topic}**\n\n${goadingMessage}`);
// Announce if this is the first participant
if (competition.participants.size === 1) {
message.channel.send(`🎨 A new speed competition is forming! Topic: **${competition.topic}**\nTime limit: ${competition.timeLimit / 60000} minutes\nUse \`!in\` to join!`);
} else {
message.channel.send(`πŸ‘‹ ${message.author.username} has joined the competition! Total participants: ${competition.participants.size}`);
}
}
async function handleLeave(message, competition) {
if (!competition || !competition.participants.has(message.author.id)) {
return message.reply('You are not in any competition in this channel.');
}
competition.participants.delete(message.author.id);
competition.submissions.delete(message.author.id);
// Remove participant role
await competition.removeParticipantRole(message.member);
// Get a random goading leave message
const goadingMessage = leaveMessages[Math.floor(Math.random() * leaveMessages.length)];
message.reply(`You have left the speed competition.\n\n${goadingMessage}`);
// If no participants left, clean up
if (competition.participants.size === 0) {
if (competition.active) {
competition.end();
}
competitions.delete(message.channel.id);
message.channel.send('Competition canceled due to lack of participants.');
}
}
async function handleUpload(message, competition) {
if (!competition || !competition.active) {
return message.reply('No active competition in this channel.');
}
if (!competition.participants.has(message.author.id)) {
return message.reply('You are not participating in this competition. Use `!in` to join.');
}
// Check for attachments
if (message.attachments.size === 0) {
return message.reply('Please attach an image with your submission. Usage: `!upload` with an image attached.');
}
const attachment = message.attachments.first();
// Verify it's an image
const validImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!validImageTypes.includes(attachment.contentType)) {
return message.reply('Please upload a valid image file (JPEG, PNG, GIF, or WEBP).');
}
// Save submission
competition.submissions.set(message.author.id, {
url: attachment.url,
timestamp: Date.now()
});
message.reply('Your submission has been recorded!');
// Check if all participants have submitted
if (competition.submissions.size === competition.participants.size) {
message.channel.send('All participants have submitted their work! The competition will continue until the time limit is reached.');
}
}
async function handleStart(message, competition) {
if (!competition) {
competition = new Competition(message.channel.id, message.author.id);
competitions.set(message.channel.id, competition);
}
if (competition.active) {
return message.reply('A competition is already active in this channel.');
}
if (competition.participants.size === 0) {
return message.reply('Cannot start competition with no participants. Use `!in` to join.');
}
competition.start();
const embed = new EmbedBuilder()
.setTitle('πŸš€ Speed Competition Started!')
.setDescription(`Topic: **${competition.topic}**`)
.setColor('#FF5733')
.addFields(
{ name: 'Time Limit', value: `${competition.timeLimit / 60000} minutes` },
{ name: 'Participants', value: `${competition.participants.size} artists` },
{ name: 'Submission', value: 'Use `!upload` with an image attachment to submit your work' }
)
.setFooter({ text: 'Good luck and have fun!' });
message.channel.send({ embeds: [embed] });
}
async function handleEnd(message, competition) {
if (!competition || !competition.active) {
return message.reply('No active competition in this channel.');
}
// Only allow the initiator or admins to end the competition
if (message.author.id !== competition.initiator && !message.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return message.reply('Only the competition creator or admins can end the competition early.');
}
message.channel.send('Competition ending early by administrator command.');
competition.end();
}
async function handleHelp(message) {
const embed = new EmbedBuilder()
.setTitle('Speed Competition Bot Commands')
.setDescription('Here are the available commands:')
.setColor('#3498DB')
.addFields(
{ name: '!topic [subject]', value: 'Set the competition topic' },
{ name: '!limit [minutes]', value: 'Set the time limit (1-180 minutes)' },
{ name: '!timeleft', value: 'Check remaining time' },
{ name: '!in', value: 'Join the competition' },
{ name: '!out', value: 'Leave the competition' },
{ name: '!upload', value: 'Submit your final image (attach image to message)' },
{ name: '!start', value: 'Start the competition timer' },
{ name: '!end', value: 'End the competition early (admin only)' },
{ name: '!stats [username]', value: 'Show stats for yourself or another user' },
{ name: '!leaderboard', value: 'Show the competition leaderboards' },
{ name: '!help', value: 'Show this help message' }
)
.setFooter({ text: 'Speed Competition Bot by wispbyte.com' });
message.channel.send({ embeds: [embed] });
}
// Handle reaction events for polls
client.on('messageReactionAdd', async (reaction, user) => {
// Ignore bot's own reactions
if (user.bot) return;
// Partial reactions need to be fetched to access their data
if (reaction.partial) {
try {
await reaction.fetch();
} catch (error) {
console.error('Error fetching reaction:', error);
return;
}
}
// Check if this is a poll message from any active competition
for (const [channelId, competition] of competitions.entries()) {
if (competition.pollMessage && reaction.message.id === competition.pollMessage.id) {
// Ensure users only vote once by removing other reactions from the same user
const userReactions = reaction.message.reactions.cache.filter(r => r.users.cache.has(user.id));
for (const r of userReactions.values()) {
if (r.emoji.name !== reaction.emoji.name) {
try {
await r.users.remove(user.id);
} catch (error) {
console.error('Error removing reaction:', error);
}
}
}
break;
}
}
});
// Periodically save stats (every 5 minutes as a backup)
setInterval(() => {
saveUserStats();
}, 5 * 60 * 1000);
// Login to Discord with the app token
client.login(process.env.DISCORD_TOKEN);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment