import 'dotenv/config'; import { Client, GatewayIntentBits, Partials, Events, ButtonBuilder, ButtonStyle, ActionRowBuilder, PermissionsBitField, EmbedBuilder } from 'discord.js'; import fs from 'fs'; import { NEWS_CHANNEL_MESSAGE, INELLIGIBLE_MESSAGE, formatNoKeysMessage, formatKeyMessage, formatFailedDMMessage } from './messages.js'; const TOKEN = process.env.DISCORD_TOKEN; const DATA_FILE = process.env.DATA_FILE; const ERROR_FILE = process.env.ERROR_FILE; function loadData() { if (!fs.existsSync(DATA_FILE)) { return { messageId: null, joinCutoff: null, users: [], keys: [], loggingChannel: null }; } const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); if (!data.keys) data.keys = []; return data; } function saveData(data) { fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2)); } function loadErrorLog() { if (!fs.existsSync(ERROR_FILE)) { return []; } return JSON.parse(fs.readFileSync(ERROR_FILE, 'utf8')); } function saveErrorLog(errors) { fs.writeFileSync(ERROR_FILE, JSON.stringify(errors, null, 2)); } function handleFailedLoggingChannel(tag, id, key, err) { errors.push({ timestamp: new Date().toISOString(), message: `Unable to send error message in logging channel when attempting to DM to user ${tag} (${id}) with key ${key}. Please see error log for details.`, userId: interaction.user.id, error: JSON.stringify(err, null, 2) }) } let data = loadData(); let errors = loadErrorLog(); const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.DirectMessages], partials: [Partials.Channel] }); async function deleteLatestBotDMs(userId) { try { const user = await client.users.fetch(userId); const dm = await user.createDM(); // Fetch up to 100 messages (Discord API limit per request) const messages = await dm.messages.fetch({ limit: 100 }); let deleted = 0; for (const msg of messages.values()) { if (msg.author.id === client.user.id) { await msg.delete(); deleted++; } } console.log(`Deleted ${deleted} DMs sent by the bot.`); } catch (err) { console.error('Error deleting DMs:', err); } } client.once(Events.ClientReady, async () => { console.log(`Logged in as ${client.user.tag}`); }); client.on(Events.InteractionCreate, async interaction => { if (!interaction.guild) { return interaction.reply({ content: 'This feature can only be used in a server.' }); } // Slash command: /register if (interaction.isChatInputCommand() && interaction.commandName === 'register') { // Check if loggingChannel is set if (!data.loggingChannel) { return interaction.reply({ content: 'You must set a logging channel first using `/keys logging`. Registration message not sent.', ephemeral: true }); } const embed = new EmbedBuilder() .setDescription('**NOTE**: Click "Claim Key" again if you need your key re-sent.') .setColor(0xff0000); const button = new ButtonBuilder() .setCustomId('register_button') .setLabel('Claim Key') .setStyle(ButtonStyle.Success); const row = new ActionRowBuilder().addComponents(button); const sent = await interaction.channel.send({ content: NEWS_CHANNEL_MESSAGE, embeds: [embed], components: [row] }); data.messageId = sent.id; saveData(data); await interaction.reply({ content: 'Registration message sent!', ephemeral: true }); } // Slash command: /key if (interaction.isChatInputCommand() && interaction.commandName === 'keys') { const sub = interaction.options.getSubcommand(); // /key load if (sub === 'load') { const file = interaction.options.getAttachment('file'); if (!file) { return interaction.reply({ content: 'You must attach a file.', ephemeral: true }); } const response = await fetch(file.url); const text = await response.text(); const keys = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); const yesButton = new ButtonBuilder() .setCustomId('key_load_yes') .setLabel('Yes') .setStyle(ButtonStyle.Success); const noButton = new ButtonBuilder() .setCustomId('key_load_no') .setLabel('No') .setStyle(ButtonStyle.Danger); const row = new ActionRowBuilder().addComponents(yesButton, noButton); client.tempKeyLoads = client.tempKeyLoads || {}; client.tempKeyLoads[interaction.user.id] = keys; await interaction.reply({ content: `Found ${keys.length} steam keys. Do you wish to load these into the key vault?`, components: [row], ephemeral: true }); return; } // /key get if (sub === 'get') { const user = interaction.options.getUser('user'); if (!user) { return interaction.reply({ content: 'Invalid user.', ephemeral: true }); } data = loadData(); const found = data.users.find(u => u.id === user.id); if (found && found.key) { return interaction.reply({ content: `User <@${user.id}> has registered key: \`${found.key}\``, ephemeral: true }); } else { return interaction.reply({ content: `User <@${user.id}> does not have a registered key.`, ephemeral: true }); } } // /key check if (sub === 'check') { data = loadData(); const count = data.keys ? data.keys.length : 0; return interaction.reply({ content: `There are ${count} keys left in the vault.`, ephemeral: true }); } // /keys logging if (sub === 'logging') { const channel = interaction.options.getChannel('channel'); if (!channel) { return interaction.reply({ content: 'Invalid channel.', ephemeral: true }); } data = loadData(); data.loggingChannel = channel.id; saveData(data); await channel.send(`Successfully set logging channel for all Swordcery Key Bot-related messages to <#${channel.id}>.`); return interaction.reply({ content: `Logging channel set to <#${channel.id}>.`, ephemeral: true }); } } // Handle key load confirmation buttons if (interaction.isButton() && (interaction.customId === 'key_load_yes' || interaction.customId === 'key_load_no')) { if (!client.tempKeyLoads || !client.tempKeyLoads[interaction.user.id]) { return interaction.reply({ content: 'No pending key load found.', ephemeral: true }); } if (interaction.customId === 'key_load_no') { delete client.tempKeyLoads[interaction.user.id]; await interaction.update({ content: 'Key load cancelled.', components: [] }); return; } // Yes pressed const keys = client.tempKeyLoads[interaction.user.id]; data = loadData(); data.keys = data.keys || []; data.keys.push(...keys); saveData(data); delete client.tempKeyLoads[interaction.user.id]; await interaction.update({ content: `Loaded ${keys.length} keys into the key vault.`, components: [] }); } // Button interaction for register if (interaction.isButton() && interaction.customId === 'register_button') { if (interaction.message.id !== data.messageId) return; const channel = await interaction.guild.channels.fetch(data.loggingChannel).catch(() => handleFailedLoggingChannel(interaction.user.tag, interaction.user.id, null, 'No logging channel found')); // Already registered? let user = data.users.find(u => u.id === interaction.user.id); if (user && user.key) { await interaction.user.send(formatKeyMessage(user.key)); return interaction.reply({ content: 'Key successfully re-sent! Please check your DMs.', ephemeral: true }); } // Check join date const member = await interaction.guild.members.fetch(interaction.user.id); const joinDate = member.joinedAt; const cutoffDate = data.joinCutoff ? new Date(data.joinCutoff) : null; if (cutoffDate) { const join = joinDate.toISOString().slice(0, 10); const cutoff = cutoffDate.toISOString().slice(0, 10); if (new Date(join).getTime() > new Date(cutoff).getTime()) { return interaction.reply({ content: INELLIGIBLE_MESSAGE, ephemeral: true }); } } // Check for available keys data = loadData(); const keyCheck = data.keys ? data.keys.length : 0; if (keyCheck === 0) { if (channel) { channel.send(formatNoKeysMessage(interaction.user.id, interaction.user.tag)); } return interaction.reply({ content: 'There was an error retrieving your keys. Please reach out to <@404872989188816906> in <#580122303342313473>!', ephemeral: true }); } // Assign and remove key from vault const key = data.keys.shift(); const keyCount = data.keys.length; user = { id: interaction.user.id, key }; data.users.push(user); saveData(data); // Send warnings for low key count if (channel) { if (keyCount <= 10) { channel.send(`⚠️ Only ${keyCount} keys left in the vault!`); } else if (keyCount <= 50) { channel.send(`⚠️ Only ${keyCount} keys left in the vault.`); } } await interaction.reply({ content: 'Successfully claimed key! Check your DMs.', ephemeral: true }); try { await interaction.user.send(formatKeyMessage(key)); } catch (e) { if (channel) { channel.send(formatFailedDMMessage(interaction.user.id, interaction.user.tag)); } return interaction.reply({ content: 'Unable to send DM. Please change your privacy settings or reach out to <@404872989188816906> in <#580122303342313473> for support!', ephemeral: true }); } } }); client.login(TOKEN); // Handle uncaught exceptions process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err); errors.push({ userId: null, timestamp: new Date().toISOString(), message: 'Uncaught Exception', error: JSON.stringify(err, null, 2) }); saveErrorLog(errors); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); errors.push({ userId: null, timestamp: new Date().toISOString(), message: 'Unhandled Rejection', error: JSON.stringify(reason, null, 2) }); saveErrorLog(errors); }); // Handle SIGINT (Ctrl+C) & SIGTERM (kill command) function gracefulShutdown() { console.log('Shutting down gracefully...'); saveData(data); saveErrorLog(errors); client.destroy(); process.exit(0); } process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); let lastErrorCount = errors.length; setInterval(() => { if (errors.length > lastErrorCount) { saveErrorLog(errors); lastErrorCount = errors.length; } }, 10000); // Check every 10 seconds