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 `<t:${ts}:${type}>`;
};
// 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);