Skip to content

Commit

Permalink
Use discord sharding, improve error handling in intervals
Browse files Browse the repository at this point in the history
  • Loading branch information
JulianVennen committed Dec 27, 2024
1 parent 998f3b0 commit 8033c39
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 114 deletions.
44 changes: 0 additions & 44 deletions index.js

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"name": "modbot",
"version": "3.6.2",
"description": "Discord Bot for the Aternos Discord server",
"main": "index.js",
"main": "src/index.js",
"type": "module",
"scripts": {
"lint": "eslint",
"start": "node index.js"
"start": "node src/index.js"
},
"repository": {
"type": "git",
Expand Down
12 changes: 10 additions & 2 deletions src/bot/Logger.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import config from './Config.js';
import {Logging} from '@google-cloud/logging';
import bot from './Bot.js';

export class Logger {
#cloudLog;
Expand Down Expand Up @@ -95,9 +96,16 @@ export class Logger {
return Promise.resolve();
}

/**
* @type {import('@google-cloud/logging').LogEntry}
*/
const metadata = {
resource: {
type: 'global'
type: 'global',
labels: {
project_id: this.config.projectId,
shard_id: bot.client.shard?.ids[0],
}
},
severity
};
Expand All @@ -118,4 +126,4 @@ export class Logger {
}
}

export default new Logger();
export default new Logger();
30 changes: 23 additions & 7 deletions src/commands/CommandManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import bot from '../bot/Bot.js';
import {
ApplicationCommandType,
hyperlink,
RESTJSONErrorCodes
REST,
RESTJSONErrorCodes, Routes
} from 'discord.js';
import {AUTOCOMPLETE_OPTIONS_LIMIT} from '../util/apiLimits.js';
import Cache from '../bot/Cache.js';
Expand Down Expand Up @@ -41,10 +42,11 @@ import {replyOrFollowUp} from '../util/interaction.js';
import logger from '../bot/Logger.js';
import SafeSearchCommand from './settings/SafeSearchCommand.js';
import SlashCommandPermissionManagers from '../discord/permissions/SlashCommandPermissionManagers.js';
import config from '../bot/Config.js';

/**
* @import Command from './Command.js';
* @import ExecutableCommand from './ExecutableCommand.js';
* @import {Command} from './Command.js';
* @import {ExecutableCommand} from './ExecutableCommand.js';
*/

const cooldowns = new Cache();
Expand Down Expand Up @@ -103,17 +105,31 @@ export class CommandManager {
}

/**
* register all slash commands
* Register slash commands available in all guilds
* @returns {Promise<void>}
*/
async register() {
async registerGlobalCommands() {
const globalCommands = this.#commands.filter(command => command.isAvailableInAllGuilds());
for (const [id, command] of await bot.client.application.commands.set(this.buildCommands(globalCommands))) {
const rest = new REST().setToken(config.data.authToken);
/** @type {{id: import('discord.js').Snowflake}} */
const application = await rest.get(Routes.currentApplication());
/** @type {{id: import('discord.js').Snowflake, name: string, type: number}[]} */
const data = await rest.put(
Routes.applicationCommands(application.id),
{ body: this.buildCommands(globalCommands) },
);
for (const command of data) {
if (command.type === ApplicationCommandType.ChatInput) {
this.findCommand(command.name).id = id;
this.findCommand(command.name).id = command.id;
}
}
}

/**
* Update which commands are available in which guilds
* @returns {Promise<void>}
*/
async updateGuildCommands() {
for (const guild of bot.client.guilds.cache.values()) {
await this.updateCommandsForGuild(guild);
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/bot/InfoCommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default class InfoCommand extends Command {
.addPairIf(COMMIT, 'Commit', hyperlink(COMMIT, `${GITHUB_REPOSITORY}/tree/${COMMIT}`, 'View on GitHub'))
.addPair('Uptime', formatTime(process.uptime()))
.addPair('Ping', bot.client.ws.ping + 'ms')
.addPairIf(bot.client.shard, 'Shard ID', bot.client.shard.ids.join(',') + ' (Count:' + bot.client.shard.count) + ')'
],
components: [
/** @type {ActionRowBuilder} */
Expand All @@ -121,4 +122,4 @@ export default class InfoCommand extends Command {
getName() {
return 'info';
}
}
}
41 changes: 41 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logger from './bot/Logger.js';
import {ShardingManager} from 'discord.js';
import config from './bot/Config.js';
import database from './bot/Database.js';
import commandManager from './commands/CommandManager.js';

try {
await logger.debug('Loading settings');
await config.load();
await logger.info('Connecting to database');
await database.connect();
await logger.info('Creating database tables');
await database.createTables();
await database.runMigrations();
await logger.notice('Registering slash commands');
await commandManager.registerGlobalCommands();

await logger.info('Spawning shards');
const manager = new ShardingManager( import.meta.dirname + '/shard.js', {
token: config.data.authToken,
totalShards: 2,
});

manager.on('shardCreate', async shard => {
shard.args = [shard.id, manager.totalShards];
await logger.notice(`Launched shard ${shard.id}`);


shard.on("ready", () => {
logger.info(`Shard ${shard.id} connected to Discord's Gateway.`);
});
});
await manager.spawn();
} catch (error) {
try {
await logger.critical('Shard Manager crashed', error);
} catch (e) {
console.error('Failed to send fatal error to monitoring', e);
}
process.exit(1);
}
2 changes: 1 addition & 1 deletion src/interval/CleanupConfirmationInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export default class CleanupConfirmationInterval extends Interval {
const now = Math.floor(Date.now() / 1000);
await database.queryAll('DELETE FROM confirmations WHERE expires <= ?', now);
}
}
}
40 changes: 25 additions & 15 deletions src/interval/TransferMuteToTimeoutInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {PermissionFlagsBits} from 'discord.js';
import {TIMEOUT_DURATION_LIMIT} from '../util/apiLimits.js';
import UserWrapper from '../discord/UserWrapper.js';
import GuildSettings from '../settings/GuildSettings.js';
import bot from '../bot/Bot.js';
import logger from '../bot/Logger.js';

export default class TransferMuteToTimeoutInterval extends Interval {

Expand All @@ -16,25 +18,33 @@ export default class TransferMuteToTimeoutInterval extends Interval {
async run() {
for (const result of await database.queryAll('SELECT * FROM moderations WHERE action = \'mute\' AND active = TRUE AND expireTime IS NOT NULL AND expireTime <= ?',
Math.floor(Date.now() / 1000) + TIMEOUT_DURATION_LIMIT)) {
const guild = await GuildWrapper.fetch(result.guildid),
me = await guild.guild.members.fetchMe();
if (!me.permissions.has(PermissionFlagsBits.ModerateMembers)) {
if (!bot.client.guilds.cache.has(result.guildid)) {
continue;
}

const user = await (new UserWrapper(result.userid)).fetchUser();
if (!user) {
continue;
}
try {
const guild = await GuildWrapper.fetch(result.guildid),
me = await guild.guild.members.fetchMe();
if (!me.permissions.has(PermissionFlagsBits.ModerateMembers)) {
continue;
}

const member = await (new MemberWrapper(user, guild)).fetchMember();
const guildSettings = await GuildSettings.get(guild.guild.id);
if (!member || !member.roles.cache.has(guildSettings.mutedRole)) {
continue;
}
const user = await (new UserWrapper(result.userid)).fetchUser();
if (!user) {
continue;
}

const member = await (new MemberWrapper(user, guild)).fetchMember();
const guildSettings = await GuildSettings.get(guild.guild.id);
if (!member || !member.roles.cache.has(guildSettings.mutedRole)) {
continue;
}

await member.disableCommunicationUntil(parseInt(result.expireTime) * 1000);
await member.roles.remove(guildSettings.mutedRole);
await member.disableCommunicationUntil(parseInt(result.expireTime) * 1000);
await member.roles.remove(guildSettings.mutedRole);
} catch (e) {
await logger.error(`Failed to transfer mute to timeout for user ${result.userid} in guild ${result.guildid}`, e);
}
}
}
}
}
50 changes: 28 additions & 22 deletions src/interval/UnbanInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,43 @@ import logger from '../bot/Logger.js';

export default class UnbanInterval extends Interval {
getInterval() {
return 30*1000;
return 30 * 1000;
}

async run() {
for (const result of await database.queryAll('SELECT * FROM moderations WHERE action = \'ban\' AND active = TRUE AND expireTime IS NOT NULL AND expireTime <= ?',
Math.floor(Date.now()/1000))) {
const guild = await GuildWrapper.fetch(result.guildid);

if (!guild) {
const wrapper = new GuildWrapper({id: result.guildid});
await wrapper.deleteData();
Math.floor(Date.now() / 1000))) {
if (!bot.client.guilds.cache.has(result.guildid)) {
continue;
}

const user = await bot.client.users.fetch(result.userid);
const member = new MemberWrapper(user, guild);
try {
await member.unban('Temporary ban completed!', null, bot.client.user);
}
catch (e) {
if (e.code === RESTJSONErrorCodes.MissingPermissions) {
await database.query('UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = \'ban\'',
guild.guild.id, user.id);
await guild.log(new ErrorEmbed('Missing permissions to unban user!')
.setAuthor({name: user.displayName, iconURL: user.displayAvatarURL()})
.setFooter({text: user.id})
.toMessage(false));
const guild = await GuildWrapper.fetch(result.guildid);

if (!guild) {
const wrapper = new GuildWrapper({id: result.guildid});
await wrapper.deleteData();
continue;
}
else {
await logger.error(`Failed to unmute user ${user.id} in guild ${guild.guild.id}`, e);

const user = await bot.client.users.fetch(result.userid);
const member = new MemberWrapper(user, guild);
try {
await member.unban('Temporary ban completed!', null, bot.client.user);
} catch (e) {
if (e.code === RESTJSONErrorCodes.MissingPermissions) {
await database.query('UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = \'ban\'',
guild.guild.id, user.id);
await guild.log(new ErrorEmbed('Missing permissions to unban user!')
.setAuthor({name: user.displayName, iconURL: user.displayAvatarURL()})
.setFooter({text: user.id})
.toMessage(false));
}
throw e;
}
} catch (e) {
await logger.error(`Failed to unmute user ${result.userid} in guild ${result.guildid}`, e);
}
}
}
}
}
46 changes: 26 additions & 20 deletions src/interval/UnmuteInterval.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,37 @@ export default class UnmuteInterval extends Interval {
async run() {
for (const result of await database.queryAll('SELECT * FROM moderations WHERE action = \'mute\' AND active = TRUE AND expireTime IS NOT NULL AND expireTime <= ?',
Math.floor(Date.now()/1000))) {
const guild = await GuildWrapper.fetch(result.guildid);

if (!guild) {
const wrapper = new GuildWrapper({id: result.guildid});
await wrapper.deleteData();
if (!bot.client.guilds.cache.has(result.guildid)) {
continue;
}

const user = await bot.client.users.fetch(result.userid);
const member = new MemberWrapper(user, guild);
try {
await member.unmute('Temporary mute completed!', null, bot.client.user);
}
catch (e) {
if (e.code === RESTJSONErrorCodes.MissingPermissions) {
await database.query('UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = \'mute\'',
guild.guild.id, user.id);
await guild.log(new ErrorEmbed('Missing permissions to unmute user!')
.setAuthor({name: await member.displayAvatarURL(), iconURL: await member.displayAvatarURL()})
.setFooter({text: user.id})
.toMessage(false));
const guild = await GuildWrapper.fetch(result.guildid);

if (!guild) {
const wrapper = new GuildWrapper({id: result.guildid});
await wrapper.deleteData();
}

const user = await bot.client.users.fetch(result.userid);
const member = new MemberWrapper(user, guild);
try {
await member.unmute('Temporary mute completed!', null, bot.client.user);
}
else {
await logger.error(`Failed to unmute user ${user.id} in guild ${guild.guild.id}`, e);
catch (e) {
if (e.code === RESTJSONErrorCodes.MissingPermissions) {
await database.query('UPDATE moderations SET active = FALSE WHERE active = TRUE AND guildid = ? AND userid = ? AND action = \'mute\'',
guild.guild.id, user.id);
await guild.log(new ErrorEmbed('Missing permissions to unmute user!')
.setAuthor({name: await member.displayAvatarURL(), iconURL: await member.displayAvatarURL()})
.setFooter({text: user.id})
.toMessage(false));
}
throw e;
}
} catch (e) {
await logger.error(`Failed to unmute user ${result.userid} in guild ${result.guildid}`, e);
}
}
}
}
}
Loading

0 comments on commit 8033c39

Please sign in to comment.