From 6b2fab8b9546a87eb60e6da4241ad606c0e449b9 Mon Sep 17 00:00:00 2001 From: admin_rb Date: Thu, 2 Jul 2026 18:37:22 +0200 Subject: [PATCH] EZD Logik --- commands/slash/applications/ezd.js | 114 ++++++++++++++++ commands/slash/applications/wge.js | 211 ++++++++++++++++++++++------- events/ClientReady.js | 21 +-- events/InteractionCreate.js | 2 +- events/MessageCreate.js | 89 +++++++++++- 5 files changed, 374 insertions(+), 63 deletions(-) create mode 100644 commands/slash/applications/ezd.js diff --git a/commands/slash/applications/ezd.js b/commands/slash/applications/ezd.js new file mode 100644 index 0000000..316b173 --- /dev/null +++ b/commands/slash/applications/ezd.js @@ -0,0 +1,114 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + MessageFlags +} = require('discord.js'); +const { mClient } = require('../../..'); +require('dotenv').config() + +function formatGermanDate(dateStr) { + const date = new Date(dateStr); + + return new Intl.DateTimeFormat("de-DE", { + year: "numeric", + month: "2-digit", + day: "2-digit" + }).format(date); +} + +module.exports = { + data: new SlashCommandBuilder() + .setName('ezd') + .setDescription('Zeigt die täglichen Erster/Zweiter/Dritter Ergebnisse'), + + async execute(interaction, client) { + const db = mClient.db(process.env.M_DB); // adjust if needed + const coll = db.collection('items_daily_numbers'); + + const today = new Date().toISOString().slice(0, 10); + + let currentDate = today; + + const sessionId = `${interaction.user.id}-${Date.now()}`; + + const buildEmbed = (doc, date) => { + const p = doc?.positions || {}; + + const line = (emoji, label, key) => { + const v = p[key]; + return `${emoji} **${label}:** ${v ? `<@${v.userId}>` : "—"}`; + }; + + return new EmbedBuilder() + .setTitle(`📊 EZD Übersicht – ${formatGermanDate(date)}`) + .setColor(0x2b2d31) + .setDescription( + [ + line("🥇", "Erster", "Erster"), + line("🥈", "Zweiter", "Zweiter"), + line("🥉", "Dritter", "Dritter") + ].join("\n") + ) + .setFooter({ text: `Datum: ${date}` }); + }; + + const buildButtons = (date) => { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ezd_prev`) + .setLabel("⬅️") + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ezd_next`) + .setLabel("➡️") + .setStyle(ButtonStyle.Secondary) + ); + }; + + const doc = await coll.findOne({ date: currentDate }); + + const msg = await interaction.reply({ + embeds: [buildEmbed(doc, currentDate)], + components: [buildButtons(currentDate)], + flags: MessageFlags.Ephemeral + }); + + const collector = msg.createMessageComponentCollector({ + time: 1000 * 60 * 10 // 10 minutes + }); + + collector.on("collect", async (btn) => { + if (btn.user.id !== interaction.user.id) { + return btn.reply({ + content: "❌ Diese Buttons gehören nicht dir.", + flags: MessageFlags.Ephemeral + }); + } + + if (btn.customId === `ezd_prev`) { + currentDate = shiftDate(currentDate, -1); + } + + if (btn.customId === `ezd_next`) { + currentDate = shiftDate(currentDate, 1); + } + + const newDoc = await coll.findOne({ date: currentDate }); + + await btn.update({ + embeds: [buildEmbed(newDoc, currentDate)], + components: [buildButtons(currentDate)] + }); + }); + } +}; + +// helper: shift YYYY-MM-DD safely +function shiftDate(dateStr, offset) { + const d = new Date(dateStr); + d.setDate(d.getDate() + offset); + return d.toISOString().slice(0, 10); +} \ No newline at end of file diff --git a/commands/slash/applications/wge.js b/commands/slash/applications/wge.js index 84b0687..a9bab52 100644 --- a/commands/slash/applications/wge.js +++ b/commands/slash/applications/wge.js @@ -1,6 +1,8 @@ -const { ModalBuilder, UserSelectMenuBuilder, TextInputBuilder, EmbedBuilder, TimestampStyles, time, StringSelectMenuBuilder } = require("@discordjs/builders"); +const { ModalBuilder, UserSelectMenuBuilder, TextInputBuilder, EmbedBuilder, TimestampStyles, time, StringSelectMenuBuilder, messageLink } = require("@discordjs/builders"); +const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require('discord.js') const { SlashCommandBuilder, LabelBuilder, TextInputStyle, MessageFlags } = require("discord.js"); const { mClient } = require("../../.."); +const db = mClient.db('neo-db') require('dotenv').config() function getRemaining(timer) { @@ -22,7 +24,7 @@ function ordinal(n) { } async function wgeStatus(interaction) { - const db = mClient.db('neo-db') + const wgeColl = db.collection('timer_wge') const historyColl = db.collection('history_wge'); @@ -62,7 +64,7 @@ async function wgeStart(interaction) { const defaultUsers = [] defaultUsers.push(process.env.D_ID) - const db = mClient.db('neo-db') + const participantColl = db.collection('items_wge') const participants = await participantColl.find({ status: true @@ -128,7 +130,7 @@ async function wgeStart(interaction) { } async function wgeStop(interaction) { - const db = mClient.db('neo-db'); + ; const wgeColl = db.collection('timer_wge'); const timer = await wgeColl.findOne({ @@ -162,7 +164,7 @@ async function wgeStop(interaction) { } async function wgeResume(interaction) { - const db = mClient.db('neo-db'); + ; const wgeColl = db.collection('timer_wge'); const timer = await wgeColl.findOne({ @@ -196,7 +198,7 @@ async function wgeResume(interaction) { } async function wgeForfeit(interaction) { - const db = mClient.db('neo-db'); + ; const wgeColl = db.collection('timer_wge'); const participateColl = db.collection('items_wge'); @@ -275,14 +277,10 @@ async function wgeForfeit(interaction) { }); } -async function wgeHistory(interaction) { - //FIXME! -} - async function wgeParticipate(interaction) { const status = interaction.options.getBoolean('status') const user = interaction.user - const db = mClient.db('neo-db') + const coll = db.collection('items_wge') // Choice was false, ergo not participating @@ -303,50 +301,167 @@ async function wgeParticipate(interaction) { }) } +async function listParticipants(interaction) { + ; + const participantColl = db.collection('items_wge'); + const participants = await participantColl.find({}).toArray(); + + const participating = participants + .filter(p => p.status) + .map(p => `<@${p.userID}>`); + + const notParticipating = participants + .filter(p => !p.status) + .map(p => `<@${p.userID}>`); + + const embed = new EmbedBuilder() + .setTitle('WGE Participation') + .setColor(0x5865F2) + .addFields( + { + name: `Participating (${participating.length})`, + value: participating.join('\n') || 'No participants found.', + inline: true + }, + { + name: `Not Participating (${notParticipating.length})`, + value: notParticipating.join('\n') || 'No entries found.', + inline: true + } + ) + .setFooter({ + text: `Total entries: ${participants.length}` + }) + + await interaction.reply({ + embeds: [embed], + flags: MessageFlags.Ephemeral + }); +} + +function formatDuration(ms) { + if (ms == null) return "unknown" + + const totalSeconds = Math.floor(ms / 1000) + const days = Math.floor(totalSeconds / 86400) + const hours = Math.floor((totalSeconds % 86400) / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + const parts = [] + + if (days) parts.push(`${days}d`) + if (hours) parts.push(`${hours}h`) + if (minutes) parts.push(`${minutes}m`) + if (seconds && !days) parts.push(`${seconds}s`) // only show seconds for short durations + + return parts.length ? parts.join(" ") : "0s" +} + +async function listHistory(interaction) { + const historyColl = db.collection('history_wge') + + const wgeHistory = await historyColl + .find({ debug: false }) + .sort({ createdAt: -1 }) + .toArray() + + if (!wgeHistory.length) { + return interaction.reply({ + content: "No WGE history found.", + flags: MessageFlags.Ephemeral + }) + } + + const buildEmbed = (entry, index) => { + const createdTs = entry.createdAt + ? Math.floor(new Date(entry.createdAt).getTime() / 1000) + : null + + return new EmbedBuilder() + .setTitle(`WGE History (${index + 1}/${wgeHistory.length})`) + .setDescription( + `<@${entry.assignedUser}> was the **World's Greatest Expert** of...\n\n` + + `**${entry.topic}**\n\n` + + + `Assigned by: <@${entry.assigningUser}>\n` + + (createdTs ? `Created: ` : "") + ) + .setColor(0x2b2d31) + } + + let page = 0 + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('prev') + .setLabel('◀') + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + + new ButtonBuilder() + .setCustomId('next') + .setLabel('▶') + .setStyle(ButtonStyle.Secondary) + .setDisabled(wgeHistory.length <= 1) + ) + + const msg = await interaction.reply({ + embeds: [buildEmbed(wgeHistory[page], page)], + components: [row], + fetchReply: true + }) + + const collector = msg.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 120000 + }) + + collector.on('collect', async i => { + if (i.user.id !== interaction.user.id) { + return i.reply({ + content: "You can't control this pagination.", + flags: MessageFlags.Ephemeral + }) + } + + if (i.customId === 'prev') { + page-- + } else if (i.customId === 'next') { + page++ + } + + page = Math.max(0, Math.min(page, wgeHistory.length - 1)) + + row.components[0].setDisabled(page === 0) + row.components[1].setDisabled(page === wgeHistory.length - 1) + + await i.update({ + embeds: [buildEmbed(wgeHistory[page], page)], + components: [row] + }) + }) + + collector.on('end', async () => { + const disabledRow = new ActionRowBuilder().addComponents( + row.components.map(btn => ButtonBuilder.from(btn).setDisabled(true)) + ) + + await msg.edit({ components: [disabledRow] }).catch(() => { }) + }) +} + async function wgeList(interaction) { const thing = interaction.options.getString('thing'); switch (thing) { case 'participants': { - const db = mClient.db('neo-db'); - const participantColl = db.collection('items_wge'); - const participants = await participantColl.find({}).toArray(); - - const participating = participants - .filter(p => p.status) - .map(p => `<@${p.userID}>`); - - const notParticipating = participants - .filter(p => !p.status) - .map(p => `<@${p.userID}>`); - - const embed = new EmbedBuilder() - .setTitle('WGE Participation') - .setColor(0x5865F2) - .addFields( - { - name: `Participating (${participating.length})`, - value: participating.join('\n') || 'No participants found.', - inline: true - }, - { - name: `Not Participating (${notParticipating.length})`, - value: notParticipating.join('\n') || 'No entries found.', - inline: true - } - ) - .setFooter({ - text: `Total entries: ${participants.length}` - }) - - await interaction.reply({ - embeds: [embed], - flags: MessageFlags.Ephemeral - }); - + listParticipants(interaction) break; } - + case 'history': + listHistory(interaction) + break; default: break; } diff --git a/events/ClientReady.js b/events/ClientReady.js index 663ea83..c71f868 100644 --- a/events/ClientReady.js +++ b/events/ClientReady.js @@ -50,8 +50,19 @@ module.exports = { if (!dbEntry) continue; - // skip if already notified - if (dbEntry.notified) { + // Reset notification once the birthday has passed + if (!isToday(dbEntry.day, dbEntry.month) && dbEntry.notified) { + await bdayColl.updateOne( + { userID: member.id }, + { $set: { notified: false } } + ); + + dbEntry.notified = false; // keep local copy in sync + console.log(`Removed notified flag for ${member.user.tag}`); + } + + // Skip if already handled today + if (dbEntry.notified && isToday(dbEntry.day, dbEntry.month)) { console.log(`${member.user.username} was already notified today, skipping...`); continue; } @@ -78,12 +89,6 @@ module.exports = { console.log(`Removed birthday role from ${member.user.tag}`); } - - // reset notification flag if not birthday anymore - await bdayColl.updateOne( - { userID: member.id }, - { $set: { notified: false } } - ); } } }; diff --git a/events/InteractionCreate.js b/events/InteractionCreate.js index 28d2496..1bb6815 100644 --- a/events/InteractionCreate.js +++ b/events/InteractionCreate.js @@ -33,7 +33,7 @@ async function commandHandler(interaction) { } async function buttonHandler(interaction) { const button = interaction.client.buttons.get(interaction.customId) - if (!button) { return console.error(`No button registered matching ${interaction.customId}.`) } + if (!button) { return }// console.error(`No button registered matching ${interaction.customId}.`) } try { await button.execute(interaction); } catch (error) { diff --git a/events/MessageCreate.js b/events/MessageCreate.js index 4e38e1c..97a07ed 100644 --- a/events/MessageCreate.js +++ b/events/MessageCreate.js @@ -1,20 +1,97 @@ -const { Events } = require('discord.js') +const { Events, MessageFlags } = require('discord.js') require('dotenv').config() -const { client } = require('../index') +const { client, mClient } = require('../index') const prefix = process.env.D_Prefix +const db = mClient.db(process.env.M_DB) module.exports = { name: Events.MessageCreate, once: false, async execute(message) { - if (!message.content.startsWith(prefix) || message.author.bot) return; + if (message.author.bot) { return } + // Erster/Zweiter/Dritter Handler + if (message.channelId === '471058991481487361') { + const numbered = ["Erster", "Zweiter", "Dritter"]; + + // Regex-Erkennung (ganze Wörter, unabhängig von Groß-/Kleinschreibung) + const found = numbered.find(word => + new RegExp(`\\b${word}\\b`, "i").test(message.content) + ); + + if (!found) return; + + const numberedColl = db.collection('items_daily_numbers'); + const today = new Date().toISOString().slice(0, 10); + + // Tages-Dokument sicherstellen + await numberedColl.findOneAndUpdate( + { date: today }, + { + $setOnInsert: { + date: today, + positions: { + Erster: null, + Zweiter: null, + Dritter: null + } + } + }, + { upsert: true } + ); + + // 🔒 Atomarer Claim: + // - Position muss frei sein + // - User darf noch keine Position heute haben + const result = await numberedColl.findOneAndUpdate( + { + date: today, + + // Position frei + [`positions.${found}`]: null, + + // User hat noch keine Position + "positions.Erster.userId": { $ne: message.author.id }, + "positions.Zweiter.userId": { $ne: message.author.id }, + "positions.Dritter.userId": { $ne: message.author.id } + }, + { + $set: { + [`positions.${found}`]: { + userId: message.author.id, + timestamp: new Date() + } + } + }, + { returnDocument: "after" } + ); + + // ❌ bereits vergeben oder schon eine Position + if (!result) { + return message.reply({ + content: `❌ Du kannst pro Tag nur eine Position beanspruchen, und sie muss noch frei sein.`, + flags: MessageFlags.Ephemeral + }); + } + + // ✅ Erfolg + return message.reply({ + content: `✅ Du bist ${found}! <@${message.author.id}>`, + flags: MessageFlags.Ephemeral + }); + } + + // Command Handler const args = message.content.slice(prefix.length).trim().split(/ +/); + if (!message.content.startsWith(prefix)) return; + const cmd = args.shift().toLowerCase(); let command = client.legacyCommands.get(cmd) - if(!command) command = client.legacyCommands.get(client.aliases.get(cmd)) - if(!command) return - try { + + + if (!command) command = client.legacyCommands.get(client.aliases.get(cmd)) + if (!command) return + try { client.legacyCommands.get(command.name).execute(message, args) } catch (error) { console.error(error)