Created
April 13, 2025 07:19
-
-
Save RH2/6eb2048b5f4f0f940e72b8eb5e000503 to your computer and use it in GitHub Desktop.
smcman _with polling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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