/* eslint-disable no-case-declarations */ /* eslint-disable indent */ // dotenv for handling environment variables const dotenv = require('dotenv'); dotenv.config(); const token = process.env.TOKEN; const statusChannelId = process.env.STATUSCHANNELID; const heartbeatUrl = process.env.HEARTBEAT_URL; const sendHeartbeat = typeof heartbeatUrl === 'string'; // Discord.JS const { Client, GatewayIntentBits, Partials, ActivityType } = require('discord.js'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions, ], partials: [ Partials.Channel, ], }); // Various imports const fn = require('./modules/functions.js'); const strings = require('./data/strings.json'); const dbfn = require('./modules/dbfn.js'); const { GuildInfo } = require('./modules/CustomClasses.js'); const isDev = process.env.DEBUG === "true"; let statusChannel; // Silvanus is being shut down: on launch, leave every guild and exit instead of starting normally. const shuttingDown = true; const farewellMessage = [ 'Silvanus is being permanently shut down. Many of its core features are now built into Grow A Tree, and Discord has restricted bots\' access to message content, which Silvanus needs in order to work.', '', 'It has been so much fun over the last few years. Thank you all for your support, and much love to you all!', '', 'So long and thanks for all the apples 🍎', '-Silvanus and @vfsh' ].join('\n'); // Post the farewell in the guild's reminder channel, falling back to its tree channel. Returns the channel used, if any. async function sendFarewell(guild) { const guildInfo = client.guildInfos.get(guild.id); if (!guildInfo) return; const channelIds = [...new Set([guildInfo.reminderChannelId, guildInfo.treeChannelId].filter(id => id))]; for (const channelId of channelIds) { try { const channel = await client.channels.fetch(channelId); if (channel.guildId !== guild.id) continue; await channel.send({ content: farewellMessage, allowedMentions: { parse: [] } }); return channel; } catch (e) { console.error(`Failed to send farewell to channel ${channelId} in ${guild.name} (${guild.id}): ${e}`); } } } async function leaveAllGuilds() { // Skip the normal guildDelete handler, it depends on state that isn't built in shutdown mode client.removeAllListeners('guildDelete'); statusChannel = await client.channels.fetch(statusChannelId).catch(e => console.error('Unable to fetch status channel', e)); const logStatus = async msg => { console.log(msg); if (statusChannel) await statusChannel.send(msg).catch(e => console.error(e)); }; // Load each guild's configured channels from the database so we know where to say goodbye // If the database can't be read, abort without leaving anything so the shutdown can be retried try { await fn.collectionBuilders.guildInfos(client); } catch (e) { await logStatus(`Shutdown aborted, unable to load guild infos from the database: ${e}`); client.destroy(); process.exit(1); } // Don't leave the server the status channel lives in until the end, so messages can still be sent const guilds = [...client.guilds.cache.values()] .sort((a, b) => (a.id === statusChannel?.guildId) - (b.id === statusChannel?.guildId)); await logStatus(`Shutting down: leaving ${guilds.length} guild(s)...`); let farewellCount = 0; for (const guild of guilds) { // Once the bot leaves the dev server it can't post there anymore, so say goodbye first if (guild.id === statusChannel?.guildId) await logStatus(`Leaving my final guild: ${guild.name} (${guild.id}). Goodbye, and thanks for all the trees. 🌳`); const farewellChannel = await sendFarewell(guild); if (farewellChannel) farewellCount++; const farewellNote = farewellChannel ? `farewell sent in #${farewellChannel.name}` : 'no farewell sent'; await guild.leave() .then(() => { const msg = `I've left a guild: ${guild.name} (${guild.id}) -- ${farewellNote}`; return guild.id === statusChannel?.guildId ? console.log(msg) : logStatus(msg); }) .catch(e => logStatus(`Failed to leave guild: ${guild.name} (${guild.id}) -- ${e}`)); } console.log(`Finished leaving guilds, sent ${farewellCount} farewell(s). Goodbye!`); client.destroy(); process.exit(0); } client.once('ready', async () => { if (shuttingDown) return leaveAllGuilds(); fn.collectionBuilders.slashCommands(client); fn.collectionBuilders.dotCommands(client); fn.collectionBuilders.setvalidCommands(client); await fn.collectionBuilders.guildInfos(client); await fn.collectionBuilders.messageCollectors(client); const serverCount = client.guilds.cache.size; // checkRateLimits(); console.log('Ready!'); client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching }); statusChannel = await client.channels.fetch(statusChannelId); if (isDev == 'false') { statusChannel.send(`${new Date().toISOString()} -- \nStartup Sequence Complete <@481933290912350209>`); } // Heartbeat Timer if (sendHeartbeat) { setInterval(() => { fn.sendHeartbeat(heartbeatUrl); }, 30000); if (isDev) console.log("Heartbeat interval set."); } else { if (isDev) console.log("No heartbeat URL set, will not send heartbeats for uptime monitoring."); } }); // slash-commands client.on('interactionCreate', async interaction => { try { if (interaction.isCommand()) { if (isDev) { // console.log(interaction); } const { commandName } = interaction; if (client.slashCommands.has(commandName)) { client.slashCommands.get(commandName).execute(interaction); } else { interaction.reply('Sorry, I don\'t have access to that command.'); console.error('Slash command attempted to run but not found: ' + commandName); } } if (interaction.isButton()) { switch (interaction.component.customId) { case 'refresh': // console.log(JSON.stringify(interaction)); await fn.refresh(interaction).catch(err => { interaction.channel.send(fn.builders.errorEmbed("Oops! Something went wrong!")); }); break; case 'deleteping': if (interaction.message.deletable) { await interaction.message.delete().catch(err => { // console.error(err); }); } break; case 'waterpingrole': const waterPingStatus = await fn.buttonHandlers.waterPing(interaction); await interaction.reply(waterPingStatus).catch(e => console.error(e)); break; case 'fruitpingrole': const fruitPingStatus = await fn.buttonHandlers.fruitPing(interaction); await interaction.reply(fruitPingStatus).catch(e => console.error(e)); break; default: break; } } } catch(err) { if (err === "Guild doesn't exist in database!") { interaction.channel.send(fn.builders.errorEmbed(strings.error.noGuild)); console.error(err); } else { interaction.channel.send("Oops! An error occurred... Sorry about that, please contact my owner @vfsh if this keeps happening."); console.error(err); } } }); client.on('messageUpdate', async (oldMessage, message) => { await fn.messages.updateHandler(message).catch(async e => { switch (e) { case strings.error.noCompareMessage: await message.channel.send(strings.error.noCompareMessage); break; default: break; } }); }); client.on('messageCreate', async message => { await fn.messages.updateHandler(message).catch(e => console.error(e)); // Dot Command Handling // Some basic checking to prevent running unnecessary code if (message.author.bot) return; // Break the message down into its components and analyze it const commandData = fn.dotCommands.getCommandData(message); // if (isDev) console.log(commandData); if (commandData.isValid && commandData.isCommand) { try { client.dotCommands.get(commandData.command).execute(message, commandData); } catch (error) { console.error(error); message.reply('There was an error trying to execute that command.'); } } return; }); client.on('guildCreate', async guild => { const serverCount = client.guilds.cache.size; client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching }); await statusChannel.send(`I've been added to a new guild: ${guild.name} (${guild.id})`); const guildInfo = new GuildInfo() .setIds(guild.id, guild.ownerId); const setBasicQuery = guildInfo.queryBuilder("setBasic"); await dbfn.setGuildInfo(setBasicQuery).catch(e => console.error(e)); }); client.on('guildDelete', async guild => { const serverCount = client.guilds.cache.size; client.user.setActivity({ name: `${serverCount} trees grow.`, type: ActivityType.Watching }); await statusChannel.send(`I've been removed from a guild: ${guild.name} (${guild.id})`); if (client.guildInfos.has(guild.id)) { let guildInfo = client.guildInfos.get(guild.id); guildInfo.setReminders(undefined, undefined, undefined, undefined, false); const setRemindersQuery = guildInfo.queryBuilder("setReminders"); await dbfn.setGuildInfo(setRemindersQuery); } }); async function checkRateLimits(hi) { const axios = require('axios'); // Make a GET request to the Discord API await axios.get('https://discord.com/api/v10/users/@me', { headers: { 'Authorization': `Bot ${token}` } }).then(response => { // Get the rate limit headers const remaining = response.headers['x-ratelimit-remaining']; const reset = response.headers['x-ratelimit-reset']; // Log the rate limit headers console.log(`Remaining requests: ${remaining}`); console.log(`Reset time (Unix epoch seconds): ${reset}`); }).catch(error => { console.error(error); }); await fn.sleep(500).then(async () =>{ await checkRateLimits(); }) } process.on('unhandledRejection', error => { console.error('Unhandled promise rejection (pls dont break up with me):', error); }); client.login(token);