Added errorh andling, organized commands

This commit is contained in:
2025-07-31 15:08:44 -04:00
parent 54b8c67dd9
commit a0a7f8679a
5 changed files with 299 additions and 95 deletions

254
index.js
View File

@ -11,10 +11,11 @@ import {
EmbedBuilder
} from 'discord.js';
import fs from 'fs';
import { NEWS_CHANNEL_MESSAGE, INELLIGIBLE_MESSAGE, KEY_DM_MESSAGE } from './messages.js';
import { NEWS_CHANNEL_MESSAGE, INELLIGIBLE_MESSAGE, formatNoKeysMessage, formatKeyMessage, formatFailedDMMessage } from './messages.js';
const TOKEN = process.env.DISCORD_TOKEN;
const DATA_FILE = './key_data.json';
const DATA_FILE = process.env.DATA_FILE;
const ERROR_FILE = process.env.ERROR_FILE;
function loadData() {
if (!fs.existsSync(DATA_FILE)) {
@ -28,15 +29,55 @@ 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]
});
client.once(Events.ClientReady, () => {
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}`);
await deleteLatestBotDMs('265334763324178433');
});
client.on(Events.InteractionCreate, async interaction => {
@ -74,58 +115,100 @@ client.on(Events.InteractionCreate, async interaction => {
await interaction.reply({ content: 'Registration message sent!', ephemeral: true });
}
// Slash command: /load
if (interaction.isChatInputCommand() && interaction.commandName === 'load') {
// Slash command: /key
if (interaction.isChatInputCommand() && interaction.commandName === 'keys') {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return interaction.reply({ content: 'You must be an administrator.', ephemeral: true });
}
const file = interaction.options.getAttachment('file');
if (!file) {
return interaction.reply({ content: 'You must attach a file.', ephemeral: true });
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;
}
// Download the file
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);
// /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 });
}
}
const yesButton = new ButtonBuilder()
.setCustomId('load_yes')
.setLabel('Yes')
.setStyle(ButtonStyle.Success);
const noButton = new ButtonBuilder()
.setCustomId('load_no')
.setLabel('No')
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder().addComponents(yesButton, noButton);
// /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 });
}
// Store keys temporarily in memory for this user
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
});
// /keys logging
if (sub === 'logging') {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return interaction.reply({ content: 'You must be an administrator.', ephemeral: true });
}
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 load confirmation buttons
if (interaction.isButton() && (interaction.customId === 'load_yes' || interaction.customId === 'load_no')) {
// Handle key load confirmation buttons
if (interaction.isButton() && (interaction.customId === 'key_load_yes' || interaction.customId === 'key_load_no')) {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return interaction.reply({ content: 'You must be an administrator.', ephemeral: true });
}
if (!client.tempKeyLoads || !client.tempKeyLoads[interaction.user.id]) {
return interaction.reply({ content: 'No pending key load found.', ephemeral: true });
}
if (interaction.customId === 'load_no') {
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(); // Reload in case of concurrent changes
data = loadData();
data.keys = data.keys || [];
data.keys.push(...keys);
saveData(data);
@ -136,12 +219,12 @@ client.on(Events.InteractionCreate, async interaction => {
// 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) {
const message = KEY_DM_MESSAGE.replace('{key}', `https://store.steampowered.com/account/registerkey?key=${user.key}`);
await interaction.user.send(message);
await interaction.user.send(formatKeyMessage(user.key));
return interaction.reply({ content: 'Key successfully re-sent! Please check your DMs.', ephemeral: true });
}
@ -150,10 +233,9 @@ client.on(Events.InteractionCreate, async interaction => {
const joinDate = member.joinedAt;
const cutoffDate = data.joinCutoff ? new Date(data.joinCutoff) : null;
if (cutoffDate) {
// Compare only the date parts (YYYY-MM-DD)
const join = joinDate.toISOString().slice(0, 10);
const cutoff = cutoffDate.toISOString().slice(0, 10);
if (new Date(join) > new Date(cutoff)) {
if (new Date(join).getTime() > new Date(cutoff).getTime()) {
return interaction.reply({
content: INELLIGIBLE_MESSAGE,
ephemeral: true
@ -161,14 +243,14 @@ client.on(Events.InteractionCreate, async interaction => {
}
}
// Assign key
data = loadData(); // Reload in case of concurrent changes
if (!data.keys || data.keys.length === 0) {
// No keys left
// Check for available keys
data = loadData();
const keyCount = data.keys ? data.keys.length : 0;
if (keyCount === 0) {
if (data.loggingChannel) {
const channel = await interaction.guild.channels.fetch(data.loggingChannel).catch(() => console.log(`Failed to fetch logging channel ${data.loggingChannel}`));
if (channel) {
channel.send('WARNING: No more Steam keys available for registration! Please run `/load` to add more keys.');
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 });
@ -180,12 +262,25 @@ client.on(Events.InteractionCreate, async interaction => {
data.users.push(user);
saveData(data);
// Send warnings for low key count
if (data.loggingChannel) {
if (channel) {
if (keyCount <= 10) {
channel.send(`⚠️ Only ${data.keys.length} keys left in the vault!`);
} else if (keyCount <= 50) {
channel.send(`⚠️ Only ${data.keys.length} keys left in the vault.`);
}
}
}
await interaction.reply({ content: 'Successfully claimed key! Check your DMs.', ephemeral: true });
try {
const message = KEY_DM_MESSAGE.replace('{key}', `https://store.steampowered.com/account/registerkey?key=${key}`);
await interaction.user.send(message);
await interaction.user.send(formatKeyMessage(key));
} catch (e) {
return interaction.reply({ content: 'Unable to send DM. Please change your privacy settings or reach out for support!', ephemeral: true });
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 });
}
}
@ -201,8 +296,71 @@ client.on(Events.InteractionCreate, async interaction => {
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 });
}
// Slash command: /getkey
if (interaction.isChatInputCommand() && interaction.commandName === 'getkey') {
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
return interaction.reply({ content: 'You must be an administrator.', ephemeral: true });
}
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 });
}
}
});
client.login(TOKEN);
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