const fs = require('fs'); const Discord = require('discord.js'); const config = require('./config.json'); const storage = require('./storage.json'); // Write changes to storage.json const writeStorage = () => { fs.writeFileSync('./storage.json', JSON.stringify(storage, null, 4)); }; // Convert a permissions integer to an array of permission names const permIntToNames = perms => { // Convert to integer perms = parseInt(perms.bitfield ? perms.bitfield : perms); // Loop through permission entries and compile a list const entries = []; for (const [ name, bit ] of Object.entries(Discord.PermissionFlagsBits)) { entries.push({ name, value: parseInt(bit) }); } // Sort the list in descending order entries.sort((a, b) => b.value - a.value); // Check each entry and add it to the list if it's greater than // the remaining number const result = []; for (const entry of entries) { if (perms >= entry.value) { result.push(entry.name); perms -= entry.value; } } // Return the list return result; } const getRichTs = (date = new Date(), type = 'R') => { const ts = Math.floor(date.getTime() / 1000); return ``; }; // Read command files from these folders const dirs = [ './commands', './context/message', './context/user' ]; const embedChannelIds = []; const bot = new Discord.Client({ intents: [ Discord.GatewayIntentBits.Guilds, Discord.GatewayIntentBits.GuildMembers, Discord.GatewayIntentBits.GuildMessages, Discord.GatewayIntentBits.GuildMessageReactions, Discord.GatewayIntentBits.MessageContent ], partials: [ Discord.Partials.User, Discord.Partials.Channel, Discord.Partials.Message, Discord.Partials.Reaction ] }); const sendLog = async(title, fields, type, color = '#77a4ff') => { try { const channelId = config.discord.log_channels[type]; const channel = bot.channels.cache.get(channelId) || await bot.channels.fetch(channelId); channel.send({ embeds: [ new Discord.EmbedBuilder() .setTitle(title) .setFields(fields) .setColor(color) ] }); } catch (error) { console.error(`Failed to send log:`, error); console.error(`Log info:`, JSON.stringify({ type, title, fields, color }, null, 2)); } }; bot.on(Discord.Events.ClientReady, async () => { console.log(`Logged in as ${bot.user.tag}`); // Send embeds if they don't exist const embedFiles = fs.readdirSync('./embeds'); for (const file of embedFiles) { const filePath = `./embeds/${file}`; // Clear require cache and require the file delete require.cache[require.resolve(filePath)]; const data = require(filePath); // Make sure the channel exists const channel = bot.channels.cache.get(data.channel); if (!channel) continue; embedChannelIds.push(data.channel); // Check if a message ID is saved and if it exists const messageId = storage.embeds[filePath]; let msg; if (messageId) { try { msg = await channel.messages.fetch(messageId); } catch (error) { console.log(`Error fetching message ${messageId}:`, error); } } // If the message exists, edit it if (msg) { //msg.edit(data.message); // Otherwise, send a new one } else { channel.send(data.message).then(msg => { storage.embeds[filePath] = msg.id; writeStorage(); }); } } // Run onLoad functions for each command for (const dir of dirs) { if (!fs.existsSync(dir)) continue; const files = fs.readdirSync(dir); for (const file of files) { const data = require(`${dir}/${file}`); if (data.onLoad) data.onLoad(bot); } } // Send log await sendLog('Bot started', [{ name: 'Started', value: `${getRichTs()}` }], 'system', '#7eff77'); }); // Handle new messages bot.on(Discord.Events.MessageCreate, message => { // If the message isn't in an embed channel, ignore it if (!embedChannelIds.includes(message.channel.id)) return; // If the message was sent by a bot, ignore it if (message.author.bot) return; // If the message was sent in an embed channel, delete it if (embedChannelIds.includes(message.channel.id)) { message.delete(); } }); // Log message deletions bot.on(Discord.Events.MessageDelete, message => { if (message.type != Discord.MessageType.Default) return; let content = message.content || '*no content*'; if (content.length > 1024) content = content.substring(0, 1021) + '...'; sendLog('Message deleted', [ { name: 'Message', value: content }, { name: 'Channel', value: `<#${message.channel.id}>`, inline: true }, { name: 'Author', value: `<@${message.author.id}>`, inline: true } ], 'messages', '#ff77a3'); }); // Log message edits bot.on(Discord.Events.MessageUpdate, (oldMessage, newMessage) => { let oldContent = oldMessage.content || '*no content*'; let newContent = newMessage.content || '*no content*'; if (oldContent == newContent) return; if (oldContent.length > 1024) oldContent = oldContent.substring(0, 1021) + '...'; if (newContent.length > 1024) newContent = newContent.substring(0, 1021) + '...'; sendLog('Message edited', [ { name: 'Channel', value: `<#${newMessage.channel.id}>`, inline: true }, { name: 'Author', value: `<@${newMessage.author.id}>`, inline: true }, { name: 'Old message', value: oldContent }, { name: 'New message', value: newContent } ], 'messages'); }); // Log user joins bot.on(Discord.Events.GuildMemberAdd, member => { sendLog('User joined', [ { name: 'User', value: `<@${member.id}>`, inline: true }, { name: 'Account created', value: getRichTs(member.user.createdAt), inline: true } ], 'join_leave', '#7eff77'); }); // Log user leaves bot.on(Discord.Events.GuildMemberRemove, member => { sendLog('User left', [ { name: 'User', value: `<@${member.id}> (\`${member.user.tag}\`)`, inline: true }, { name: 'Joined', value: getRichTs(member.joinedAt), inline: true }, ], 'join_leave', '#ff77a3'); }); // Log channel creation bot.on(Discord.Events.ChannelCreate, channel => { sendLog('Channel created', [ { name: 'Channel', value: `<#${channel.id}>`, inline: true } ], 'channels', '#7eff77'); }); // Log channel deletion bot.on(Discord.Events.ChannelDelete, channel => { sendLog('Channel deleted', [ { name: 'Channel', value: `<#${channel.id}>`, inline: true }, { name: 'Created', value: getRichTs(channel.createdAt), inline: true } ], 'channels', '#ff77a3'); }); // Log channel edits bot.on(Discord.Events.ChannelUpdate, (oldChannel, newChannel) => { const newName = newChannel.name; const oldName = oldChannel.name; const newTopic = newChannel.topic ? newChannel.topic.substring(0, 1021) + '...' : '*no topic*'; const oldTopic = oldChannel.topic ? oldChannel.topic.substring(0, 1021) + '...' : '*no topic*'; const fields = [ { name: 'Channel', value: `<#${newChannel.id}>` } ]; if (newName !== oldName) { fields.push( { name: 'Old name', value: oldName, inline: true }, { name: 'New name', value: newName, inline: true } ); } if (newTopic !== oldTopic) { fields.push( { name: 'Old topic', value: oldTopic }, { name: 'New topic', value: newTopic } ); } sendLog('Channel edited', fields, 'channels'); }); // Log role creation bot.on(Discord.Events.GuildRoleCreate, role => { sendLog('Role created', [ { name: 'Role', value: `<@&${role.id}>`, inline: true } ], 'roles', '#7eff77'); }); // Log role deletion bot.on(Discord.Events.GuildRoleDelete, role => { sendLog('Role deleted', [ { name: 'Name', value: role.name, inline: true }, { name: 'Created', value: getRichTs(role.createdAt), inline: true } ], 'roles', '#ff77a3'); }); // Log role edits bot.on(Discord.Events.GuildRoleUpdate, (oldRole, newRole) => { const newName = newRole.name; const oldName = oldRole.name; const newColor = newRole.hexColor; const oldColor = oldRole.hexColor; const newHoisted = newRole.hoist; const oldHoisted = oldRole.hoist; const newMentionable = newRole.mentionable; const oldMentionable = oldRole.mentionable; const newPerms = permIntToNames(newRole.permissions); const oldPerms = permIntToNames(oldRole.permissions); const addedPerms = newPerms.filter(perm => !oldPerms.includes(perm)); const removedPerms = oldPerms.filter(perm => !newPerms.includes(perm)); const oldPos = oldRole.position; const newPos = newRole.position; const fields = [ { name: 'Role', value: `<@&${newRole.id}>` } ]; if (newName !== oldName) { fields.push( { name: 'Old name', value: oldName, inline: true }, { name: 'New name', value: newName, inline: true } ); } if (newColor !== oldColor) { fields.push( { name: 'Color', value: `\`${oldColor}\` => \`${newColor}\`` } ); } if (newHoisted !== oldHoisted) { fields.push( { name: 'Separate members', value: newHoisted ? 'No => Yes' : 'Yes => No' } ); } if (newMentionable !== oldMentionable) { fields.push( { name: 'Mentionable', value: newMentionable ? 'No => Yes' : 'Yes => No' } ); } if (newPerms !== oldPerms) { if (addedPerms.length) fields.push( { name: 'Added permissions', value: `\`${addedPerms.join('`, `')}\``, inline: true } ); if (removedPerms.length) fields.push( { name: 'Removed permissions', value: `\`${removedPerms.join('`, `')}\``, inline: true } ); } if (newPos !== oldPos) { fields.push( { name: 'Position', value: `${oldPos} => ${oldPos}` } ); } if (fields.length == 1) return; sendLog('Role edited', fields, 'roles'); }); // Log user role list and nickname changes bot.on(Discord.Events.GuildMemberUpdate, (oldMember, newMember) => { const oldRoles = oldMember.roles.cache; const newRoles = newMember.roles.cache; const addedRoles = newRoles.filter(role => !oldRoles.has(role.id)); const removedRoles = oldRoles.filter(role => !newRoles.has(role.id)); const oldNick = oldMember.nickname || '*no nickname*'; const newNick = newMember.nickname || '*no nickname*'; if (addedRoles.size || removedRoles.size) { const fields = [ { name: 'User', value: `<@${newMember.id}>`, inline: true } ]; if (addedRoles.size) { fields.push( { name: 'Added roles', value: addedRoles.map(role => `<@&${role.id}>`).join(', '), inline: true } ); } if (removedRoles.size) { fields.push( { name: 'Removed roles', value: removedRoles.map(role => `<@&${role.id}>`).join(', '), inline: true } ); } sendLog('User roles updated', fields, 'roles'); } if (oldNick !== newNick) { sendLog('User nickname changed', [ { name: 'User', value: `<@${newMember.id}>`, inline: true }, { name: 'Old nickname', value: oldNick, inline: true }, { name: 'New nickname', value: newNick, inline: true } ], 'roles'); } }); // Handle interactions bot.on(Discord.Events.InteractionCreate, async interaction => { // Handle Apps command on messages if (interaction.isMessageContextMenuCommand()) { const data = require(`./context/message/${interaction.commandName}.js`); data.handler(interaction); } // Handle Apps command on users if (interaction.isUserContextMenuCommand()) { const data = require(`./context/user/${interaction.commandName}.js`); data.handler(interaction); } // Handle slash commands if (interaction.isChatInputCommand()) { const data = require(`./commands/${interaction.commandName}.js`); data.handler(interaction); } }); // Handle RSS feeds const rssChecks = {}; setTimeout(async() => { const rssFiles = fs.readdirSync('./rss'); for (const file of rssFiles) { const filePath = `./rss/${file}`; const data = require(filePath); if (rssChecks[file] && (Date.now()-rssChecks[file].lastCheck) < data.frequency_mins * 60 * 1000) continue; const parser = new rssParser(); const rss = await parser.parseURL(data.rss); if (rssChecks[filePath]) { const newEntries = rss.items.filter(item => item.link != rssChecks[filePath].lastItem.link); newEntries.reverse(); const webhook = new Discord.WebhookClient({ url: data.webhook }); for (const entry of newEntries) { webhook.send({ embeds: [ new Discord.EmbedBuilder() .setAuthor({ name: entry.author }) .setTitle(entry.title) .setURL(entry.link) .setDescription(entry.contentSnippet.split('\n')[0]) .setFooter({ text: 'Click the title to read more...' }) .setColor(data.embed_color) ] }); } } rssChecks[filePath] = { lastCheck: Date.now(), lastItem: rss.items[0] }; } }, 1000); bot.login(config.discord.token);