[READ-ONLY] Mirror of https://github.com/maybeanerd/MagiBot. MagiBot - the community building discord bot that adds joinsounds to your voice channels! discord.gg/2Evcf4T
bot bots community discord gaming hacktoberfest sound
1

Configure Feed

Select the types of activity you want to include in your feed.

fix: set indenting to use spaces; migrate info to slash commands

Sebastian Di Luzio (Jun 5, 2022, 3:11 PM +0200) b53c715e dace7a96

+1817 -1818
+1 -2
.eslintrc.json
··· 13 13 "@typescript-eslint/no-unused-vars": "error", 14 14 "no-extra-semi": 0, 15 15 "semi": 2, 16 - "no-tabs": ["error", { "allowIndentationTabs": true }], 17 - "indent": ["warn", "tab"], 16 + "indent": ["error", 2], 18 17 "quotes": [ 19 18 "error", 20 19 "single",
+69 -69
src/applicationCommandHandler.ts
··· 7 7 import { catchErrorOnDiscord } from './sendToMyDiscord'; 8 8 import { isBlacklistedUser } from './dbHelpers'; 9 9 import { 10 - commandAllowed, 11 - printCommandChannels, 12 - usageUp, 10 + commandAllowed, 11 + printCommandChannels, 12 + usageUp, 13 13 } from './commandHandler'; 14 14 import { applicationCommands } from './commands/applicationCommands'; 15 15 16 16 async function catchError( 17 - error: Error, 18 - interaction: Discord.CommandInteraction, 17 + error: Error, 18 + interaction: Discord.CommandInteraction, 19 19 ) { 20 - console.error( 21 - `Caught:\n${error.stack}\nin command ${interaction.commandName} ${interaction.options}`, 22 - ); 23 - await catchErrorOnDiscord( 24 - `**Command:** ${interaction.commandName} ${interaction.options}\n**Caught Error:**\n\`\`\`${error.stack}\`\`\``, 25 - ); 20 + console.error( 21 + `Caught:\n${error.stack}\nin command ${interaction.commandName} ${interaction.options}`, 22 + ); 23 + await catchErrorOnDiscord( 24 + `**Command:** ${interaction.commandName} ${interaction.options}\n**Caught Error:**\n\`\`\`${error.stack}\`\`\``, 25 + ); 26 26 27 - interaction.reply(`Something went wrong while using ${ 28 - interaction.commandName 29 - }. The devs have been automatically notified. 27 + interaction.reply(`Something went wrong while using ${ 28 + interaction.commandName 29 + }. The devs have been automatically notified. 30 30 If you can reproduce this, consider using \`/bugreport\` or join the support discord (link via \`${ 31 - interaction.guild ? PREFIXES.get(interaction.guild.id) : 'k' 31 + interaction.guild ? PREFIXES.get(interaction.guild.id) : 'k' 32 32 }.info\`) to tell us exactly how.`); 33 33 } 34 34 35 35 export async function checkApplicationCommand( 36 - interaction: Discord.CommandInteraction, 36 + interaction: Discord.CommandInteraction, 37 37 ) { 38 - if (!(interaction.member && interaction.guild && interaction.guild.me)) { 39 - // check for valid message 40 - console.error('Invalid interaction received:', interaction); 41 - return; 42 - } 43 - // ignore blacklisted users 44 - if ( 45 - await isBlacklistedUser(interaction.member.user.id, interaction.guild.id) 46 - ) { 47 - // do nothing 48 - return; 49 - } 50 - try { 51 - Statcord.ShardingClient.postCommand( 52 - interaction.commandName, 53 - interaction.member.user.id, 54 - bot, 55 - ); 56 - const command = applicationCommands[interaction.commandName]; 57 - if (command) { 58 - const { permissions } = command; 59 - if ( 60 - !(await commandAllowed(interaction.guild.id, interaction.channel?.id)) 61 - ) { 62 - await interaction.reply({ 63 - content: `Commands aren't allowed in <#${ 64 - interaction.channel?.id 65 - }>. Use them in ${await printCommandChannels( 66 - interaction.guild.id, 67 - )}. If you're an admin use \`/help\` to see how you can change that.`, 68 - ephemeral: true, 69 - }); 70 - return; 71 - } 72 - // check for all needed permissions 73 - const botPermissions = ( 38 + if (!(interaction.member && interaction.guild && interaction.guild.me)) { 39 + // check for valid message 40 + console.error('Invalid interaction received:', interaction); 41 + return; 42 + } 43 + // ignore blacklisted users 44 + if ( 45 + await isBlacklistedUser(interaction.member.user.id, interaction.guild.id) 46 + ) { 47 + // do nothing 48 + return; 49 + } 50 + try { 51 + Statcord.ShardingClient.postCommand( 52 + interaction.commandName, 53 + interaction.member.user.id, 54 + bot, 55 + ); 56 + const command = applicationCommands[interaction.commandName]; 57 + if (command) { 58 + const { permissions } = command; 59 + if ( 60 + !(await commandAllowed(interaction.guild.id, interaction.channel?.id)) 61 + ) { 62 + await interaction.reply({ 63 + content: `Commands aren't allowed in <#${ 64 + interaction.channel?.id 65 + }>. Use them in ${await printCommandChannels( 66 + interaction.guild.id, 67 + )}. If you're an admin use \`/help\` to see how you can change that.`, 68 + ephemeral: true, 69 + }); 70 + return; 71 + } 72 + // check for all needed permissions 73 + const botPermissions = ( 74 74 interaction.channel as Discord.TextChannel 75 - ).permissionsFor(interaction.guild.me); 76 - if (!botPermissions.has(permissions)) { 77 - await interaction.reply( 78 - `I am missing permissions for this command. I require all of the following:\n${permissions}`, 79 - ); 80 - } 81 - if (command.isSlow) { 82 - // allow slow commands to have more time to respond 83 - await interaction.deferReply(); 84 - } 85 - // actually use the command 86 - await command.run(interaction); 87 - await usageUp(interaction.member.user.id, interaction.guild.id); 88 - } 89 - } catch (err) { 90 - catchError(err as Error, interaction); 91 - } 75 + ).permissionsFor(interaction.guild.me); 76 + if (!botPermissions.has(permissions)) { 77 + await interaction.reply( 78 + `I am missing permissions for this command. I require all of the following:\n${permissions}`, 79 + ); 80 + } 81 + if (command.isSlow) { 82 + // allow slow commands to have more time to respond 83 + await interaction.deferReply(); 84 + } 85 + // actually use the command 86 + await command.run(interaction); 87 + await usageUp(interaction.member.user.id, interaction.guild.id); 88 + } 89 + } catch (err) { 90 + catchError(err as Error, interaction); 91 + } 92 92 }
+106 -106
src/bot.ts
··· 1 1 import Discord, { 2 - Client, DiscordAPIError, Guild, Intents, 2 + Client, DiscordAPIError, Guild, Intents, 3 3 } from 'discord.js'; 4 4 import { handle } from 'blapi'; 5 5 import { generateDependencyReport } from '@discordjs/voice'; 6 6 import config from './configuration'; 7 7 import { 8 - PREFIX, 9 - PREFIXES, 10 - TOKEN, 11 - setUser, 12 - resetPrefixes, 8 + PREFIX, 9 + PREFIXES, 10 + TOKEN, 11 + setUser, 12 + resetPrefixes, 13 13 } from './shared_assets'; 14 14 // eslint-disable-next-line import/no-cycle 15 15 import { checkCommand } from './commandHandler'; ··· 25 25 console.log(generateDependencyReport()); 26 26 27 27 async function initializePrefixes(bot: Client) { 28 - resetPrefixes(); 29 - const guilds = bot.guilds.cache; 30 - asyncForEach(guilds, async (G) => { 31 - PREFIXES.set(G.id, await getPrefix(G.id)); 32 - }); 28 + resetPrefixes(); 29 + const guilds = bot.guilds.cache; 30 + asyncForEach(guilds, async (G) => { 31 + PREFIXES.set(G.id, await getPrefix(G.id)); 32 + }); 33 33 } 34 34 35 35 const intents = [ 36 - Intents.FLAGS.GUILDS, 37 - // Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS, 38 - Intents.FLAGS.GUILD_INTEGRATIONS, 39 - /* | 'GUILD_WEBHOOKS' 36 + Intents.FLAGS.GUILDS, 37 + // Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS, 38 + Intents.FLAGS.GUILD_INTEGRATIONS, 39 + /* | 'GUILD_WEBHOOKS' 40 40 | 'GUILD_INVITES' */ 41 - Intents.FLAGS.GUILD_VOICE_STATES, 42 - // | 'GUILD_PRESENCES' 43 - Intents.FLAGS.GUILD_MESSAGES, 44 - Intents.FLAGS.GUILD_MESSAGE_REACTIONS, 45 - // | 'GUILD_MESSAGE_TYPING' 46 - Intents.FLAGS.DIRECT_MESSAGES, 47 - Intents.FLAGS.DIRECT_MESSAGE_REACTIONS, 48 - // | 'DIRECT_MESSAGE_TYPING'; 41 + Intents.FLAGS.GUILD_VOICE_STATES, 42 + // | 'GUILD_PRESENCES' 43 + Intents.FLAGS.GUILD_MESSAGES, 44 + Intents.FLAGS.GUILD_MESSAGE_REACTIONS, 45 + // | 'GUILD_MESSAGE_TYPING' 46 + Intents.FLAGS.DIRECT_MESSAGES, 47 + Intents.FLAGS.DIRECT_MESSAGE_REACTIONS, 48 + // | 'DIRECT_MESSAGE_TYPING'; 49 49 ]; 50 50 51 51 export const bot = new Client({ intents }); 52 52 // post to the APIs every 30 minutes 53 53 if (config.blapis) { 54 - handle(bot, config.blapis, 30); 54 + handle(bot, config.blapis, 30); 55 55 } 56 56 process.on('uncaughtException', async (err) => { 57 - console.error(`Uncaught Exception:\n${err.stack ? err.stack : err}`); 58 - await catchErrorOnDiscord( 59 - `**Uncaught Exception:**\n\`\`\`${err.stack ? err.stack : err}\`\`\``, 60 - ); 57 + console.error(`Uncaught Exception:\n${err.stack ? err.stack : err}`); 58 + await catchErrorOnDiscord( 59 + `**Uncaught Exception:**\n\`\`\`${err.stack ? err.stack : err}\`\`\``, 60 + ); 61 61 }); 62 62 process.on( 63 - 'unhandledRejection', 64 - async (err: any /* to fix weird type issues */) => { 65 - console.error(`Unhandled promise rejection:\n${err}`); 66 - if (err) { 67 - if (err instanceof DiscordAPIError) { 68 - await catchErrorOnDiscord( 69 - `**DiscordAPIError (${err.method || 'NONE'}):**\n\`\`\`${ 70 - err.message 71 - }\`\`\`\`\`\`${err.path ? err.path.substring(0, 1200) : ''}\`\`\``, 72 - ); 73 - } else { 74 - await catchErrorOnDiscord( 75 - `**Outer Unhandled promise rejection:**\n\`\`\`${err}\`\`\`\`\`\`${ 76 - err.stack ? err.stack.substring(0, 1200) : '' 77 - }\`\`\``, 78 - ); 79 - } 80 - } 81 - }, 63 + 'unhandledRejection', 64 + async (err: any /* to fix weird type issues */) => { 65 + console.error(`Unhandled promise rejection:\n${err}`); 66 + if (err) { 67 + if (err instanceof DiscordAPIError) { 68 + await catchErrorOnDiscord( 69 + `**DiscordAPIError (${err.method || 'NONE'}):**\n\`\`\`${ 70 + err.message 71 + }\`\`\`\`\`\`${err.path ? err.path.substring(0, 1200) : ''}\`\`\``, 72 + ); 73 + } else { 74 + await catchErrorOnDiscord( 75 + `**Outer Unhandled promise rejection:**\n\`\`\`${err}\`\`\`\`\`\`${ 76 + err.stack ? err.stack.substring(0, 1200) : '' 77 + }\`\`\``, 78 + ); 79 + } 80 + } 81 + }, 82 82 ); 83 83 84 84 // fires on startup and on reconnect 85 85 let justStartedUp = true; 86 86 bot.on('ready', async () => { 87 - if (!bot.user) { 88 - throw new Error('FATAL Bot has no user.'); 89 - } 90 - setUser(bot.user); // give user ID to other code 91 - if (justStartedUp) { 92 - startUp(bot); 93 - justStartedUp = false; 94 - } 95 - bot.user.setPresence({ 96 - activities: [ 97 - { 98 - name: `${PREFIX}.help`, 99 - type: 'WATCHING', 100 - url: 'https://bots.ondiscord.xyz/bots/384820232583249921', 101 - }, 102 - ], 103 - status: 'online', 104 - }); 105 - initializePrefixes(bot); 87 + if (!bot.user) { 88 + throw new Error('FATAL Bot has no user.'); 89 + } 90 + setUser(bot.user); // give user ID to other code 91 + if (justStartedUp) { 92 + startUp(bot); 93 + justStartedUp = false; 94 + } 95 + bot.user.setPresence({ 96 + activities: [ 97 + { 98 + name: `${PREFIX}.help`, 99 + type: 'WATCHING', 100 + url: 'https://bots.ondiscord.xyz/bots/384820232583249921', 101 + }, 102 + ], 103 + status: 'online', 104 + }); 105 + initializePrefixes(bot); 106 106 }); 107 107 108 108 bot.on('message', async (message: Discord.Message) => { 109 - try { 110 - await checkCommand(message); 111 - } catch (err) { 112 - console.error(err); 113 - } 109 + try { 110 + await checkCommand(message); 111 + } catch (err) { 112 + console.error(err); 113 + } 114 114 }); 115 115 116 116 bot.on('interactionCreate', async (interaction) => { 117 - if (!interaction.isCommand()) { 118 - return; 119 - } 120 - try { 121 - await checkApplicationCommand(interaction); 122 - } catch (err) { 123 - console.error(err); 124 - } 117 + if (!interaction.isCommand()) { 118 + return; 119 + } 120 + try { 121 + await checkApplicationCommand(interaction); 122 + } catch (err) { 123 + console.error(err); 124 + } 125 125 }); 126 126 127 127 async function guildPrefixStartup(guild: Guild) { 128 - try { 129 - await checkGuild(guild.id); 130 - PREFIXES.set(guild.id, await getPrefix(guild.id)); 131 - } catch (err) { 132 - console.error(err); 133 - } 128 + try { 129 + await checkGuild(guild.id); 130 + PREFIXES.set(guild.id, await getPrefix(guild.id)); 131 + } catch (err) { 132 + console.error(err); 133 + } 134 134 } 135 135 136 136 bot.on('guildCreate', async (guild) => { 137 - if (guild.available) { 138 - await guildPrefixStartup(guild); 139 - const owner = await guild.fetchOwner(); 140 - if (owner) { 141 - owner 142 - .send( 143 - `Hi there ${owner.displayName}.\nThanks for adding me to your server! If you have any need for help or want to help develop the bot by reporting bugs and requesting features, just join https://discord.gg/2Evcf4T\n\nTo setup the bot, use \`${PREFIX}:help setup\`.\nYou should:\n\t- setup an admin role, as only you and users with administrative permission are able to use admin commands (\`${PREFIX}:setup admin @role\`)\n\t- add some text channels where users can use the bot (\`${PREFIX}:setup command\`)\n\t- add voice channels in which the bot is allowed to ` 137 + if (guild.available) { 138 + await guildPrefixStartup(guild); 139 + const owner = await guild.fetchOwner(); 140 + if (owner) { 141 + owner 142 + .send( 143 + `Hi there ${owner.displayName}.\nThanks for adding me to your server! If you have any need for help or want to help develop the bot by reporting bugs and requesting features, just join https://discord.gg/2Evcf4T\n\nTo setup the bot, use \`${PREFIX}:help setup\`.\nYou should:\n\t- setup an admin role, as only you and users with administrative permission are able to use admin commands (\`${PREFIX}:setup admin @role\`)\n\t- add some text channels where users can use the bot (\`${PREFIX}:setup command\`)\n\t- add voice channels in which the bot is allowed to ` 144 144 + `join to use joinsounds (\`${PREFIX}:setup join\`)\n\t- add a notification channel where bot updates and information will be posted (\`${PREFIX}:setup notification\`)\n\nTo make sure the bot can use all its functions consider giving it a role with administrative rights, if you have not done so yet in the invitation.\n\nThanks for being part of this project,\nBasti aka. the MagiBot Dev`, 145 - ) 146 - .catch(() => {}); 147 - } 148 - await sendJoinEvent( 149 - `:white_check_mark: joined **${guild.name}**: "${guild.preferredLocale}" (${guild.memberCount} users, ID: ${guild.id})\nOwner is: <@${guild.ownerId}> (ID: ${guild.ownerId})`, 150 - ); 151 - } 145 + ) 146 + .catch(() => {}); 147 + } 148 + await sendJoinEvent( 149 + `:white_check_mark: joined **${guild.name}**: "${guild.preferredLocale}" (${guild.memberCount} users, ID: ${guild.id})\nOwner is: <@${guild.ownerId}> (ID: ${guild.ownerId})`, 150 + ); 151 + } 152 152 }); 153 153 154 154 bot.on('guildDelete', async (guild) => { 155 - if (guild.available) { 156 - await sendJoinEvent( 157 - `:x: left ${guild.name} (${guild.memberCount} users, ID: ${guild.id})`, 158 - ); 159 - } 155 + if (guild.available) { 156 + await sendJoinEvent( 157 + `:x: left ${guild.name} (${guild.memberCount} users, ID: ${guild.id})`, 158 + ); 159 + } 160 160 }); 161 161 162 162 bot.on('error', (err) => { 163 - console.error(err); 163 + console.error(err); 164 164 }); 165 165 166 166 bot.on('voiceStateUpdate', onVoiceStateChange); 167 167 168 168 bot.on('disconnect', () => { 169 - console.log('Disconnected!'); 169 + console.log('Disconnected!'); 170 170 }); 171 171 172 172 bot.login(TOKEN); // connect to discord
+1 -1
src/commandCollector.ts
··· 2 2 import { magibotCommand } from './types/command'; 3 3 4 4 export const commands: { [k: string]: magibotCommand } = { 5 - help, 5 + help, 6 6 };
+201 -203
src/commandHandler.ts
··· 2 2 import Statcord from 'statcord.js'; 3 3 import { vote } from './commands/old/vote'; 4 4 // eslint-disable-next-line import/no-cycle 5 - import { inf as info } from './commands/old/info'; 6 - // eslint-disable-next-line import/no-cycle 7 5 import { queue as _queue } from './commands/old/@queue'; 8 6 import { setup as _setup } from './commands/old/@setup'; 9 7 // we allow this cycle once, as the help command also needs to list itself 10 8 import { help } from './commands/old/help'; // eslint-disable-line import/no-cycle 11 9 12 10 import { 13 - PREFIXES, OWNERID, DELETE_COMMANDS, user, 11 + PREFIXES, OWNERID, DELETE_COMMANDS, user, 14 12 } from './shared_assets'; 15 13 // eslint-disable-next-line import/no-cycle 16 14 import { catchErrorOnDiscord } from './sendToMyDiscord'; 17 15 import { magibotCommand } from './types/command'; 18 16 import { 19 - isBlacklistedUser, 20 - getCommandChannels, 21 - getUser, 22 - isAdmin, 17 + isBlacklistedUser, 18 + getCommandChannels, 19 + getUser, 20 + isAdmin, 23 21 } from './dbHelpers'; 24 22 // eslint-disable-next-line import/no-cycle 25 23 import { bot } from './bot'; 26 24 import { asyncWait, notifyAboutSlashCommand } from './helperFunctions'; 27 25 28 26 export const commands: { [k: string]: magibotCommand } = { 29 - _queue, 30 - _setup, 27 + _queue, 28 + _setup, 31 29 32 - help, 33 - vote, 34 - info, 30 + help, 31 + vote, 35 32 }; 36 33 37 34 const migratedCommands = new Map([ 38 - ['rfact', 'randomfact'], 39 - ['bug', 'bugreport'], 40 - ['invite', 'invite'], 41 - ['ping', 'ping'], 42 - ['roll', 'roll'], 43 - ['salt', 'salt'], 44 - ['profile', 'profile'], 45 - ['_salt', 'salt'], // admin command 46 - ['_sound', 'joinsound'], // admin command 47 - ['sound', 'joinsound'], 35 + ['rfact', 'randomfact'], 36 + ['bug', 'bugreport'], 37 + ['invite', 'invite'], 38 + ['ping', 'ping'], 39 + ['roll', 'roll'], 40 + ['salt', 'salt'], 41 + ['profile', 'profile'], 42 + ['_salt', 'salt'], // admin command 43 + ['_sound', 'joinsound'], // admin command 44 + ['sound', 'joinsound'], 45 + ['info', 'info'], 48 46 ]); 49 47 50 48 async function sendMigrationMessageIfComandHasBeenMigrated( 51 - message: Discord.Message, 52 - commandName: string, 53 - isAdminCommand = false, 49 + message: Discord.Message, 50 + commandName: string, 51 + isAdminCommand = false, 54 52 ) { 55 - const migratedCommand = migratedCommands.get(commandName); 56 - if (migratedCommand) { 57 - await notifyAboutSlashCommand( 58 - message, 59 - isAdminCommand ? `admin ${migratedCommand}` : migratedCommand, 60 - ); 61 - } 53 + const migratedCommand = migratedCommands.get(commandName); 54 + if (migratedCommand) { 55 + await notifyAboutSlashCommand( 56 + message, 57 + isAdminCommand ? `admin ${migratedCommand}` : migratedCommand, 58 + ); 59 + } 62 60 } 63 61 64 62 async function catchError(error: Error, msg: Discord.Message, command: string) { 65 - console.error( 66 - `Caught:\n${error.stack}\nin command ${command} ${msg.content}`, 67 - ); 68 - await catchErrorOnDiscord( 69 - `**Command:** ${command} ${msg.content}\n**Caught Error:**\n\`\`\`${error.stack}\`\`\``, 70 - ); 63 + console.error( 64 + `Caught:\n${error.stack}\nin command ${command} ${msg.content}`, 65 + ); 66 + await catchErrorOnDiscord( 67 + `**Command:** ${command} ${msg.content}\n**Caught Error:**\n\`\`\`${error.stack}\`\`\``, 68 + ); 71 69 72 - msg.reply(`something went wrong while using ${command}. The devs have been automatically notified. 73 - If you can reproduce this, consider using \`/bugreport\` or join the support discord (link via \`${ 74 - msg.guild ? PREFIXES.get(msg.guild.id) : 'k' 70 + msg.reply(`something went wrong while using ${command}. The devs have been automatically notified. 71 + If you can reproduce this, consider using \`/bugreport\` or join the support discord (link via \`${ 72 + msg.guild ? PREFIXES.get(msg.guild.id) : 'k' 75 73 }.info\`) to tell us exactly how.`); 76 74 } 77 75 78 76 export async function commandAllowed(guildID: string, channelId?: string) { 79 - if (!channelId) { 80 - return false; 81 - } 82 - const channels = await getCommandChannels(guildID); 83 - return channels.length === 0 || channels.includes(channelId); 77 + if (!channelId) { 78 + return false; 79 + } 80 + const channels = await getCommandChannels(guildID); 81 + return channels.length === 0 || channels.includes(channelId); 84 82 } 85 83 86 84 export async function usageUp(userid: string, guildID: string) { 87 - const usr = await getUser(userid, guildID); 88 - const updateval = usr.botusage + 1; 89 - usr.botusage = updateval; 90 - await usr.save(); 85 + const usr = await getUser(userid, guildID); 86 + const updateval = usr.botusage + 1; 87 + usr.botusage = updateval; 88 + await usr.save(); 91 89 } 92 90 93 91 export async function printCommandChannels(guildID: string) { 94 - const channels = await getCommandChannels(guildID); 95 - let out = ''; 96 - channels.forEach((channel: string) => { 97 - out += ` <#${channel}>`; 98 - }); 99 - return out; 92 + const channels = await getCommandChannels(guildID); 93 + let out = ''; 94 + channels.forEach((channel: string) => { 95 + out += ` <#${channel}>`; 96 + }); 97 + return out; 100 98 } 101 99 102 100 const userCooldowns = new Set<string>(); 103 101 104 102 export async function checkCommand(message: Discord.Message) { 105 - if (!(message.author && message.guild && message.guild.me)) { 106 - // check for valid message 107 - console.error('Invalid message received:', message); 108 - return; 109 - } 110 - // only read guild messages from non-bots 111 - if (!(!message.author.bot && message.channel.type === 'GUILD_TEXT')) { 112 - return; 113 - } 114 - let isMention: boolean; 115 - if ( 116 - message.content.startsWith(`<@${user().id}>`) 103 + if (!(message.author && message.guild && message.guild.me)) { 104 + // check for valid message 105 + console.error('Invalid message received:', message); 106 + return; 107 + } 108 + // only read guild messages from non-bots 109 + if (!(!message.author.bot && message.channel.type === 'GUILD_TEXT')) { 110 + return; 111 + } 112 + let isMention: boolean; 113 + if ( 114 + message.content.startsWith(`<@${user().id}>`) 117 115 || message.content.startsWith(`<@!${user().id}>`) 118 - ) { 119 - isMention = true; 120 - } else if (message.content.startsWith(PREFIXES.get(message.guild.id)!)) { 121 - isMention = false; 122 - } else { 123 - return; 124 - } 125 - // ignore blacklisted users 126 - if (await isBlacklistedUser(message.author.id, message.guild.id)) { 127 - // we dont delete the message because this would delete everything that starts with the prefix 128 - /* msg.delete(); */ 129 - return; 130 - } 131 - let command: string; 132 - let content: string; 133 - if (isMention) { 134 - command = message.content.split(' ')[1]; // eslint-disable-line prefer-destructuring 135 - content = message.content 136 - .split(' ') 137 - .splice(2, message.content.split(' ').length) 138 - .join(' '); 139 - command = `.${command}`; 140 - } else { 141 - command = message.content 142 - .substring(PREFIXES.get(message.guild.id)!.length, message.content.length) 143 - .split(' ')[0] 144 - .toLowerCase(); 145 - // delete prefix and command 146 - content = message.content.slice( 147 - command.length + PREFIXES.get(message.guild.id)!.length, 148 - ); 149 - content = content.replace(/^\s+/g, ''); // delete leading spaces 150 - } 151 - if (command) { 152 - let commandVal: string; 153 - const pre = command.charAt(0); 154 - const myPerms = (message.channel as Discord.TextChannel).permissionsFor( 155 - message.guild.me, 156 - ); 157 - if (pre === '.') { 158 - command = command.slice(1); 159 - commandVal = command; 160 - } else if (pre === ':') { 161 - command = `_${command.slice(1)}`; 162 - commandVal = command.slice(1); 163 - // Check if its an admin command 164 - // if not you're allowed to use the normal version as admin (in any channel) 165 - if (!commands[command]) { 166 - command = command.slice(1); 167 - } 168 - // Check if the command exists, to not just spam k: msgs 169 - if (!commands[command]) { 170 - await sendMigrationMessageIfComandHasBeenMigrated( 171 - message, 172 - command, 173 - true, 174 - ); 175 - return; 176 - } 177 - if ( 178 - !(message.member && (await isAdmin(message.guild.id, message.member))) 179 - ) { 180 - if (myPerms) { 181 - if (myPerms.has('MANAGE_MESSAGES')) { 182 - message.delete(); 183 - } 184 - if (myPerms.has('SEND_MESSAGES')) { 185 - const reply = await message.reply( 186 - "you're not allowed to use this command.", 187 - ); 188 - await asyncWait(5000); 189 - await reply.delete(); 190 - } 191 - } 192 - return; 193 - } 194 - } else { 195 - return; 196 - } 197 - if (!commands[command]) { 198 - await sendMigrationMessageIfComandHasBeenMigrated(message, command); 199 - return; 200 - } 201 - if (!commands[command].dev || message.author.id === OWNERID) { 202 - if ( 203 - pre === ':' 116 + ) { 117 + isMention = true; 118 + } else if (message.content.startsWith(PREFIXES.get(message.guild.id)!)) { 119 + isMention = false; 120 + } else { 121 + return; 122 + } 123 + // ignore blacklisted users 124 + if (await isBlacklistedUser(message.author.id, message.guild.id)) { 125 + // we dont delete the message because this would delete everything that starts with the prefix 126 + /* msg.delete(); */ 127 + return; 128 + } 129 + let command: string; 130 + let content: string; 131 + if (isMention) { 132 + command = message.content.split(' ')[1]; // eslint-disable-line prefer-destructuring 133 + content = message.content 134 + .split(' ') 135 + .splice(2, message.content.split(' ').length) 136 + .join(' '); 137 + command = `.${command}`; 138 + } else { 139 + command = message.content 140 + .substring(PREFIXES.get(message.guild.id)!.length, message.content.length) 141 + .split(' ')[0] 142 + .toLowerCase(); 143 + // delete prefix and command 144 + content = message.content.slice( 145 + command.length + PREFIXES.get(message.guild.id)!.length, 146 + ); 147 + content = content.replace(/^\s+/g, ''); // delete leading spaces 148 + } 149 + if (command) { 150 + let commandVal: string; 151 + const pre = command.charAt(0); 152 + const myPerms = (message.channel as Discord.TextChannel).permissionsFor( 153 + message.guild.me, 154 + ); 155 + if (pre === '.') { 156 + command = command.slice(1); 157 + commandVal = command; 158 + } else if (pre === ':') { 159 + command = `_${command.slice(1)}`; 160 + commandVal = command.slice(1); 161 + // Check if its an admin command 162 + // if not you're allowed to use the normal version as admin (in any channel) 163 + if (!commands[command]) { 164 + command = command.slice(1); 165 + } 166 + // Check if the command exists, to not just spam k: msgs 167 + if (!commands[command]) { 168 + await sendMigrationMessageIfComandHasBeenMigrated( 169 + message, 170 + command, 171 + true, 172 + ); 173 + return; 174 + } 175 + if ( 176 + !(message.member && (await isAdmin(message.guild.id, message.member))) 177 + ) { 178 + if (myPerms) { 179 + if (myPerms.has('MANAGE_MESSAGES')) { 180 + message.delete(); 181 + } 182 + if (myPerms.has('SEND_MESSAGES')) { 183 + const reply = await message.reply( 184 + "you're not allowed to use this command.", 185 + ); 186 + await asyncWait(5000); 187 + await reply.delete(); 188 + } 189 + } 190 + return; 191 + } 192 + } else { 193 + return; 194 + } 195 + if (!commands[command]) { 196 + await sendMigrationMessageIfComandHasBeenMigrated(message, command); 197 + return; 198 + } 199 + if (!commands[command].dev || message.author.id === OWNERID) { 200 + if ( 201 + pre === ':' 204 202 || (await commandAllowed(message.guild.id, message.channel.id)) 205 - ) { 206 - const perms = commands[command].perm; 207 - if (!perms || (myPerms && myPerms.has(perms))) { 208 - // cooldown for command usage 209 - if (!userCooldowns.has(message.author.id)) { 210 - userCooldowns.add(message.author.id); 211 - setTimeout(() => { 212 - userCooldowns.delete(message.author.id); 213 - }, 4000); 214 - try { 215 - Statcord.ShardingClient.postCommand( 216 - command, 217 - message.author.id, 218 - bot, 219 - ); 220 - await commands[command].main({ content, message }); 221 - } catch (err) { 222 - catchError( 203 + ) { 204 + const perms = commands[command].perm; 205 + if (!perms || (myPerms && myPerms.has(perms))) { 206 + // cooldown for command usage 207 + if (!userCooldowns.has(message.author.id)) { 208 + userCooldowns.add(message.author.id); 209 + setTimeout(() => { 210 + userCooldowns.delete(message.author.id); 211 + }, 4000); 212 + try { 213 + Statcord.ShardingClient.postCommand( 214 + command, 215 + message.author.id, 216 + bot, 217 + ); 218 + await commands[command].main({ content, message }); 219 + } catch (err) { 220 + catchError( 223 221 err as Error, 224 222 message, 225 223 `${PREFIXES.get(message.guild.id)}${pre}${commandVal}`, 226 - ); 227 - } 228 - usageUp(message.author.id, message.guild.id); 229 - } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 230 - message.reply("whoa cool down, you're using commands too quick!"); 231 - } 232 - // endof cooldown management 233 - } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 234 - message.channel.send( 235 - `I don't have all the permissions needed for this command: (${perms}) `, 236 - ); 237 - } 238 - } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 239 - if (myPerms && myPerms.has('MANAGE_MESSAGES')) { 240 - message.delete(); 241 - } 242 - const reply = await message.reply( 243 - `commands aren't allowed in <#${ 244 - message.channel.id 245 - }>. Use them in ${await printCommandChannels( 246 - message.guild.id, 247 - )}. If you're an admin use \`${PREFIXES.get( 248 - message.guild.id, 249 - )}:help\` to see how you can change that.`, 250 - ); 251 - await asyncWait(15000); 252 - await reply.delete(); 253 - } 254 - } 255 - } 224 + ); 225 + } 226 + usageUp(message.author.id, message.guild.id); 227 + } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 228 + message.reply("whoa cool down, you're using commands too quick!"); 229 + } 230 + // endof cooldown management 231 + } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 232 + message.channel.send( 233 + `I don't have all the permissions needed for this command: (${perms}) `, 234 + ); 235 + } 236 + } else if (myPerms && myPerms.has('SEND_MESSAGES')) { 237 + if (myPerms && myPerms.has('MANAGE_MESSAGES')) { 238 + message.delete(); 239 + } 240 + const reply = await message.reply( 241 + `commands aren't allowed in <#${ 242 + message.channel.id 243 + }>. Use them in ${await printCommandChannels( 244 + message.guild.id, 245 + )}. If you're an admin use \`${PREFIXES.get( 246 + message.guild.id, 247 + )}:help\` to see how you can change that.`, 248 + ); 249 + await asyncWait(15000); 250 + await reply.delete(); 251 + } 252 + } 253 + } 256 254 257 - if (DELETE_COMMANDS) message.delete(); 255 + if (DELETE_COMMANDS) message.delete(); 258 256 }
+15 -15
src/commandSync.ts
··· 1 1 import { REST } from '@discordjs/rest'; 2 2 import { 3 - RESTPostAPIApplicationCommandsJSONBody, 4 - Routes, 3 + RESTPostAPIApplicationCommandsJSONBody, 4 + Routes, 5 5 } from 'discord-api-types/v10'; 6 6 import { applicationCommands } from './commands/applicationCommands'; 7 7 import { APP_ID, TOKEN } from './shared_assets'; 8 8 9 9 const commands = Object.values(applicationCommands).map( 10 - (command) => command.definition, 10 + (command) => command.definition, 11 11 ); 12 12 const testCommands: Array<RESTPostAPIApplicationCommandsJSONBody> = [ 13 - // admin.definition, 13 + // admin.definition, 14 14 ]; 15 15 16 16 const rest = new REST({ version: '9' }).setToken(TOKEN); ··· 18 18 const teabotsGuildId = '380669498014957569'; 19 19 20 20 export async function syncCommands() { 21 - try { 22 - console.log('Started refreshing application (/) commands.'); 23 - await rest.put(Routes.applicationCommands(APP_ID), { body: commands }); 24 - // for quick updates during testing: 25 - await rest.put(Routes.applicationGuildCommands(APP_ID, teabotsGuildId), { 26 - body: testCommands, 27 - }); 28 - console.log('Successfully reloaded application (/) commands.'); 29 - } catch (error) { 30 - console.error(error); 31 - } 21 + try { 22 + console.log('Started refreshing application (/) commands.'); 23 + await rest.put(Routes.applicationCommands(APP_ID), { body: commands }); 24 + // for quick updates during testing: 25 + await rest.put(Routes.applicationGuildCommands(APP_ID, teabotsGuildId), { 26 + body: testCommands, 27 + }); 28 + console.log('Successfully reloaded application (/) commands.'); 29 + } catch (error) { 30 + console.error(error); 31 + } 32 32 }
+12 -10
src/commands/applicationCommands.ts
··· 6 6 import { salt } from './salt'; 7 7 import { profile } from './profile'; 8 8 import { joinsound } from './joinsound'; 9 + import { info } from './info'; 9 10 import { admin } from './admin/adminApplicationCommands'; 10 11 import { MagibotSlashCommand } from '../types/command'; 11 12 12 13 export const applicationCommands: { [k: string]: MagibotSlashCommand } = { 13 - ping, 14 - roll, 15 - invite, 16 - bugreport, 17 - randomfact, 18 - salt, 19 - profile, 20 - joinsound, 14 + ping, 15 + roll, 16 + invite, 17 + bugreport, 18 + randomfact, 19 + salt, 20 + profile, 21 + joinsound, 22 + info, 21 23 22 - // all admin commands 23 - admin, 24 + // all admin commands 25 + admin, 24 26 };
+64
src/commands/info.ts
··· 1 + import { CommandInteraction, MessageEmbedOptions } from 'discord.js'; 2 + import { SlashCommandBuilder } from '@discordjs/builders'; 3 + import { COLOR, user, SIGN } from '../shared_assets'; 4 + import { commandCategories } from '../types/enums'; 5 + import { MagibotSlashCommand } from '../types/command'; 6 + 7 + const inviteURL = 'https://discord.com/api/oauth2/authorize?client_id=384820232583249921&permissions=276131153&redirect_uri=https%3A%2F%2Fdiscord.gg%2F2Evcf4T&scope=bot'; 8 + 9 + async function main(interaction: CommandInteraction) { 10 + const info: Array<{ 11 + name: string; 12 + value: string; 13 + inline: boolean; 14 + }> = [ 15 + { 16 + name: 'Links', 17 + value: `[Invite me to your guild](${inviteURL})\n[Official support Discord](https://discord.gg/2Evcf4T)`, 18 + inline: false, 19 + }, 20 + { 21 + name: 'How to support MagiBot', 22 + value: 23 + 'Pledge on [MagiBots Patreon](https://www.patreon.com/MagiBot)\nLeave a review on [bots.ondiscord.xyz](https://bots.ondiscord.xyz/bots/384820232583249921)!', 24 + inline: false, 25 + }, 26 + { 27 + name: 'A bit of background', 28 + value: 29 + "MagiBot is being developed in Germany by T0TProduction#0001 as a side project.\nIt was originally a private bot for a Discord guild themed after the Pokemon Magikarp which is the reason it's called MagiBot.", 30 + inline: false, 31 + }, 32 + ]; 33 + const embed: MessageEmbedOptions = { 34 + color: COLOR, 35 + description: 'Some information about the bot:', 36 + fields: info, 37 + footer: { 38 + iconURL: user().avatarURL() || '', 39 + text: SIGN, 40 + }, 41 + }; 42 + 43 + interaction.reply({ embeds: [embed] }); 44 + } 45 + 46 + const slashCommand = new SlashCommandBuilder() 47 + .setName('info') 48 + .setDescription('Get some info about the bot and official resources.'); 49 + 50 + export const info: MagibotSlashCommand = { 51 + help() { 52 + return [ 53 + { 54 + name: '', 55 + value: 56 + 'Get some info about the bot as well as links to official MagiBot stuff.', 57 + }, 58 + ]; 59 + }, 60 + run: main, 61 + definition: slashCommand.toJSON(), 62 + permissions: ['SEND_MESSAGES', 'EMBED_LINKS'], 63 + category: commandCategories.misc, 64 + };
+87 -87
src/commands/joinsounds/fileManagement.ts
··· 1 1 import { createReadStream, createWriteStream } from 'node:fs'; 2 2 import { 3 - mkdir, unlink, stat, readdir, rmdir, 3 + mkdir, unlink, stat, readdir, rmdir, 4 4 } from 'node:fs/promises'; 5 5 import Path from 'path'; 6 6 import { get } from 'node:https'; ··· 21 21 } 22 22 23 23 async function setupLocalFolders() { 24 - await mkdir(basePath).catch((error) => { 25 - // If the error is that it already exists, that's fine 26 - if (error.code !== 'EEXIST') { 27 - throw error; 28 - } 29 - }); 24 + await mkdir(basePath).catch((error) => { 25 + // If the error is that it already exists, that's fine 26 + if (error.code !== 'EEXIST') { 27 + throw error; 28 + } 29 + }); 30 30 } 31 31 setupLocalFolders(); 32 32 33 33 async function downloadFile(url: string, path: string) { 34 - return new Promise<void>((resolve /* , reject */) => { 35 - get(url, (res) => { 36 - const writeStream = createWriteStream(path); 37 - res.pipe(writeStream); 38 - writeStream.on('finish', () => { 39 - writeStream.close(); 40 - resolve(); 41 - }); 42 - }); 43 - }); 34 + return new Promise<void>((resolve /* , reject */) => { 35 + get(url, (res) => { 36 + const writeStream = createWriteStream(path); 37 + res.pipe(writeStream); 38 + writeStream.on('finish', () => { 39 + writeStream.close(); 40 + resolve(); 41 + }); 42 + }); 43 + }); 44 44 } 45 45 46 46 function getFolderSize(path: string): Promise<number> { 47 - return new Promise((resolve, reject) => { 48 - fastFolderSize(path, (err, bytes) => { 49 - if (err) { 50 - reject(err); 51 - } 52 - resolve(bytes !== undefined ? bytes : 9999999); 53 - }); 54 - }); 47 + return new Promise((resolve, reject) => { 48 + fastFolderSize(path, (err, bytes) => { 49 + if (err) { 50 + reject(err); 51 + } 52 + resolve(bytes !== undefined ? bytes : 9999999); 53 + }); 54 + }); 55 55 } 56 56 57 57 const oneMegabyte = 1024 * 1024; 58 58 const fourtyGigabyte = 40 * 1024 * oneMegabyte; 59 59 60 60 async function doesServerHaveEnoughSpace() { 61 - const sizeOfFolder = await getFolderSize(basePath); 62 - return sizeOfFolder <= fourtyGigabyte; 61 + const sizeOfFolder = await getFolderSize(basePath); 62 + return sizeOfFolder <= fourtyGigabyte; 63 63 } 64 64 65 65 const userPrefix = 'user_'; 66 66 const guildPrefix = 'guild_'; 67 67 68 68 function getTargetPath(target: JoinsoundTarget) { 69 - return Path.join( 70 - basePath, 71 - target.userId 72 - ? `${userPrefix}${target.userId}` 73 - : `${guildPrefix}${target.guildId}`, 74 - ); 69 + return Path.join( 70 + basePath, 71 + target.userId 72 + ? `${userPrefix}${target.userId}` 73 + : `${guildPrefix}${target.guildId}`, 74 + ); 75 75 } 76 76 77 77 // eslint-disable-next-line no-undef 78 78 function gracefullyCatchENOENT(error: NodeJS.ErrnoException) { 79 - // If the error is that it doesn't exists, that's fine 80 - if (error.code !== 'ENOENT') { 81 - throw error; 82 - } 79 + // If the error is that it doesn't exists, that's fine 80 + if (error.code !== 'ENOENT') { 81 + throw error; 82 + } 83 83 } 84 84 85 85 async function doesUserHaveEnoughSpace( 86 - target: JoinsoundTarget, 87 - targetFileName: string, 86 + target: JoinsoundTarget, 87 + targetFileName: string, 88 88 ) { 89 - const path = getTargetPath(target); 90 - await mkdir(path).catch((error) => { 91 - // If the error is that it already exists, that's fine 92 - if (error.code !== 'EEXIST') { 93 - throw error; 94 - } 95 - }); 96 - const sizeOfFolder = await getFolderSize(path); 97 - const statsOfExistingFile = await stat(targetFileName).catch( 98 - gracefullyCatchENOENT, 99 - ); 100 - const sizeOfExistingFile = statsOfExistingFile?.size || 0; 101 - return sizeOfFolder - sizeOfExistingFile <= oneMegabyte; 89 + const path = getTargetPath(target); 90 + await mkdir(path).catch((error) => { 91 + // If the error is that it already exists, that's fine 92 + if (error.code !== 'EEXIST') { 93 + throw error; 94 + } 95 + }); 96 + const sizeOfFolder = await getFolderSize(path); 97 + const statsOfExistingFile = await stat(targetFileName).catch( 98 + gracefullyCatchENOENT, 99 + ); 100 + const sizeOfExistingFile = statsOfExistingFile?.size || 0; 101 + return sizeOfFolder - sizeOfExistingFile <= oneMegabyte; 102 102 } 103 103 104 104 function getFilename(target: JoinsoundTarget) { 105 - const title = target.default ? 'default' : `${guildPrefix}${target.guildId}`; 106 - return Path.join(getTargetPath(target), title); 105 + const title = target.default ? 'default' : `${guildPrefix}${target.guildId}`; 106 + return Path.join(getTargetPath(target), title); 107 107 } 108 108 109 109 export async function storeJoinsoundOfTarget( 110 - target: JoinsoundTarget, 111 - fileUrl: string, 110 + target: JoinsoundTarget, 111 + fileUrl: string, 112 112 ) { 113 - if (!(await doesServerHaveEnoughSpace())) { 114 - // this should not happen. but if it does, we handle it to stop the server from being overloaded 115 - return JoinsoundStoreError.noStorageLeftOnServer; 116 - } 117 - const filename = getFilename(target); 118 - if (!(await doesUserHaveEnoughSpace(target, filename))) { 119 - return JoinsoundStoreError.noStorageLeftForUser; 120 - } 121 - await downloadFile(fileUrl, filename); 122 - return null; 113 + if (!(await doesServerHaveEnoughSpace())) { 114 + // this should not happen. but if it does, we handle it to stop the server from being overloaded 115 + return JoinsoundStoreError.noStorageLeftOnServer; 116 + } 117 + const filename = getFilename(target); 118 + if (!(await doesUserHaveEnoughSpace(target, filename))) { 119 + return JoinsoundStoreError.noStorageLeftForUser; 120 + } 121 + await downloadFile(fileUrl, filename); 122 + return null; 123 123 } 124 124 125 125 export async function removeLocallyStoredJoinsoundOfTarget( 126 - target: JoinsoundTarget, 126 + target: JoinsoundTarget, 127 127 ) { 128 - const filename = getFilename(target); 129 - await unlink(filename).catch(gracefullyCatchENOENT); 128 + const filename = getFilename(target); 129 + await unlink(filename).catch(gracefullyCatchENOENT); 130 130 } 131 131 132 132 async function getExistingUserFolders() { 133 - const folders = await readdir(basePath, { withFileTypes: true }); 134 - return folders 135 - .filter( 136 - (directory) => directory.isDirectory() && directory.name.startsWith(userPrefix), 137 - ) 138 - .map((directory) => directory.name.substring(userPrefix.length)); 133 + const folders = await readdir(basePath, { withFileTypes: true }); 134 + return folders 135 + .filter( 136 + (directory) => directory.isDirectory() && directory.name.startsWith(userPrefix), 137 + ) 138 + .map((directory) => directory.name.substring(userPrefix.length)); 139 139 } 140 140 141 141 export async function removeLocallyStoredJoinsoundsOfGuild(guildId: string) { 142 - // remove default server sound 143 - await removeLocallyStoredJoinsoundOfTarget({ guildId, default: true }); 142 + // remove default server sound 143 + await removeLocallyStoredJoinsoundOfTarget({ guildId, default: true }); 144 144 145 - // remove server folder 146 - await rmdir(Path.join(basePath, `${guildPrefix}${guildId}`)).catch( 147 - gracefullyCatchENOENT, 148 - ); 145 + // remove server folder 146 + await rmdir(Path.join(basePath, `${guildPrefix}${guildId}`)).catch( 147 + gracefullyCatchENOENT, 148 + ); 149 149 150 - // remove sounds of all users in server 151 - const userIds = await getExistingUserFolders(); 152 - await asyncForEach(userIds, async (userId) => { 153 - await removeLocallyStoredJoinsoundOfTarget({ userId, guildId }); 154 - }); 150 + // remove sounds of all users in server 151 + const userIds = await getExistingUserFolders(); 152 + await asyncForEach(userIds, async (userId) => { 153 + await removeLocallyStoredJoinsoundOfTarget({ userId, guildId }); 154 + }); 155 155 } 156 156 157 157 export function getJoinsoundReadableStreamOfUser(target: JoinsoundTarget) { 158 - return createReadStream(getFilename(target)); 158 + return createReadStream(getFilename(target)); 159 159 }
-64
src/commands/old/info.ts
··· 1 - import { MessageEmbedOptions } from 'discord.js'; 2 - import { COLOR, user, SIGN } from '../../shared_assets'; 3 - 4 - import { commandCategories } from '../../types/enums'; 5 - import { magibotCommand } from '../../types/command'; 6 - 7 - const inviteURL = 'https://discord.com/api/oauth2/authorize?client_id=384820232583249921&permissions=276131153&redirect_uri=https%3A%2F%2Fdiscord.gg%2F2Evcf4T&scope=bot'; 8 - 9 - export const inf: magibotCommand = { 10 - dev: false, 11 - name: 'info', 12 - main: ({ message }) => { 13 - const info: Array<{ 14 - name: string; 15 - value: string; 16 - inline: boolean; 17 - }> = []; 18 - 19 - info.push({ 20 - name: 'Links', 21 - value: `[Invite me to your guild](${inviteURL})\n[Official support Discord](https://discord.gg/2Evcf4T)`, 22 - inline: false, 23 - }); 24 - 25 - info.push({ 26 - name: 'How to support MagiBot', 27 - value: 28 - 'Donate a buck via [Paypal](https://paypal.me/pools/c/8be5ok31vB)\nPledge on [MagiBots Patreon](https://www.patreon.com/MagiBot)\nLeave a review on [bots.ondiscord.xyz](https://bots.ondiscord.xyz/bots/384820232583249921)!', 29 - inline: false, 30 - }); 31 - 32 - info.push({ 33 - name: 'A bit of background', 34 - value: 35 - "MagiBot is being developed in Germany by T0TProduction#0001 as a hobby project.\nIt was originally a private bot for a Discord guild themed after the Pokemon Magikarp which is the reason it's called MagiBot.", 36 - inline: false, 37 - }); 38 - 39 - const embed: MessageEmbedOptions = { 40 - color: COLOR, 41 - description: 'Some information about the bot:', 42 - fields: info, 43 - footer: { 44 - iconURL: user().avatarURL() || '', 45 - text: SIGN, 46 - }, 47 - }; 48 - 49 - message.channel.send({ embeds: [embed] }); 50 - }, 51 - ehelp() { 52 - return [ 53 - { 54 - name: '', 55 - value: 56 - 'Get some info about the bot as well as links to official MagiBot stuff.', 57 - }, 58 - ]; 59 - }, 60 - perm: ['SEND_MESSAGES', 'EMBED_LINKS'], 61 - admin: false, 62 - hide: false, 63 - category: commandCategories.misc, 64 - };
+18 -18
src/commands/ping.ts
··· 4 4 import { MagibotSlashCommand } from '../types/command'; 5 5 6 6 const slashCommand = new SlashCommandBuilder() 7 - .setName('ping') 8 - .setDescription('Returns the round trip time between you and MagiBot!'); 7 + .setName('ping') 8 + .setDescription('Returns the round trip time between you and MagiBot!'); 9 9 10 10 export const ping: MagibotSlashCommand = { 11 - help() { 12 - return [ 13 - { 14 - name: '', 15 - value: 'Ping the bot and get the response time.', 16 - }, 17 - ]; 18 - }, 19 - permissions: 'SEND_MESSAGES', 20 - category: commandCategories.misc, 21 - async run(interaction: CommandInteraction) { 22 - const stop = new Date(); 23 - const diff = stop.getTime() - interaction.createdAt.getTime(); 24 - await interaction.reply(`Pong! \`(${diff}ms)\``); 25 - }, 26 - definition: slashCommand.toJSON(), 11 + help() { 12 + return [ 13 + { 14 + name: '', 15 + value: 'Ping the bot and get the response time.', 16 + }, 17 + ]; 18 + }, 19 + permissions: 'SEND_MESSAGES', 20 + category: commandCategories.misc, 21 + async run(interaction: CommandInteraction) { 22 + const stop = new Date(); 23 + const diff = stop.getTime() - interaction.createdAt.getTime(); 24 + await interaction.reply(`Pong! \`(${diff}ms)\``); 25 + }, 26 + definition: slashCommand.toJSON(), 27 27 };
+13 -13
src/configuration.ts
··· 8 8 const prefix = process.env.PREFIX; 9 9 const dburl = process.env.DATABASE_URL; 10 10 let blapis: { 11 - [listname: string]: string; 11 + [listname: string]: string; 12 12 }; 13 13 14 14 try { 15 - // eslint-disable-next-line import/no-dynamic-require, global-require 16 - blapis = require(`${__dirname}/../botlistTokens.json`); 15 + // eslint-disable-next-line import/no-dynamic-require, global-require 16 + blapis = require(`${__dirname}/../botlistTokens.json`); 17 17 } catch (e) { 18 - // eslint-disable-next-line no-console 19 - console.warn('no bot list tokens found, defaulting to none'); 20 - blapis = {}; 18 + // eslint-disable-next-line no-console 19 + console.warn('no bot list tokens found, defaulting to none'); 20 + blapis = {}; 21 21 } 22 22 23 23 if (!(token && owner && prefix && dburl && appId)) { 24 - throw new Error('Missing .env configuration!'); 24 + throw new Error('Missing .env configuration!'); 25 25 } 26 26 27 27 export default { 28 - tk: token, 29 - owner, 30 - prefix, 31 - dburl, 32 - blapis, 33 - appId, 28 + tk: token, 29 + owner, 30 + prefix, 31 + dburl, 32 + blapis, 33 + appId, 34 34 };
+264 -264
src/cronjobs.ts
··· 1 1 import { Client, TextChannel } from 'discord.js'; 2 2 import { asyncForEach } from './helperFunctions'; 3 3 import { 4 - SaltModel, 5 - SaltrankModel, 6 - SettingsModel, 7 - StillMutedModel, 8 - UserModel, 9 - Vote, 10 - VoteModel, 4 + SaltModel, 5 + SaltrankModel, 6 + SettingsModel, 7 + StillMutedModel, 8 + UserModel, 9 + Vote, 10 + VoteModel, 11 11 } from './db'; 12 12 import { checkGuild } from './dbHelpers'; 13 13 import config from './configuration'; ··· 15 15 import { removeLocallyStoredJoinsoundsOfGuild } from './commands/joinsounds/fileManagement'; 16 16 17 17 if (!config.dburl) { 18 - throw new Error('Missing DB connection URL'); 18 + throw new Error('Missing DB connection URL'); 19 19 } 20 20 21 21 const allowDeletions = true; 22 22 23 23 // automatic deletion of reports: 24 24 async function hourlyCleanup(bot: Client, isFirst: boolean) { 25 - const now = new Date(); 26 - console.log('Running hourly cleanup at:', now); 27 - const h = new Date( 28 - now.getFullYear(), 29 - now.getMonth(), 30 - now.getDate(), 31 - now.getHours() + 1, 32 - 0, 33 - 0, 34 - 0, 35 - ); 36 - const timeoutForNextHour = h.getTime() - now.getTime(); 37 - if (isFirst) { 38 - await sendStartupEvent(bot.shard!.ids[0], true); 39 - } 40 - const sevenDaysAgo = new Date(); 41 - sevenDaysAgo.setDate(now.getDate() - 7); 42 - const guilds = bot.guilds.cache; 43 - let counter = 0; 44 - await asyncForEach(guilds, async (G) => { 45 - const guildID = G.id; 46 - const localCounter = ++counter; 47 - await checkGuild(guildID); 48 - // update the guild settings entry so that it does NOT get deleted 49 - await SettingsModel.updateOne( 50 - { _id: guildID }, 51 - { $set: { lastConnected: now } }, 52 - ); 53 - const ranking = await SaltrankModel.find({ guild: guildID }); 54 - await asyncForEach(ranking, async (report) => { 55 - const removeData = await SaltModel.deleteMany({ 56 - date: { $lt: sevenDaysAgo }, 57 - guild: guildID, 58 - salter: report.salter, 59 - }); 60 - if (removeData.deletedCount && removeData.deletedCount > 0) { 61 - const slt = report.salt - removeData.deletedCount; 62 - if (slt <= 0) { 63 - await SaltrankModel.deleteOne({ 64 - salter: report.salter, 65 - guild: guildID, 66 - }); 67 - } else { 68 - await SaltrankModel.updateOne( 69 - { 70 - salter: report.salter, 71 - guild: guildID, 72 - }, 73 - { $set: { salt: slt } }, 74 - ); 75 - } 76 - } 77 - }); 78 - // update percentage message 79 - /* if (msg) { 80 - const u = process.hrtime(t0); 81 - if ( 82 - (u[0] - latestTimePassed > 0 && localCounter > lastPostedCounter) 25 + const now = new Date(); 26 + console.log('Running hourly cleanup at:', now); 27 + const h = new Date( 28 + now.getFullYear(), 29 + now.getMonth(), 30 + now.getDate(), 31 + now.getHours() + 1, 32 + 0, 33 + 0, 34 + 0, 35 + ); 36 + const timeoutForNextHour = h.getTime() - now.getTime(); 37 + if (isFirst) { 38 + await sendStartupEvent(bot.shard!.ids[0], true); 39 + } 40 + const sevenDaysAgo = new Date(); 41 + sevenDaysAgo.setDate(now.getDate() - 7); 42 + const guilds = bot.guilds.cache; 43 + let counter = 0; 44 + await asyncForEach(guilds, async (G) => { 45 + const guildID = G.id; 46 + const localCounter = ++counter; 47 + await checkGuild(guildID); 48 + // update the guild settings entry so that it does NOT get deleted 49 + await SettingsModel.updateOne( 50 + { _id: guildID }, 51 + { $set: { lastConnected: now } }, 52 + ); 53 + const ranking = await SaltrankModel.find({ guild: guildID }); 54 + await asyncForEach(ranking, async (report) => { 55 + const removeData = await SaltModel.deleteMany({ 56 + date: { $lt: sevenDaysAgo }, 57 + guild: guildID, 58 + salter: report.salter, 59 + }); 60 + if (removeData.deletedCount && removeData.deletedCount > 0) { 61 + const slt = report.salt - removeData.deletedCount; 62 + if (slt <= 0) { 63 + await SaltrankModel.deleteOne({ 64 + salter: report.salter, 65 + guild: guildID, 66 + }); 67 + } else { 68 + await SaltrankModel.updateOne( 69 + { 70 + salter: report.salter, 71 + guild: guildID, 72 + }, 73 + { $set: { salt: slt } }, 74 + ); 75 + } 76 + } 77 + }); 78 + // update percentage message 79 + /* if (msg) { 80 + const u = process.hrtime(t0); 81 + if ( 82 + (u[0] - latestTimePassed > 0 && localCounter > lastPostedCounter) 83 83 || localCounter === guilds.length 84 - ) { 85 - // eslint-disable-next-line prefer-destructuring 86 - latestTimePassed = u[0]; 87 - lastPostedCounter = localCounter; 88 - const percentage = Math.round((localCounter / guilds.length) * 100); 89 - let uptime = ''; 90 - // mins 91 - let x = Math.floor(u[0] / 60); 92 - if (x > 0) { 93 - uptime += `${x}m : `; 94 - } 95 - // secs 96 - x = u[0] % 60; 97 - if (x >= 0) { 98 - uptime += `${x}s`; 99 - } 100 - await msg.edit( 101 - `Shard ${bot.shard!.ids[0]}: ${percentage} % with ${uptime} passed`, 102 - ); 103 - } 104 - } */ 105 - if (localCounter >= guilds.size && isFirst) { 106 - await sendStartupEvent(bot.shard!.ids[0]); 107 - } 108 - }); 84 + ) { 85 + // eslint-disable-next-line prefer-destructuring 86 + latestTimePassed = u[0]; 87 + lastPostedCounter = localCounter; 88 + const percentage = Math.round((localCounter / guilds.length) * 100); 89 + let uptime = ''; 90 + // mins 91 + let x = Math.floor(u[0] / 60); 92 + if (x > 0) { 93 + uptime += `${x}m : `; 94 + } 95 + // secs 96 + x = u[0] % 60; 97 + if (x >= 0) { 98 + uptime += `${x}s`; 99 + } 100 + await msg.edit( 101 + `Shard ${bot.shard!.ids[0]}: ${percentage} % with ${uptime} passed`, 102 + ); 103 + } 104 + } */ 105 + if (localCounter >= guilds.size && isFirst) { 106 + await sendStartupEvent(bot.shard!.ids[0]); 107 + } 108 + }); 109 109 110 - // find all guilds that have not connected for a week 111 - const guilds2 = await SettingsModel.find({ 112 - lastConnected: { $lt: sevenDaysAgo }, 113 - }); 114 - // remove all data saved for those guilds 115 - await asyncForEach(guilds2, async (guild) => { 116 - // eslint-disable-next-line no-underscore-dangle 117 - const guildID = guild._id; 118 - try { 119 - if (allowDeletions) { 120 - await sendJoinEvent( 121 - `:wastebasket: Deleting all information from guild with ID ${guildID} because they removed me more than seven days ago.`, 122 - bot.shard?.ids[0], 123 - ); 124 - // ignore salt and saltrank, as they are removed after 7 days anyways 125 - await StillMutedModel.deleteMany({ guildid: guildID }); 126 - await UserModel.deleteMany({ guildID }); 127 - // doing this per guild instead of with a list of guilds is less efficient 128 - // but now it happens at the same time as the rest of deletion 129 - await removeLocallyStoredJoinsoundsOfGuild(guildID); 130 - await VoteModel.deleteMany({ guildid: guildID }); 131 - await SettingsModel.deleteMany({ _id: guildID }); 132 - } else { 133 - await sendJoinEvent( 134 - `:wastebasket: Attempting to delete all information from guild with ID ${guildID} because they removed me more than seven days ago. Deletion is deactivated for now, though.`, 135 - bot.shard?.ids[0], 136 - ); 137 - } 138 - } catch (error) { 139 - sendException( 140 - `Failed deleting all information releated to guild with ID ${guildID} : ${error}`, 141 - bot.shard?.ids[0], 142 - ); 143 - } 144 - }); 110 + // find all guilds that have not connected for a week 111 + const guilds2 = await SettingsModel.find({ 112 + lastConnected: { $lt: sevenDaysAgo }, 113 + }); 114 + // remove all data saved for those guilds 115 + await asyncForEach(guilds2, async (guild) => { 116 + // eslint-disable-next-line no-underscore-dangle 117 + const guildID = guild._id; 118 + try { 119 + if (allowDeletions) { 120 + await sendJoinEvent( 121 + `:wastebasket: Deleting all information from guild with ID ${guildID} because they removed me more than seven days ago.`, 122 + bot.shard?.ids[0], 123 + ); 124 + // ignore salt and saltrank, as they are removed after 7 days anyways 125 + await StillMutedModel.deleteMany({ guildid: guildID }); 126 + await UserModel.deleteMany({ guildID }); 127 + // doing this per guild instead of with a list of guilds is less efficient 128 + // but now it happens at the same time as the rest of deletion 129 + await removeLocallyStoredJoinsoundsOfGuild(guildID); 130 + await VoteModel.deleteMany({ guildid: guildID }); 131 + await SettingsModel.deleteMany({ _id: guildID }); 132 + } else { 133 + await sendJoinEvent( 134 + `:wastebasket: Attempting to delete all information from guild with ID ${guildID} because they removed me more than seven days ago. Deletion is deactivated for now, though.`, 135 + bot.shard?.ids[0], 136 + ); 137 + } 138 + } catch (error) { 139 + sendException( 140 + `Failed deleting all information releated to guild with ID ${guildID} : ${error}`, 141 + bot.shard?.ids[0], 142 + ); 143 + } 144 + }); 145 145 146 - // call function for next hour 147 - setTimeout(() => hourlyCleanup(bot, false), timeoutForNextHour); 146 + // call function for next hour 147 + setTimeout(() => hourlyCleanup(bot, false), timeoutForNextHour); 148 148 } 149 149 150 150 const reactions = [ 151 - '🇦', 152 - '🇧', 153 - '🇨', 154 - '🇩', 155 - '🇪', 156 - '🇫', 157 - '🇬', 158 - '🇭', 159 - '🇮', 160 - '🇯', 161 - '🇰', 162 - '🇱', 163 - '🇲', 164 - '🇳', 165 - '🇴', 166 - '🇵', 167 - '🇶', 168 - '🇷', 169 - '🇸', 170 - '🇹', 151 + '🇦', 152 + '🇧', 153 + '🇨', 154 + '🇩', 155 + '🇪', 156 + '🇫', 157 + '🇬', 158 + '🇭', 159 + '🇮', 160 + '🇯', 161 + '🇰', 162 + '🇱', 163 + '🇲', 164 + '🇳', 165 + '🇴', 166 + '🇵', 167 + '🇶', 168 + '🇷', 169 + '🇸', 170 + '🇹', 171 171 ]; 172 172 173 173 // this should take care of everything that needs to be done when a vote ends 174 174 async function endVote(vote: Vote, bot: Client) { 175 - try { 176 - const voteChannel = (await bot.channels.fetch( 177 - vote.channelID, 178 - )) as TextChannel; 179 - if (voteChannel) { 180 - const msg = await voteChannel.messages.fetch(vote.messageID); 181 - if (msg) { 182 - const reacts = msg.reactions; 183 - let finalReact: Array<{ reaction: number; count: number }> = []; 184 - reactions.forEach((x, i) => { 185 - if (i >= vote.options.length) { 186 - return; 187 - } 188 - const react = reacts.resolve(x); 189 - if (react && react.count) { 190 - if (!finalReact[0] || finalReact[0].count <= react.count) { 191 - if (!finalReact[0] || finalReact[0].count < react.count) { 192 - finalReact = [ 193 - { 194 - reaction: i, 195 - count: react.count, 196 - }, 197 - ]; 198 - } else { 199 - finalReact.push({ 200 - reaction: i, 201 - count: react.count, 202 - }); 203 - } 204 - } 205 - } 206 - }); 207 - if (finalReact[0]) { 208 - if (finalReact.length > 1) { 209 - let str = `**${vote.topic}** ended.\n\nThere was a tie between `; 210 - if (vote.authorID) { 211 - str = `**${vote.topic}** by <@${vote.authorID}> ended.\n\nThere was a tie between `; 212 - } 213 - finalReact.forEach((react, i) => { 214 - str += `**${vote.options[react.reaction]}**`; 215 - if (i < finalReact.length - 2) { 216 - str += ', '; 217 - } else if (i === finalReact.length - 2) { 218 - str += ' and '; 219 - } 220 - }); 221 - str += ` with each having ** ${finalReact[0].count - 1} ** votes.`; 222 - await msg.edit(str); 223 - } else { 224 - let str = `**${vote.topic}** ended.\n\nThe result is **${ 225 - vote.options[finalReact[0].reaction] 226 - }** with **${finalReact[0].count - 1}** votes.`; 227 - if (vote.authorID) { 228 - str = `**${vote.topic}** by <@${ 229 - vote.authorID 230 - }> ended.\n\nThe result is **${ 231 - vote.options[finalReact[0].reaction] 232 - }** with **${finalReact[0].count - 1}** votes.`; 233 - } 234 - await msg.edit(str); 235 - } 236 - } else { 237 - let str = `**${vote.topic}** ended.\n\nCould not compute a result.`; 238 - if (vote.authorID) { 239 - str = `**${vote.topic}** by <@${vote.authorID}> ended.\n\nCould not compute a result.`; 240 - } 241 - await msg.edit(str); 242 - } 243 - await msg.reactions.removeAll(); 244 - } 245 - } 246 - } catch (error) { 247 - console.error(JSON.stringify(error, null, 2)); 248 - if ( 249 - // eslint-disable-next-line eqeqeq 250 - (error as any).httpStatus != 404 /* 'DiscordAPIError: Unknown Message' */ 251 - ) { 252 - throw error; 253 - } 254 - } 175 + try { 176 + const voteChannel = (await bot.channels.fetch( 177 + vote.channelID, 178 + )) as TextChannel; 179 + if (voteChannel) { 180 + const msg = await voteChannel.messages.fetch(vote.messageID); 181 + if (msg) { 182 + const reacts = msg.reactions; 183 + let finalReact: Array<{ reaction: number; count: number }> = []; 184 + reactions.forEach((x, i) => { 185 + if (i >= vote.options.length) { 186 + return; 187 + } 188 + const react = reacts.resolve(x); 189 + if (react && react.count) { 190 + if (!finalReact[0] || finalReact[0].count <= react.count) { 191 + if (!finalReact[0] || finalReact[0].count < react.count) { 192 + finalReact = [ 193 + { 194 + reaction: i, 195 + count: react.count, 196 + }, 197 + ]; 198 + } else { 199 + finalReact.push({ 200 + reaction: i, 201 + count: react.count, 202 + }); 203 + } 204 + } 205 + } 206 + }); 207 + if (finalReact[0]) { 208 + if (finalReact.length > 1) { 209 + let str = `**${vote.topic}** ended.\n\nThere was a tie between `; 210 + if (vote.authorID) { 211 + str = `**${vote.topic}** by <@${vote.authorID}> ended.\n\nThere was a tie between `; 212 + } 213 + finalReact.forEach((react, i) => { 214 + str += `**${vote.options[react.reaction]}**`; 215 + if (i < finalReact.length - 2) { 216 + str += ', '; 217 + } else if (i === finalReact.length - 2) { 218 + str += ' and '; 219 + } 220 + }); 221 + str += ` with each having ** ${finalReact[0].count - 1} ** votes.`; 222 + await msg.edit(str); 223 + } else { 224 + let str = `**${vote.topic}** ended.\n\nThe result is **${ 225 + vote.options[finalReact[0].reaction] 226 + }** with **${finalReact[0].count - 1}** votes.`; 227 + if (vote.authorID) { 228 + str = `**${vote.topic}** by <@${ 229 + vote.authorID 230 + }> ended.\n\nThe result is **${ 231 + vote.options[finalReact[0].reaction] 232 + }** with **${finalReact[0].count - 1}** votes.`; 233 + } 234 + await msg.edit(str); 235 + } 236 + } else { 237 + let str = `**${vote.topic}** ended.\n\nCould not compute a result.`; 238 + if (vote.authorID) { 239 + str = `**${vote.topic}** by <@${vote.authorID}> ended.\n\nCould not compute a result.`; 240 + } 241 + await msg.edit(str); 242 + } 243 + await msg.reactions.removeAll(); 244 + } 245 + } 246 + } catch (error) { 247 + console.error(JSON.stringify(error, null, 2)); 248 + if ( 249 + // eslint-disable-next-line eqeqeq 250 + (error as any).httpStatus != 404 /* 'DiscordAPIError: Unknown Message' */ 251 + ) { 252 + throw error; 253 + } 254 + } 255 255 } 256 256 257 257 async function voteCheck(bot: Client) { 258 - const d = new Date(); 259 - const h = new Date( 260 - d.getFullYear(), 261 - d.getMonth(), 262 - d.getDate(), 263 - d.getHours(), 264 - d.getMinutes(), 265 - d.getSeconds() + 10, 266 - 0, 267 - ); 268 - const e = h.getTime() - d.getTime(); 269 - if (e > 100) { 270 - // some arbitrary time period 271 - setTimeout(voteCheck.bind(null, bot), e); 272 - } 273 - // do vote stuff 274 - const nd = new Date(); 275 - const votes = await VoteModel.find({ date: { $lte: nd } }); 276 - await asyncForEach(votes, async (vote) => { 277 - try { 278 - await endVote(vote, bot); 279 - await vote.delete(); 280 - } catch (err) { 281 - if ( 282 - (err as any).name === 'DiscordAPIError' 258 + const d = new Date(); 259 + const h = new Date( 260 + d.getFullYear(), 261 + d.getMonth(), 262 + d.getDate(), 263 + d.getHours(), 264 + d.getMinutes(), 265 + d.getSeconds() + 10, 266 + 0, 267 + ); 268 + const e = h.getTime() - d.getTime(); 269 + if (e > 100) { 270 + // some arbitrary time period 271 + setTimeout(voteCheck.bind(null, bot), e); 272 + } 273 + // do vote stuff 274 + const nd = new Date(); 275 + const votes = await VoteModel.find({ date: { $lte: nd } }); 276 + await asyncForEach(votes, async (vote) => { 277 + try { 278 + await endVote(vote, bot); 279 + await vote.delete(); 280 + } catch (err) { 281 + if ( 282 + (err as any).name === 'DiscordAPIError' 283 283 && (err as any).message === 'Missing Access' 284 - ) { 285 - await vote.delete(); 286 - } else { 287 - throw err; 288 - } 289 - } 290 - }); 291 - // endof vote stuff 284 + ) { 285 + await vote.delete(); 286 + } else { 287 + throw err; 288 + } 289 + } 290 + }); 291 + // endof vote stuff 292 292 } 293 293 294 294 export function startUp(bot: Client) { 295 - // repeating functions: 296 - hourlyCleanup(bot, true); 297 - voteCheck(bot); 295 + // repeating functions: 296 + hourlyCleanup(bot, true); 297 + voteCheck(bot); 298 298 }
+233 -233
src/db.ts
··· 2 2 import config from './configuration'; 3 3 4 4 if (!config.dburl) { 5 - throw new Error('Missing DB connection URL'); 5 + throw new Error('Missing DB connection URL'); 6 6 } 7 7 const url = config.dburl; 8 8 9 9 mongoose.connect(`${url}/MagiBot`, { 10 - ssl: true, 10 + ssl: true, 11 11 }); 12 12 13 13 // const mongooseConnction = mongoose.connection; ··· 29 29 defaultJoinsound?: string; 30 30 }; 31 31 const settingsSchema = new mongoose.Schema<Settings>( 32 - { 33 - _id: { 34 - // _id is auto indexed and unique 35 - type: String, 36 - required: true, 37 - }, 38 - commandChannels: { 39 - type: [String], 40 - required: true, 41 - }, 42 - adminRoles: { 43 - type: [String], 44 - required: true, 45 - }, 46 - joinChannels: { 47 - type: [String], 48 - required: true, 49 - }, 50 - blacklistedUsers: { 51 - type: [String], 52 - required: true, 53 - }, 54 - blacklistedEveryone: { 55 - type: [String], 56 - required: true, 57 - }, 58 - saltKing: { 59 - type: String, 60 - required: false, 61 - }, 62 - saltRole: { 63 - type: String, 64 - required: false, 65 - }, 66 - notChannel: { 67 - type: String, 68 - required: false, 69 - }, 70 - prefix: { 71 - type: String, 72 - required: true, 73 - }, 74 - lastConnected: { 75 - type: Date, 76 - required: true, 77 - }, 78 - defaultJoinsound: { 79 - type: String, 80 - required: false, 81 - }, 82 - }, 83 - { collection: 'settings' }, 32 + { 33 + _id: { 34 + // _id is auto indexed and unique 35 + type: String, 36 + required: true, 37 + }, 38 + commandChannels: { 39 + type: [String], 40 + required: true, 41 + }, 42 + adminRoles: { 43 + type: [String], 44 + required: true, 45 + }, 46 + joinChannels: { 47 + type: [String], 48 + required: true, 49 + }, 50 + blacklistedUsers: { 51 + type: [String], 52 + required: true, 53 + }, 54 + blacklistedEveryone: { 55 + type: [String], 56 + required: true, 57 + }, 58 + saltKing: { 59 + type: String, 60 + required: false, 61 + }, 62 + saltRole: { 63 + type: String, 64 + required: false, 65 + }, 66 + notChannel: { 67 + type: String, 68 + required: false, 69 + }, 70 + prefix: { 71 + type: String, 72 + required: true, 73 + }, 74 + lastConnected: { 75 + type: Date, 76 + required: true, 77 + }, 78 + defaultJoinsound: { 79 + type: String, 80 + required: false, 81 + }, 82 + }, 83 + { collection: 'settings' }, 84 84 ); 85 85 export const SettingsModel = mongoose.model<Settings>( 86 - 'settings', 87 - settingsSchema, 86 + 'settings', 87 + settingsSchema, 88 88 ); 89 89 90 90 // saltrank per user 91 91 type Saltrank = { salter: string; salt: number; guild: string }; 92 92 const saltrankSchema = new mongoose.Schema<Saltrank>( 93 - { 94 - salter: { 95 - type: String, 96 - required: true, 97 - }, 98 - salt: { 99 - type: Number, 100 - required: true, 101 - }, 102 - guild: { 103 - type: String, 104 - required: true, 105 - }, 106 - }, 107 - { collection: 'saltrank' }, 93 + { 94 + salter: { 95 + type: String, 96 + required: true, 97 + }, 98 + salt: { 99 + type: Number, 100 + required: true, 101 + }, 102 + guild: { 103 + type: String, 104 + required: true, 105 + }, 106 + }, 107 + { collection: 'saltrank' }, 108 108 ); 109 109 saltrankSchema.index( 110 - { 111 - salter: 1, 112 - guild: -1, 113 - }, 114 - { unique: true }, 110 + { 111 + salter: 1, 112 + guild: -1, 113 + }, 114 + { unique: true }, 115 115 ); 116 116 export const SaltrankModel = mongoose.model<Saltrank>( 117 - 'saltrank', 118 - saltrankSchema, 117 + 'saltrank', 118 + saltrankSchema, 119 119 ); 120 120 121 121 // salt reports per report ··· 126 126 guild: string; 127 127 }; 128 128 const saltSchema = new mongoose.Schema<Salt>( 129 - { 130 - salter: { 131 - type: String, 132 - required: true, 133 - }, 134 - reporter: { 135 - type: String, 136 - required: true, 137 - }, 138 - date: { 139 - type: Date, 140 - required: true, 141 - }, 142 - guild: { 143 - type: String, 144 - required: true, 145 - index: true, 146 - }, 147 - }, 148 - { collection: 'salt' }, 129 + { 130 + salter: { 131 + type: String, 132 + required: true, 133 + }, 134 + reporter: { 135 + type: String, 136 + required: true, 137 + }, 138 + date: { 139 + type: Date, 140 + required: true, 141 + }, 142 + guild: { 143 + type: String, 144 + required: true, 145 + index: true, 146 + }, 147 + }, 148 + { collection: 'salt' }, 149 149 ); 150 150 saltSchema.index({ 151 - salter: 1, 152 - reporter: -1, 151 + salter: 1, 152 + reporter: -1, 153 153 }); 154 154 export const SaltModel = mongoose.model<Salt>('salt', saltSchema); 155 155 ··· 164 164 sound?: string; 165 165 }; 166 166 const userSchema = new mongoose.Schema<User>( 167 - { 168 - userID: { 169 - type: String, 170 - required: true, 171 - }, 172 - guildID: { 173 - type: String, 174 - required: true, 175 - }, 176 - warnings: { 177 - type: Number, 178 - required: true, 179 - }, 180 - kicks: { 181 - type: Number, 182 - required: true, 183 - }, 184 - bans: { 185 - type: Number, 186 - required: true, 187 - }, 188 - botusage: { 189 - type: Number, 190 - required: true, 191 - }, 192 - sound: { 193 - type: String, 194 - required: false, 195 - }, 196 - }, 197 - { collection: 'users' }, 167 + { 168 + userID: { 169 + type: String, 170 + required: true, 171 + }, 172 + guildID: { 173 + type: String, 174 + required: true, 175 + }, 176 + warnings: { 177 + type: Number, 178 + required: true, 179 + }, 180 + kicks: { 181 + type: Number, 182 + required: true, 183 + }, 184 + bans: { 185 + type: Number, 186 + required: true, 187 + }, 188 + botusage: { 189 + type: Number, 190 + required: true, 191 + }, 192 + sound: { 193 + type: String, 194 + required: false, 195 + }, 196 + }, 197 + { collection: 'users' }, 198 198 ); 199 199 userSchema.index( 200 - { 201 - userID: 1, 202 - guildID: -1, 203 - }, 204 - { unique: true }, 200 + { 201 + userID: 1, 202 + guildID: -1, 203 + }, 204 + { unique: true }, 205 205 ); 206 206 export const UserModel = mongoose.model<User>('users', userSchema); 207 207 208 208 const deleteDuplicateUsers = true; 209 209 UserModel.aggregate([ 210 - { 211 - $group: { 212 - _id: { 213 - userID: '$userID', 214 - guildID: '$guildID', 215 - }, 216 - count: { 217 - $sum: 1, 218 - }, 219 - }, 220 - }, 210 + { 211 + $group: { 212 + _id: { 213 + userID: '$userID', 214 + guildID: '$guildID', 215 + }, 216 + count: { 217 + $sum: 1, 218 + }, 219 + }, 220 + }, 221 221 ]).then((duplicateUsers) => { 222 - console.log( 223 - 'duplicate users:', 224 - JSON.stringify( 225 - duplicateUsers.filter((u) => u.count > 1), 226 - null, 227 - 2, 228 - ), 229 - ); 230 - if (deleteDuplicateUsers) { 231 - duplicateUsers 232 - .filter((u) => u.count > 1) 233 - .forEach(async (u) => { 234 - for (let i = 1; i < u.count; i++) { 235 - // eslint-disable-next-line no-underscore-dangle, no-await-in-loop 236 - await UserModel.deleteOne(u._id); 237 - // eslint-disable-next-line no-underscore-dangle 238 - console.log('Deleted user', u._id, 'run', i, 'of', u.count - 1); 239 - } 240 - }); 241 - } 222 + console.log( 223 + 'duplicate users:', 224 + JSON.stringify( 225 + duplicateUsers.filter((u) => u.count > 1), 226 + null, 227 + 2, 228 + ), 229 + ); 230 + if (deleteDuplicateUsers) { 231 + duplicateUsers 232 + .filter((u) => u.count > 1) 233 + .forEach(async (u) => { 234 + for (let i = 1; i < u.count; i++) { 235 + // eslint-disable-next-line no-underscore-dangle, no-await-in-loop 236 + await UserModel.deleteOne(u._id); 237 + // eslint-disable-next-line no-underscore-dangle 238 + console.log('Deleted user', u._id, 'run', i, 'of', u.count - 1); 239 + } 240 + }); 241 + } 242 242 }); 243 243 244 244 // global/default values for a specific user ··· 247 247 sound?: string; 248 248 }; 249 249 const globalUserSchema = new mongoose.Schema<globalUser>( 250 - { 251 - userID: { 252 - type: String, 253 - required: true, 254 - }, 255 - sound: { 256 - type: String, 257 - required: false, 258 - }, 259 - }, 260 - { collection: 'globalUsers' }, 250 + { 251 + userID: { 252 + type: String, 253 + required: true, 254 + }, 255 + sound: { 256 + type: String, 257 + required: false, 258 + }, 259 + }, 260 + { collection: 'globalUsers' }, 261 261 ); 262 262 globalUserSchema.index( 263 - { 264 - userID: 1, 265 - }, 266 - { unique: true }, 263 + { 264 + userID: 1, 265 + }, 266 + { unique: true }, 267 267 ); 268 268 export const GlobalUserDataModel = mongoose.model<globalUser>( 269 - 'globalUsers', 270 - globalUserSchema, 269 + 'globalUsers', 270 + globalUserSchema, 271 271 ); 272 272 273 273 // entry per vote ··· 281 281 authorID: string; 282 282 }; 283 283 const voteSchema = new mongoose.Schema<Vote>( 284 - { 285 - messageID: { 286 - type: String, 287 - required: true, 288 - }, 289 - channelID: { 290 - type: String, 291 - required: true, 292 - }, 293 - options: { 294 - type: [String], 295 - required: true, 296 - }, 297 - topic: { 298 - type: String, 299 - required: true, 300 - }, 301 - date: { 302 - type: Date, 303 - required: true, 304 - index: true, 305 - }, 306 - guildid: { 307 - type: String, 308 - required: true, 309 - }, 310 - authorID: { 311 - type: String, 312 - required: true, 313 - }, 314 - }, 315 - { collection: 'votes' }, 284 + { 285 + messageID: { 286 + type: String, 287 + required: true, 288 + }, 289 + channelID: { 290 + type: String, 291 + required: true, 292 + }, 293 + options: { 294 + type: [String], 295 + required: true, 296 + }, 297 + topic: { 298 + type: String, 299 + required: true, 300 + }, 301 + date: { 302 + type: Date, 303 + required: true, 304 + index: true, 305 + }, 306 + guildid: { 307 + type: String, 308 + required: true, 309 + }, 310 + authorID: { 311 + type: String, 312 + required: true, 313 + }, 314 + }, 315 + { collection: 'votes' }, 316 316 ); 317 317 voteSchema.index({ 318 - messageID: 1, 319 - channelID: 1, 318 + messageID: 1, 319 + channelID: 1, 320 320 }); 321 321 export const VoteModel = mongoose.model<Vote>('votes', voteSchema); 322 322 ··· 324 324 // deprecated, we want to remove this system? 325 325 type StillMuted = { userid: string; guildid: string }; 326 326 const stillMutedSchema = new mongoose.Schema<StillMuted>( 327 - { 328 - userid: { 329 - type: String, 330 - required: true, 331 - }, 332 - guildid: { 333 - type: String, 334 - required: true, 335 - }, 336 - }, 337 - { collection: 'stillMuted' }, 327 + { 328 + userid: { 329 + type: String, 330 + required: true, 331 + }, 332 + guildid: { 333 + type: String, 334 + required: true, 335 + }, 336 + }, 337 + { collection: 'stillMuted' }, 338 338 ); 339 339 stillMutedSchema.index({ 340 - userid: 1, 341 - guildid: 1, 340 + userid: 1, 341 + guildid: 1, 342 342 }); 343 343 export const StillMutedModel = mongoose.model<StillMuted>( 344 - 'stillMuted', 345 - stillMutedSchema, 344 + 'stillMuted', 345 + stillMutedSchema, 346 346 );
+270 -270
src/dbHelpers.ts
··· 1 1 import { 2 - TextChannel, 3 - Guild, 4 - GuildMember, 5 - CommandInteraction, 2 + TextChannel, 3 + Guild, 4 + GuildMember, 5 + CommandInteraction, 6 6 } from 'discord.js'; 7 7 import { 8 - SettingsModel, 9 - UserModel, 10 - Settings, 11 - SaltrankModel, 12 - StillMutedModel, 13 - GlobalUserDataModel, 8 + SettingsModel, 9 + UserModel, 10 + Settings, 11 + SaltrankModel, 12 + StillMutedModel, 13 + GlobalUserDataModel, 14 14 } from './db'; 15 15 import { OWNERID, PREFIXES } from './shared_assets'; 16 16 import config from './configuration'; 17 17 import { sendJoinEvent } from './webhooks'; 18 18 19 19 export async function getUser(userid: string, guildID: string) { 20 - const result = await UserModel.findOneAndUpdate( 21 - { 22 - userID: userid, 23 - guildID, 24 - }, 25 - { 26 - $setOnInsert: { 27 - warnings: 0, 28 - kicks: 0, 29 - bans: 0, 30 - botusage: 0, 31 - sound: undefined, 32 - }, 33 - }, 34 - { 35 - returnOriginal: false, 36 - upsert: true, 37 - }, 38 - ); 39 - return result; 20 + const result = await UserModel.findOneAndUpdate( 21 + { 22 + userID: userid, 23 + guildID, 24 + }, 25 + { 26 + $setOnInsert: { 27 + warnings: 0, 28 + kicks: 0, 29 + bans: 0, 30 + botusage: 0, 31 + sound: undefined, 32 + }, 33 + }, 34 + { 35 + returnOriginal: false, 36 + upsert: true, 37 + }, 38 + ); 39 + return result; 40 40 } 41 41 42 42 export async function getGlobalUser(userId: string) { 43 - const result = await GlobalUserDataModel.findOneAndUpdate( 44 - { 45 - userID: userId, 46 - }, 47 - { 48 - $setOnInsert: { 49 - sound: undefined, 50 - }, 51 - }, 52 - { 53 - returnOriginal: false, 54 - upsert: true, 55 - }, 56 - ); 57 - return result; 43 + const result = await GlobalUserDataModel.findOneAndUpdate( 44 + { 45 + userID: userId, 46 + }, 47 + { 48 + $setOnInsert: { 49 + sound: undefined, 50 + }, 51 + }, 52 + { 53 + returnOriginal: false, 54 + upsert: true, 55 + }, 56 + ); 57 + return result; 58 58 } 59 59 60 60 async function firstSettings(guildID: string) { 61 - const settings = new SettingsModel({ 62 - _id: guildID, 63 - commandChannels: [], 64 - adminRoles: [], 65 - joinChannels: [], 66 - blacklistedUsers: [], 67 - blacklistedEveryone: [], 68 - saltKing: undefined, 69 - saltRole: undefined, 70 - notChannel: undefined, 71 - prefix: config.prefix, 72 - lastConnected: new Date(), 73 - }); 74 - await settings.save(); 75 - return settings; 61 + const settings = new SettingsModel({ 62 + _id: guildID, 63 + commandChannels: [], 64 + adminRoles: [], 65 + joinChannels: [], 66 + blacklistedUsers: [], 67 + blacklistedEveryone: [], 68 + saltKing: undefined, 69 + saltRole: undefined, 70 + notChannel: undefined, 71 + prefix: config.prefix, 72 + lastConnected: new Date(), 73 + }); 74 + await settings.save(); 75 + return settings; 76 76 } 77 77 78 78 export async function getSettings(guildID: string) { 79 - let result = await SettingsModel.findById(guildID); 80 - if (!result) { 81 - await sendJoinEvent( 82 - `:wastebasket: couldn't find settings for guild ${guildID} , resetting/setting empty state. Found this:${JSON.stringify( 83 - result, 84 - null, 85 - 2, 86 - )}`, 87 - ); 79 + let result = await SettingsModel.findById(guildID); 80 + if (!result) { 81 + await sendJoinEvent( 82 + `:wastebasket: couldn't find settings for guild ${guildID} , resetting/setting empty state. Found this:${JSON.stringify( 83 + result, 84 + null, 85 + 2, 86 + )}`, 87 + ); 88 88 89 - result = await firstSettings(guildID); 90 - } 91 - return result; 89 + result = await firstSettings(guildID); 90 + } 91 + return result; 92 92 } 93 93 94 94 export async function checkGuild(id: string) { 95 - // create settings 96 - if (await getSettings(id)) { 97 - return true; 98 - } 99 - return false; 95 + // create settings 96 + if (await getSettings(id)) { 97 + return true; 98 + } 99 + return false; 100 100 } 101 101 102 102 export async function setSettings( 103 - guildID: string, 104 - // this was difficult to find, but is awesome 105 - update: { [Property in keyof Settings]?: Settings[Property] }, 103 + guildID: string, 104 + // this was difficult to find, but is awesome 105 + update: { [Property in keyof Settings]?: Settings[Property] }, 106 106 ) { 107 - if (await getSettings(guildID)) { 108 - await SettingsModel.updateOne({ _id: guildID }, { $set: update }); 109 - } 110 - return true; 107 + if (await getSettings(guildID)) { 108 + await SettingsModel.updateOne({ _id: guildID }, { $set: update }); 109 + } 110 + return true; 111 111 } 112 112 113 113 async function getSaltKing(guildID: string) { 114 - const settings = await getSettings(guildID); 115 - return settings.saltKing; 114 + const settings = await getSettings(guildID); 115 + return settings.saltKing; 116 116 } 117 117 118 118 async function getSaltRole(guildID: string) { 119 - const set = await getSettings(guildID); 120 - return set.saltRole; 119 + const set = await getSettings(guildID); 120 + return set.saltRole; 121 121 } 122 122 123 123 async function setSaltRole(guildID: string, roleID: string) { 124 - await setSettings(guildID, { saltRole: roleID }); 124 + await setSettings(guildID, { saltRole: roleID }); 125 125 } 126 126 127 127 export async function getNotChannel(guildID: string) { 128 - const set = await getSettings(guildID); 129 - return set.notChannel; 128 + const set = await getSettings(guildID); 129 + return set.notChannel; 130 130 } 131 131 132 132 function setSaltKing(guildID: string, userID: string) { 133 - return setSettings(guildID, { saltKing: userID }); 133 + return setSettings(guildID, { saltKing: userID }); 134 134 } 135 135 136 136 // top 5 salty people 137 137 export async function topSalt(guildID: string) { 138 - const result = await SaltrankModel.find({ guild: guildID }) 139 - .sort({ salt: -1 }) 140 - .limit(5); 141 - if (!result) { 142 - return []; 143 - } 144 - return result; 138 + const result = await SaltrankModel.find({ guild: guildID }) 139 + .sort({ salt: -1 }) 140 + .limit(5); 141 + if (!result) { 142 + return []; 143 + } 144 + return result; 145 145 } 146 146 147 147 export async function updateSaltKing(G: Guild) { 148 - if (G.available && G.me) { 149 - if (G.me.permissions.has('MANAGE_ROLES', true)) { 150 - const SaltKing = await getSaltKing(G.id); 151 - let SaltRole = await getSaltRole(G.id); 152 - const groles = G.roles; 153 - if (!SaltRole || !groles.cache.has(SaltRole)) { 154 - if (G.roles.cache.size < 250) { 155 - await G.roles 156 - .create({ 157 - name: 'SaltKing', 158 - color: '#FFFFFF', 159 - position: 0, 160 - permissions: [], 161 - mentionable: true, 162 - reason: 148 + if (G.available && G.me) { 149 + if (G.me.permissions.has('MANAGE_ROLES', true)) { 150 + const SaltKing = await getSaltKing(G.id); 151 + let SaltRole = await getSaltRole(G.id); 152 + const groles = G.roles; 153 + if (!SaltRole || !groles.cache.has(SaltRole)) { 154 + if (G.roles.cache.size < 250) { 155 + await G.roles 156 + .create({ 157 + name: 'SaltKing', 158 + color: '#FFFFFF', 159 + position: 0, 160 + permissions: [], 161 + mentionable: true, 162 + reason: 163 163 'SaltKing role needed for Saltranking to work. You can adjust this role if you like.', 164 - }) 165 - .then(async (role) => { 166 - await setSaltRole(G.id, role.id); 167 - SaltRole = role.id; 168 - }); 169 - } else { 170 - const channel = await getNotChannel(G.id); 171 - if (channel) { 172 - const chan = G.channels.cache.get(channel); 173 - if (chan) { 174 - const perms = chan.permissionsFor(G.me); 175 - if (perms && perms.has('SEND_MESSAGES')) { 176 - const owner = await G.fetchOwner(); 177 - (chan as TextChannel).send( 178 - `Hey there ${owner}!\nI regret to inform you that this server has 250 roles and I therefore can't add SaltKing. If you want to manage the role yourself delete one and then just change the settings of the role i create automatically.`, 179 - ); 180 - } 181 - } 182 - } 183 - return; 184 - } 185 - } 186 - const sltID = await topSalt(G.id); 187 - let saltID: string | undefined; 188 - if (sltID[0]) { 189 - saltID = sltID[0].salter; 190 - } 191 - if (!SaltRole) { 192 - throw new Error('For some reason, there was no SaltRole.'); 193 - } 194 - const role = await groles.fetch(SaltRole); 195 - if (role && role.position < G.me.roles.highest.position) { 196 - if (SaltKing && saltID !== SaltKing) { 197 - const user = await G.members.fetch(SaltKing).catch(() => {}); 198 - if (user) { 199 - user.roles.remove(SaltRole, 'Is not as salty anymore'); 200 - } 201 - } 202 - if (saltID) { 203 - const nuser = await G.members.fetch(saltID).catch(() => {}); 204 - if (nuser) { 205 - if (!nuser.roles.cache.has(SaltRole)) { 206 - await nuser.roles.add(SaltRole, 'Saltiest user'); 207 - } 208 - } 209 - if (saltID !== SaltKing) { 210 - await setSaltKing(G.id, saltID); 211 - } 212 - } 213 - } else { 214 - const channel = await getNotChannel(G.id); 215 - if (channel) { 216 - const chan = G.channels.cache.get(channel); 217 - if (chan) { 218 - const perms = chan.permissionsFor(G.me); 219 - if (perms && perms.has('SEND_MESSAGES')) { 220 - const owner = await G.fetchOwner(); 221 - (chan as TextChannel).send( 222 - `Hey there ${owner}!\nI regret to inform you that my highest role is beneath <@&${SaltRole}>, which has the effect that i cannot give or take if from users.`, 223 - ); 224 - } 225 - } 226 - } 227 - } 228 - } else { 229 - const channel = await getNotChannel(G.id); 230 - if (channel) { 231 - const chan = G.channels.cache.get(channel); 232 - if (chan) { 233 - const perms = chan.permissionsFor(G.me); 234 - if (perms && perms.has('SEND_MESSAGES')) { 235 - const owner = await G.fetchOwner(); 236 - (chan as TextChannel).send( 237 - `Hey there ${owner}!\nI regret to inform you that i have no permission to manage roles and therefore can't manage the SaltKing role.`, 238 - ); 239 - } 240 - } 241 - } 242 - } 243 - } 164 + }) 165 + .then(async (role) => { 166 + await setSaltRole(G.id, role.id); 167 + SaltRole = role.id; 168 + }); 169 + } else { 170 + const channel = await getNotChannel(G.id); 171 + if (channel) { 172 + const chan = G.channels.cache.get(channel); 173 + if (chan) { 174 + const perms = chan.permissionsFor(G.me); 175 + if (perms && perms.has('SEND_MESSAGES')) { 176 + const owner = await G.fetchOwner(); 177 + (chan as TextChannel).send( 178 + `Hey there ${owner}!\nI regret to inform you that this server has 250 roles and I therefore can't add SaltKing. If you want to manage the role yourself delete one and then just change the settings of the role i create automatically.`, 179 + ); 180 + } 181 + } 182 + } 183 + return; 184 + } 185 + } 186 + const sltID = await topSalt(G.id); 187 + let saltID: string | undefined; 188 + if (sltID[0]) { 189 + saltID = sltID[0].salter; 190 + } 191 + if (!SaltRole) { 192 + throw new Error('For some reason, there was no SaltRole.'); 193 + } 194 + const role = await groles.fetch(SaltRole); 195 + if (role && role.position < G.me.roles.highest.position) { 196 + if (SaltKing && saltID !== SaltKing) { 197 + const user = await G.members.fetch(SaltKing).catch(() => {}); 198 + if (user) { 199 + user.roles.remove(SaltRole, 'Is not as salty anymore'); 200 + } 201 + } 202 + if (saltID) { 203 + const nuser = await G.members.fetch(saltID).catch(() => {}); 204 + if (nuser) { 205 + if (!nuser.roles.cache.has(SaltRole)) { 206 + await nuser.roles.add(SaltRole, 'Saltiest user'); 207 + } 208 + } 209 + if (saltID !== SaltKing) { 210 + await setSaltKing(G.id, saltID); 211 + } 212 + } 213 + } else { 214 + const channel = await getNotChannel(G.id); 215 + if (channel) { 216 + const chan = G.channels.cache.get(channel); 217 + if (chan) { 218 + const perms = chan.permissionsFor(G.me); 219 + if (perms && perms.has('SEND_MESSAGES')) { 220 + const owner = await G.fetchOwner(); 221 + (chan as TextChannel).send( 222 + `Hey there ${owner}!\nI regret to inform you that my highest role is beneath <@&${SaltRole}>, which has the effect that i cannot give or take if from users.`, 223 + ); 224 + } 225 + } 226 + } 227 + } 228 + } else { 229 + const channel = await getNotChannel(G.id); 230 + if (channel) { 231 + const chan = G.channels.cache.get(channel); 232 + if (chan) { 233 + const perms = chan.permissionsFor(G.me); 234 + if (perms && perms.has('SEND_MESSAGES')) { 235 + const owner = await G.fetchOwner(); 236 + (chan as TextChannel).send( 237 + `Hey there ${owner}!\nI regret to inform you that i have no permission to manage roles and therefore can't manage the SaltKing role.`, 238 + ); 239 + } 240 + } 241 + } 242 + } 243 + } 244 244 } 245 245 246 246 export async function isJoinableVc(guildID: string, channelID: string) { 247 - const settings = await getSettings(guildID); 248 - return settings.joinChannels.includes(channelID); 247 + const settings = await getSettings(guildID); 248 + return settings.joinChannels.includes(channelID); 249 249 } 250 250 251 251 export async function setPrefix(guildID: string, prefix: string) { 252 - const success = await setSettings(guildID, { 253 - prefix, 254 - }); 255 - if (success) { 256 - PREFIXES.set(guildID, prefix); 257 - } 258 - return success; 252 + const success = await setSettings(guildID, { 253 + prefix, 254 + }); 255 + if (success) { 256 + PREFIXES.set(guildID, prefix); 257 + } 258 + return success; 259 259 } 260 260 261 261 export async function getPrefix(guildID: string) { 262 - const settings = await getSettings(guildID); 263 - const { prefix } = settings; 264 - if (!prefix) { 265 - await setPrefix(guildID, config.prefix); 266 - return config.prefix; 267 - } 268 - return prefix; 262 + const settings = await getSettings(guildID); 263 + const { prefix } = settings; 264 + if (!prefix) { 265 + await setPrefix(guildID, config.prefix); 266 + return config.prefix; 267 + } 268 + return prefix; 269 269 } 270 270 271 271 export async function toggleStillMuted( 272 - userID: string, 273 - guildID: string, 274 - add: boolean, 272 + userID: string, 273 + guildID: string, 274 + add: boolean, 275 275 ) { 276 - if (add) { 277 - const isStillMutedAmount = await StillMutedModel.find({ 278 - userid: userID, 279 - guildid: guildID, 280 - }).count(); 281 - if (isStillMutedAmount === 0) { 282 - const newMute = new StillMutedModel({ 283 - userid: userID, 284 - guildid: guildID, 285 - }); 286 - await newMute.save(); 287 - } 288 - } else { 289 - await StillMutedModel.deleteMany({ 290 - userid: userID, 291 - guildid: guildID, 292 - }); 293 - } 276 + if (add) { 277 + const isStillMutedAmount = await StillMutedModel.find({ 278 + userid: userID, 279 + guildid: guildID, 280 + }).count(); 281 + if (isStillMutedAmount === 0) { 282 + const newMute = new StillMutedModel({ 283 + userid: userID, 284 + guildid: guildID, 285 + }); 286 + await newMute.save(); 287 + } 288 + } else { 289 + await StillMutedModel.deleteMany({ 290 + userid: userID, 291 + guildid: guildID, 292 + }); 293 + } 294 294 } 295 295 296 296 export async function getAdminRoles(guildID: string) { 297 - const settings = await getSettings(guildID); 298 - return settings.adminRoles; 297 + const settings = await getSettings(guildID); 298 + return settings.adminRoles; 299 299 } 300 300 301 301 export async function isAdmin(guildID: string, member: GuildMember) { 302 - // checks for admin and Owner, they can always use 303 - if (member.permissions.has('ADMINISTRATOR', true)) { 304 - return true; 305 - } 306 - // Owner of bot is always admin hehe 307 - if (member.id === OWNERID) { 308 - return true; 309 - } 310 - const roles = await getAdminRoles(guildID); 311 - return member.roles.cache.hasAny(...roles); 302 + // checks for admin and Owner, they can always use 303 + if (member.permissions.has('ADMINISTRATOR', true)) { 304 + return true; 305 + } 306 + // Owner of bot is always admin hehe 307 + if (member.id === OWNERID) { 308 + return true; 309 + } 310 + const roles = await getAdminRoles(guildID); 311 + return member.roles.cache.hasAny(...roles); 312 312 } 313 313 314 314 export async function interactionMemberIsAdmin( 315 - interaction: CommandInteraction, 315 + interaction: CommandInteraction, 316 316 ) { 317 - const { member, guild } = interaction; 318 - if (member instanceof GuildMember) { 319 - // checks for admin and Owner, they can always use 320 - if (member.permissions.has('ADMINISTRATOR', true)) { 321 - return true; 322 - } 323 - const roles = await getAdminRoles(guild!.id); 324 - return member.roles.cache.hasAny(...roles); 325 - } 326 - if (member) { 327 - // checks for admin and Owner, they can always use 328 - if (interaction.memberPermissions?.has('ADMINISTRATOR', true)) { 329 - return true; 330 - } 331 - } 332 - // Owner of bot is always admin hehe 333 - if (interaction.user.id === OWNERID) { 334 - return true; 335 - } 336 - // default: no admin 337 - return false; 317 + const { member, guild } = interaction; 318 + if (member instanceof GuildMember) { 319 + // checks for admin and Owner, they can always use 320 + if (member.permissions.has('ADMINISTRATOR', true)) { 321 + return true; 322 + } 323 + const roles = await getAdminRoles(guild!.id); 324 + return member.roles.cache.hasAny(...roles); 325 + } 326 + if (member) { 327 + // checks for admin and Owner, they can always use 328 + if (interaction.memberPermissions?.has('ADMINISTRATOR', true)) { 329 + return true; 330 + } 331 + } 332 + // Owner of bot is always admin hehe 333 + if (interaction.user.id === OWNERID) { 334 + return true; 335 + } 336 + // default: no admin 337 + return false; 338 338 } 339 339 340 340 export async function getCommandChannels(guildID: string) { 341 - const settings = await getSettings(guildID); 342 - return settings.commandChannels; 341 + const settings = await getSettings(guildID); 342 + return settings.commandChannels; 343 343 } 344 344 345 345 export async function isBlacklistedUser(userid: string, guildID: string) { 346 - const settings = await getSettings(guildID); 347 - const users = settings.blacklistedUsers; 348 - return users.includes(userid); 346 + const settings = await getSettings(guildID); 347 + const users = settings.blacklistedUsers; 348 + return users.includes(userid); 349 349 }
+190 -190
src/helperFunctions.ts
··· 1 1 import Discord, { 2 - Message, 3 - MessageActionRow, 4 - MessageButton, 5 - MessageComponentInteraction, 2 + Message, 3 + MessageActionRow, 4 + MessageButton, 5 + MessageComponentInteraction, 6 6 } from 'discord.js'; 7 7 8 8 export function doNothingOnError() {} 9 9 10 10 export function returnNullOnError() { 11 - return null; 11 + return null; 12 12 } 13 13 export async function findMember( 14 - guild: Discord.Guild, 15 - userMention: string, 14 + guild: Discord.Guild, 15 + userMention: string, 16 16 ): Promise<{ user: Discord.GuildMember | null; fuzzy?: boolean }> { 17 - if (!userMention || userMention.length === 0) { 18 - return { user: null }; 19 - } 20 - let mention = userMention.toLowerCase(); 21 - if (mention.startsWith('<@') && mention.endsWith('>')) { 22 - mention = mention.slice(2, -1); 23 - if (mention.startsWith('!')) { 24 - mention = mention.substr(1); 25 - } 26 - } 27 - // fails if its not a snowflake 28 - const user = await guild.members.fetch(mention).catch(returnNullOnError); 29 - if (user) { 30 - return { user, fuzzy: false }; 31 - } 32 - // it will sometimes only find one if multiple would fit (even with higher limit). 33 - // so we just call it fuzzy and take the first we get 34 - const membersFound = await guild.members.search({ query: mention, limit: 1 }); 35 - if (membersFound.size >= 1) { 36 - return { user: membersFound.first()!, fuzzy: true }; 37 - } 38 - return { user: null }; 17 + if (!userMention || userMention.length === 0) { 18 + return { user: null }; 19 + } 20 + let mention = userMention.toLowerCase(); 21 + if (mention.startsWith('<@') && mention.endsWith('>')) { 22 + mention = mention.slice(2, -1); 23 + if (mention.startsWith('!')) { 24 + mention = mention.substr(1); 25 + } 26 + } 27 + // fails if its not a snowflake 28 + const user = await guild.members.fetch(mention).catch(returnNullOnError); 29 + if (user) { 30 + return { user, fuzzy: false }; 31 + } 32 + // it will sometimes only find one if multiple would fit (even with higher limit). 33 + // so we just call it fuzzy and take the first we get 34 + const membersFound = await guild.members.search({ query: mention, limit: 1 }); 35 + if (membersFound.size >= 1) { 36 + return { user: membersFound.first()!, fuzzy: true }; 37 + } 38 + return { user: null }; 39 39 } 40 40 41 41 export async function findRole(guild: Discord.Guild, ment: string) { 42 - if (!ment) { 43 - return false; 44 - } 45 - const mention = ment.toLowerCase(); 46 - if (mention.startsWith('<@&') && mention.endsWith('>')) { 47 - const id = mention.substr(3).slice(0, -1); 48 - return id; 49 - } 50 - const role = await guild.roles.fetch(mention); 51 - if (role) { 52 - return role.id; 53 - } 54 - if (mention.length >= 3) { 55 - let roleArray = guild.roles.cache.filter((rol) => rol.name.toLowerCase().startsWith(mention)); 56 - if (roleArray.size === 1) { 57 - return roleArray.first()!.id; 58 - } 59 - if (roleArray.size === 0) { 60 - roleArray = guild.roles.cache.filter((rol) => rol.name.toLowerCase().includes(mention)); 61 - if (roleArray.size === 1) { 62 - return roleArray.first()!.id; 63 - } 64 - } 65 - } 66 - return false; 42 + if (!ment) { 43 + return false; 44 + } 45 + const mention = ment.toLowerCase(); 46 + if (mention.startsWith('<@&') && mention.endsWith('>')) { 47 + const id = mention.substr(3).slice(0, -1); 48 + return id; 49 + } 50 + const role = await guild.roles.fetch(mention); 51 + if (role) { 52 + return role.id; 53 + } 54 + if (mention.length >= 3) { 55 + let roleArray = guild.roles.cache.filter((rol) => rol.name.toLowerCase().startsWith(mention)); 56 + if (roleArray.size === 1) { 57 + return roleArray.first()!.id; 58 + } 59 + if (roleArray.size === 0) { 60 + roleArray = guild.roles.cache.filter((rol) => rol.name.toLowerCase().includes(mention)); 61 + if (roleArray.size === 1) { 62 + return roleArray.first()!.id; 63 + } 64 + } 65 + } 66 + return false; 67 67 } 68 68 69 69 // for some reason eslint doesnt get this... ··· 76 76 // this is an idea to implement rather reusable confirmation processes. 77 77 // ; abortMessage, timeoutMessage and time are optional parameters 78 78 export async function yesOrNo( 79 - msg: Discord.Message, 80 - question: string, 81 - abortMessage?: string, 82 - timeoutMessage?: string, 83 - timeoutTime?: number, 79 + msg: Discord.Message, 80 + question: string, 81 + abortMessage?: string, 82 + timeoutMessage?: string, 83 + timeoutTime?: number, 84 84 ): Promise<boolean> { 85 - const row = new MessageActionRow(); 86 - row.addComponents( 87 - new MessageButton() 88 - .setCustomId(`${buttonId.yesOrNo}-${msg.id}-yes`) 89 - .setLabel('Yes') 90 - .setStyle('SUCCESS'), 91 - ); 92 - row.addComponents( 93 - new MessageButton() 94 - .setCustomId(`${buttonId.yesOrNo}-${msg.id}-no`) 95 - .setLabel('No') 96 - .setStyle('DANGER'), 97 - ); 98 - const questionMessage = await msg.channel.send({ 99 - content: question, 100 - components: [row], 101 - }); 102 - const time = timeoutTime || 20000; 103 - const messageForTimeout = timeoutMessage || 'Cancelled due to timeout.'; 104 - // only accept reactions from the user that created this question 105 - const filter = (interaction: MessageComponentInteraction) => interaction.user.id === msg.author.id 85 + const row = new MessageActionRow(); 86 + row.addComponents( 87 + new MessageButton() 88 + .setCustomId(`${buttonId.yesOrNo}-${msg.id}-yes`) 89 + .setLabel('Yes') 90 + .setStyle('SUCCESS'), 91 + ); 92 + row.addComponents( 93 + new MessageButton() 94 + .setCustomId(`${buttonId.yesOrNo}-${msg.id}-no`) 95 + .setLabel('No') 96 + .setStyle('DANGER'), 97 + ); 98 + const questionMessage = await msg.channel.send({ 99 + content: question, 100 + components: [row], 101 + }); 102 + const time = timeoutTime || 20000; 103 + const messageForTimeout = timeoutMessage || 'Cancelled due to timeout.'; 104 + // only accept reactions from the user that created this question 105 + const filter = (interaction: MessageComponentInteraction) => interaction.user.id === msg.author.id 106 106 && interaction.customId.startsWith(`${buttonId.yesOrNo}-${msg.id}-`); 107 - const collector = questionMessage.createMessageComponentCollector({ 108 - filter, 109 - time, 110 - }); 111 - const promise = new Promise<boolean>((resolve) => { 112 - let alreadyResolved = false; 113 - collector.once('collect', async (interaction) => { 114 - alreadyResolved = true; 115 - // load info of that button 116 - const idParts = interaction.customId.split('-'); 117 - const isYesButton = idParts[2] === 'yes'; 118 - questionMessage.delete(); 119 - if (!isYesButton && abortMessage) { 120 - questionMessage.channel.send(abortMessage).catch(doNothingOnError); 121 - } 122 - resolve(isYesButton); 123 - collector.stop('Got an answer.'); 124 - }); 125 - collector.once('end', (/* collected */) => { 126 - if (!alreadyResolved) { 127 - msg.channel.send(messageForTimeout); 128 - resolve(false); 129 - } 130 - }); 131 - }); 132 - return promise; 107 + const collector = questionMessage.createMessageComponentCollector({ 108 + filter, 109 + time, 110 + }); 111 + const promise = new Promise<boolean>((resolve) => { 112 + let alreadyResolved = false; 113 + collector.once('collect', async (interaction) => { 114 + alreadyResolved = true; 115 + // load info of that button 116 + const idParts = interaction.customId.split('-'); 117 + const isYesButton = idParts[2] === 'yes'; 118 + questionMessage.delete(); 119 + if (!isYesButton && abortMessage) { 120 + questionMessage.channel.send(abortMessage).catch(doNothingOnError); 121 + } 122 + resolve(isYesButton); 123 + collector.stop('Got an answer.'); 124 + }); 125 + collector.once('end', (/* collected */) => { 126 + if (!alreadyResolved) { 127 + msg.channel.send(messageForTimeout); 128 + resolve(false); 129 + } 130 + }); 131 + }); 132 + return promise; 133 133 } 134 134 135 135 export async function notifyAboutSlashCommand( 136 - message: Message, 137 - command: string, 136 + message: Message, 137 + command: string, 138 138 ) { 139 - await message.reply(`This command has been moved to application (/) commands! You can simply use it by typing \`/${command}\`! 140 - 139 + await message.reply(`This command has been moved to application (/) commands! You can simply use it by typing \`/${command}\`! 140 + 141 141 If you can't find it, either you are missing permissions, or the admins of this server have not given MagiBot permission to create application commands yet. 142 142 To do the latter, re-invite the bot by clicking the big blue "Add to Server" button in the bot's profile!`); 143 143 } ··· 145 145 // this is an idea to implement rather reusable confirmation processes. 146 146 // ; abortMessage, timeoutMessage and time are optional parameters 147 147 export async function interactionConfirmation( 148 - interaction: Discord.CommandInteraction, 149 - question: string, 150 - abortMessage?: string, 151 - timeoutMessage?: string, 152 - timeoutTime?: number, 148 + interaction: Discord.CommandInteraction, 149 + question: string, 150 + abortMessage?: string, 151 + timeoutMessage?: string, 152 + timeoutTime?: number, 153 153 ): Promise<MessageComponentInteraction | null> { 154 - const row = new MessageActionRow(); 155 - row.addComponents( 156 - new MessageButton() 157 - .setCustomId(`${buttonId.yesOrNo}-${interaction.id}-yes`) 158 - .setLabel('Yes') 159 - .setStyle('SUCCESS'), 160 - ); 161 - row.addComponents( 162 - new MessageButton() 163 - .setCustomId(`${buttonId.yesOrNo}-${interaction.id}-no`) 164 - .setLabel('No') 165 - .setStyle('DANGER'), 166 - ); 167 - await interaction.reply({ 168 - content: question, 169 - components: [row], 170 - }); 171 - const questionMessage = (await interaction.fetchReply()) as Discord.Message; 172 - const time = timeoutTime || 20000; 173 - const messageForTimeout = timeoutMessage || 'Cancelled due to timeout.'; 174 - // only accept reactions from the user that created this question 175 - // eslint-disable-next-line max-len 176 - const filter = (intraction: MessageComponentInteraction) => intraction.user.id === interaction.member?.user.id 154 + const row = new MessageActionRow(); 155 + row.addComponents( 156 + new MessageButton() 157 + .setCustomId(`${buttonId.yesOrNo}-${interaction.id}-yes`) 158 + .setLabel('Yes') 159 + .setStyle('SUCCESS'), 160 + ); 161 + row.addComponents( 162 + new MessageButton() 163 + .setCustomId(`${buttonId.yesOrNo}-${interaction.id}-no`) 164 + .setLabel('No') 165 + .setStyle('DANGER'), 166 + ); 167 + await interaction.reply({ 168 + content: question, 169 + components: [row], 170 + }); 171 + const questionMessage = (await interaction.fetchReply()) as Discord.Message; 172 + const time = timeoutTime || 20000; 173 + const messageForTimeout = timeoutMessage || 'Cancelled due to timeout.'; 174 + // only accept reactions from the user that created this question 175 + // eslint-disable-next-line max-len 176 + const filter = (intraction: MessageComponentInteraction) => intraction.user.id === interaction.member?.user.id 177 177 && intraction.customId.startsWith(`${buttonId.yesOrNo}-${interaction.id}-`); 178 - const collector = questionMessage.createMessageComponentCollector({ 179 - filter, 180 - time, 181 - }); 182 - const promise = new Promise<MessageComponentInteraction | null>((resolve) => { 183 - let alreadyResolved = false; 184 - collector.once('collect', async (collectionInteraction) => { 185 - alreadyResolved = true; 186 - // load info of that button 187 - const idParts = collectionInteraction.customId.split('-'); 188 - const isYesButton = idParts[2] === 'yes'; 189 - await questionMessage.delete(); 190 - if (!isYesButton && abortMessage) { 191 - collectionInteraction.reply({ content: abortMessage, ephemeral: true }); 192 - } 193 - resolve(isYesButton ? collectionInteraction : null); 194 - collector.stop('Got an answer.'); 195 - }); 196 - collector.once('end', async () => { 197 - if (!alreadyResolved) { 198 - await questionMessage.delete(); 199 - interaction.followUp({ content: messageForTimeout, ephemeral: true }); 200 - resolve(null); 201 - } 202 - }); 203 - }); 204 - return promise; 178 + const collector = questionMessage.createMessageComponentCollector({ 179 + filter, 180 + time, 181 + }); 182 + const promise = new Promise<MessageComponentInteraction | null>((resolve) => { 183 + let alreadyResolved = false; 184 + collector.once('collect', async (collectionInteraction) => { 185 + alreadyResolved = true; 186 + // load info of that button 187 + const idParts = collectionInteraction.customId.split('-'); 188 + const isYesButton = idParts[2] === 'yes'; 189 + await questionMessage.delete(); 190 + if (!isYesButton && abortMessage) { 191 + collectionInteraction.reply({ content: abortMessage, ephemeral: true }); 192 + } 193 + resolve(isYesButton ? collectionInteraction : null); 194 + collector.stop('Got an answer.'); 195 + }); 196 + collector.once('end', async () => { 197 + if (!alreadyResolved) { 198 + await questionMessage.delete(); 199 + interaction.followUp({ content: messageForTimeout, ephemeral: true }); 200 + resolve(null); 201 + } 202 + }); 203 + }); 204 + return promise; 205 205 } 206 206 export function printError(error: { 207 207 response: { status: string; statusText: string }; 208 208 }) { 209 - console.error( 210 - `Errorstatus: ${error.response.status} ${error.response.statusText}`, 211 - ); 209 + console.error( 210 + `Errorstatus: ${error.response.status} ${error.response.statusText}`, 211 + ); 212 212 } 213 213 214 214 export async function asyncForEach<T, F, O>( 215 - values: Array<T> | Discord.Collection<string | number, T>, 216 - callback: ( 215 + values: Array<T> | Discord.Collection<string | number, T>, 216 + callback: ( 217 217 input: T, 218 218 index: number | string, 219 219 optionalParams?: O 220 220 ) => Promise<F>, 221 - optParams?: O, 221 + optParams?: O, 222 222 ) { 223 - if (Array.isArray(values)) { 224 - const arr = values.map((e, i) => callback(e, i, optParams)); 225 - return Promise.all<F>(arr); 226 - } 227 - const arr = values.map((e, i) => callback(e, i, optParams)); 228 - return Promise.all<F>(arr); 223 + if (Array.isArray(values)) { 224 + const arr = values.map((e, i) => callback(e, i, optParams)); 225 + return Promise.all<F>(arr); 226 + } 227 + const arr = values.map((e, i) => callback(e, i, optParams)); 228 + return Promise.all<F>(arr); 229 229 } 230 230 231 231 // unused 232 232 export async function asyncForEachFromFlint<T, F, N, O>( 233 - array: Array<T> | Map<N, T>, 234 - callback: (input: T, index: number | N, optParams?: O) => Promise<F>, 235 - optionalParams?: O, 233 + array: Array<T> | Map<N, T>, 234 + callback: (input: T, index: number | N, optParams?: O) => Promise<F>, 235 + optionalParams?: O, 236 236 ) { 237 - if (Array.isArray(array)) { 238 - const arr = array.map((e, i) => callback(e, i, optionalParams)); 239 - return Promise.all<F>(arr); 240 - } 241 - const arr: Array<Promise<F>> = []; 242 - array.forEach((e, i) => arr.push(callback(e, i, optionalParams))); 243 - return Promise.all<F>(arr); 237 + if (Array.isArray(array)) { 238 + const arr = array.map((e, i) => callback(e, i, optionalParams)); 239 + return Promise.all<F>(arr); 240 + } 241 + const arr: Array<Promise<F>> = []; 242 + array.forEach((e, i) => arr.push(callback(e, i, optionalParams))); 243 + return Promise.all<F>(arr); 244 244 } 245 245 246 246 export async function asyncWait(ms: number) { 247 - return new Promise((resolve) => { 248 - setTimeout(resolve, ms); 249 - }); 247 + return new Promise((resolve) => { 248 + setTimeout(resolve, ms); 249 + }); 250 250 }
+8 -8
src/index.ts
··· 3 3 import configuration from './configuration'; 4 4 import { syncCommands } from './commandSync'; 5 5 /* import { 6 - accumulateJoinsoundsPlayed, 7 - accumulateUsersJoinedQueue, 6 + accumulateJoinsoundsPlayed, 7 + accumulateUsersJoinedQueue, 8 8 } from './statTracking'; */ 9 9 10 10 export const TOKEN = configuration.tk; 11 11 12 12 const manager = new ShardingManager('dist/bot.js', { 13 - token: TOKEN, 13 + token: TOKEN, 14 14 }); 15 15 16 16 if (!process.env.STATCORD_TOKEN) { 17 - throw new Error('Statcord token missing!'); 17 + throw new Error('Statcord token missing!'); 18 18 } 19 19 20 20 export const statcord = new Statcord.ShardingClient({ 21 - manager, 22 - key: process.env.STATCORD_TOKEN, 21 + manager, 22 + key: process.env.STATCORD_TOKEN, 23 23 }); 24 24 25 25 // register custom stats ··· 27 27 // statcord.registerCustomFieldHandler(2, accumulateUsersJoinedQueue); 28 28 29 29 statcord.on('autopost-start', () => { 30 - // Emitted when statcord autopost starts 31 - console.log('[Statcord]: Started autopost'); 30 + // Emitted when statcord autopost starts 31 + console.log('[Statcord]: Started autopost'); 32 32 }); 33 33 34 34 manager.on('shardCreate', (shard) => console.log(`Launched shard ${shard.id}`));
+15 -15
src/sendToMyDiscord.ts
··· 3 3 const maxMessageLength = 1950; 4 4 5 5 export async function catchErrorOnDiscord(message: string, shardId?: number) { 6 - try { 7 - let idx = 0; 8 - for (let i = 0; i < message.length; i += maxMessageLength) { 9 - idx++; 10 - // I want this to never hit ratelimit, so let's take it slow 11 - // eslint-disable-next-line no-await-in-loop 12 - await sendException( 13 - `${idx}: ${message.substring(i, i + maxMessageLength)}`, 14 - shardId, 15 - ); 16 - } 17 - } catch (error) { 18 - // eslint-disable-next-line no-console 19 - console.error(error); 20 - } 6 + try { 7 + let idx = 0; 8 + for (let i = 0; i < message.length; i += maxMessageLength) { 9 + idx++; 10 + // I want this to never hit ratelimit, so let's take it slow 11 + // eslint-disable-next-line no-await-in-loop 12 + await sendException( 13 + `${idx}: ${message.substring(i, i + maxMessageLength)}`, 14 + shardId, 15 + ); 16 + } 17 + } catch (error) { 18 + // eslint-disable-next-line no-console 19 + console.error(error); 20 + } 21 21 }
+41 -41
src/shared_assets.ts
··· 17 17 export const queueVoiceChannels: Map<string, string> = new Map(); 18 18 19 19 export function sendNotification( 20 - info: string, 21 - type: string, 22 - msg: Discord.Message, 20 + info: string, 21 + type: string, 22 + msg: Discord.Message, 23 23 ) { 24 - let icolor: number | undefined; 24 + let icolor: number | undefined; 25 25 26 - if (type === 'success') { 27 - icolor = SUCCESS_COLOR; 28 - } else if (type === 'error') { 29 - icolor = ERROR_COLOR; 30 - } else if (type === 'info') { 31 - icolor = INFO_COLOR; 32 - } else { 33 - icolor = COLOR; 34 - } 26 + if (type === 'success') { 27 + icolor = SUCCESS_COLOR; 28 + } else if (type === 'error') { 29 + icolor = ERROR_COLOR; 30 + } else if (type === 'info') { 31 + icolor = INFO_COLOR; 32 + } else { 33 + icolor = COLOR; 34 + } 35 35 36 - const embed = { 37 - color: icolor, 38 - description: info, 39 - }; 40 - msg.channel.send({ embeds: [embed] }); 36 + const embed = { 37 + color: icolor, 38 + description: info, 39 + }; 40 + msg.channel.send({ embeds: [embed] }); 41 41 } 42 42 43 43 let bot_user: ClientUser; 44 44 45 45 export function setUser(usr: ClientUser) { 46 - bot_user = usr; 46 + bot_user = usr; 47 47 } 48 48 49 49 export function user() { 50 - return bot_user; 50 + return bot_user; 51 51 } 52 52 53 53 export function resetPrefixes() { 54 - PREFIXES.clear(); 54 + PREFIXES.clear(); 55 55 } 56 56 57 57 // for shadowbanned servers we want do deny 58 58 const shadowbannedGuilds = new Map([ 59 - // rik and stefans server 60 - ['859803064537186334', true], 61 - // test: teabots : seems to work! 62 - // ['380669498014957569', true], 59 + // rik and stefans server 60 + ['859803064537186334', true], 61 + // test: teabots : seems to work! 62 + // ['380669498014957569', true], 63 63 ]); 64 64 const shadowbannedUsers = new Map([ 65 - // rik 66 - ['206502772533624832', true], 67 - // stefan 68 - ['223967778615459842', true], 69 - // test nico 70 - // ['185845248163840002', true], 65 + // rik 66 + ['206502772533624832', true], 67 + // stefan 68 + ['223967778615459842', true], 69 + // test nico 70 + // ['185845248163840002', true], 71 71 ]); 72 72 73 73 // eslint-disable-next-line no-shadow ··· 78 78 } 79 79 80 80 export function isShadowBanned( 81 - userid: string, 82 - guildid: string, 83 - guildOwnerId: string, 81 + userid: string, 82 + guildid: string, 83 + guildOwnerId: string, 84 84 ) { 85 - if (shadowbannedGuilds.get(guildid) || shadowbannedUsers.get(guildOwnerId)) { 86 - return shadowBannedLevel.guild; 87 - } 88 - if (shadowbannedUsers.get(userid)) { 89 - return shadowBannedLevel.member; 90 - } 91 - return shadowBannedLevel.not; 85 + if (shadowbannedGuilds.get(guildid) || shadowbannedUsers.get(guildOwnerId)) { 86 + return shadowBannedLevel.guild; 87 + } 88 + if (shadowbannedUsers.get(userid)) { 89 + return shadowBannedLevel.member; 90 + } 91 + return shadowBannedLevel.not; 92 92 } 93 93 export const shadowBannedSound = 'https://www.myinstants.com/media/sounds/wet-fart_1.mp3';
+61 -59
src/statTracking.ts
··· 2 2 import { ShardingManager } from 'discord.js'; 3 3 import { promises as fsp } from 'fs'; 4 4 import path from 'path'; 5 - import { asyncForEach/* , doNothingOnError, returnNullOnError */ } from './helperFunctions'; 5 + import { 6 + asyncForEach /* , doNothingOnError, returnNullOnError */, 7 + } from './helperFunctions'; 6 8 7 9 // attempt to share data over saved files 8 10 // tbh we should just use REDIS instead of this... 9 11 10 12 export async function saveJoinsoundsPlayedOfShard(shardId: number) { 11 - return shardId; 12 - /* try { 13 - const filePath = path.resolve(__dirname, `./shard-${shardId}-jsplayed`); 14 - const data = await fsp.readFile(filePath, 'utf8').catch(returnNullOnError); 15 - let joinsoundsPlayed = data ? Number(data) : 0; 16 - await fsp.writeFile(filePath, String(++joinsoundsPlayed)); 17 - } catch (e) { 18 - doNothingOnError(); 19 - } */ 13 + return shardId; 14 + /* try { 15 + const filePath = path.resolve(__dirname, `./shard-${shardId}-jsplayed`); 16 + const data = await fsp.readFile(filePath, 'utf8').catch(returnNullOnError); 17 + let joinsoundsPlayed = data ? Number(data) : 0; 18 + await fsp.writeFile(filePath, String(++joinsoundsPlayed)); 19 + } catch (e) { 20 + doNothingOnError(); 21 + } */ 20 22 } 21 23 export async function saveUsersWhoJoinedQueue(shardId: number) { 22 - return shardId; 23 - /* try { 24 - const filePath = path.resolve(__dirname, `./shard-${shardId}-uqueue`); 25 - const data = await fsp.readFile(filePath, 'utf8').catch(returnNullOnError); 26 - let usersWhoJoinedQueue = data ? Number(data) : 0; 27 - await fsp.writeFile(filePath, String(++usersWhoJoinedQueue)); 28 - } catch (e) { 29 - doNothingOnError(); 30 - } */ 24 + return shardId; 25 + /* try { 26 + const filePath = path.resolve(__dirname, `./shard-${shardId}-uqueue`); 27 + const data = await fsp.readFile(filePath, 'utf8').catch(returnNullOnError); 28 + let usersWhoJoinedQueue = data ? Number(data) : 0; 29 + await fsp.writeFile(filePath, String(++usersWhoJoinedQueue)); 30 + } catch (e) { 31 + doNothingOnError(); 32 + } */ 31 33 } 32 34 async function loadJoinsoundsPlayedOfShard( 33 - shardId: number, 34 - dataMap: Map<number, number>, 35 + shardId: number, 36 + dataMap: Map<number, number>, 35 37 ) { 36 - try { 37 - const filePath = path.resolve(__dirname, `./shard-${shardId}-jsplayed`); 38 - const data = await fsp.readFile(filePath, 'utf8'); 39 - await fsp.writeFile(filePath, String(0)); 40 - dataMap.set(shardId, Number(data)); 41 - } catch (e) { 42 - console.error('Error loading shared shard data:', e); 43 - } 38 + try { 39 + const filePath = path.resolve(__dirname, `./shard-${shardId}-jsplayed`); 40 + const data = await fsp.readFile(filePath, 'utf8'); 41 + await fsp.writeFile(filePath, String(0)); 42 + dataMap.set(shardId, Number(data)); 43 + } catch (e) { 44 + console.error('Error loading shared shard data:', e); 45 + } 44 46 } 45 47 async function loadUsersWhoJoinedQueue( 46 - shardId: number, 47 - dataMap: Map<number, number>, 48 + shardId: number, 49 + dataMap: Map<number, number>, 48 50 ) { 49 - try { 50 - const filePath = path.resolve(__dirname, `./shard-${shardId}-uqueue`); 51 - const data = await fsp.readFile(filePath, 'utf8'); 52 - await fsp.writeFile(filePath, String(0)); 53 - dataMap.set(shardId, Number(data)); 54 - } catch (e) { 55 - console.error('Error loading shared shard data:', e); 56 - } 51 + try { 52 + const filePath = path.resolve(__dirname, `./shard-${shardId}-uqueue`); 53 + const data = await fsp.readFile(filePath, 'utf8'); 54 + await fsp.writeFile(filePath, String(0)); 55 + dataMap.set(shardId, Number(data)); 56 + } catch (e) { 57 + console.error('Error loading shared shard data:', e); 58 + } 57 59 } 58 60 59 61 export async function accumulateJoinsoundsPlayed(manager: ShardingManager) { 60 - const dataMap = new Map<number, number>(); 61 - await asyncForEach(manager.shards, async (shard) => { 62 - await loadJoinsoundsPlayedOfShard(shard.id, dataMap); 63 - }); 64 - let amount = 0; 65 - dataMap.forEach((val) => { 66 - amount += val; 67 - }); 68 - // console.log('joinsounds played:', amount); 69 - return String(amount); 62 + const dataMap = new Map<number, number>(); 63 + await asyncForEach(manager.shards, async (shard) => { 64 + await loadJoinsoundsPlayedOfShard(shard.id, dataMap); 65 + }); 66 + let amount = 0; 67 + dataMap.forEach((val) => { 68 + amount += val; 69 + }); 70 + // console.log('joinsounds played:', amount); 71 + return String(amount); 70 72 } 71 73 export async function accumulateUsersJoinedQueue(manager: ShardingManager) { 72 - const dataMap = new Map<number, number>(); 73 - await asyncForEach(manager.shards, async (shard) => { 74 - await loadUsersWhoJoinedQueue(shard.id, dataMap); 75 - }); 76 - let amount = 0; 77 - dataMap.forEach((val) => { 78 - amount += val; 79 - }); 80 - // console.log('users joined:', amount); 81 - return String(amount); 74 + const dataMap = new Map<number, number>(); 75 + await asyncForEach(manager.shards, async (shard) => { 76 + await loadUsersWhoJoinedQueue(shard.id, dataMap); 77 + }); 78 + let amount = 0; 79 + dataMap.forEach((val) => { 80 + amount += val; 81 + }); 82 + // console.log('users joined:', amount); 83 + return String(amount); 82 84 }
+110 -112
src/voiceChannelManager.ts
··· 1 1 import { 2 - AudioPlayer, 3 - AudioPlayerStatus, 4 - createAudioPlayer, 5 - createAudioResource, 6 - DiscordGatewayAdapterCreator, 7 - joinVoiceChannel, 8 - VoiceConnection, 2 + AudioPlayer, 3 + AudioPlayerStatus, 4 + createAudioPlayer, 5 + createAudioResource, 6 + DiscordGatewayAdapterCreator, 7 + joinVoiceChannel, 8 + VoiceConnection, 9 9 } from '@discordjs/voice'; 10 10 import { VoiceState } from 'discord.js'; 11 11 import { getJoinsoundOfUser } from './commands/joinsounds/management'; ··· 13 13 import { isBlacklistedUser, isJoinableVc, toggleStillMuted } from './dbHelpers'; 14 14 import { catchErrorOnDiscord } from './sendToMyDiscord'; 15 15 import { 16 - isShadowBanned, 17 - queueVoiceChannels, 18 - shadowBannedLevel, 19 - shadowBannedSound, 16 + isShadowBanned, 17 + queueVoiceChannels, 18 + shadowBannedLevel, 19 + shadowBannedSound, 20 20 } from './shared_assets'; 21 21 import { saveJoinsoundsPlayedOfShard } from './statTracking'; 22 22 23 23 async function isStillMuted(userID: string, guildID: string) { 24 - const find = await StillMutedModel.findOne({ 25 - userid: userID, 26 - guildid: guildID, 27 - }); 28 - return Boolean(find); 24 + const find = await StillMutedModel.findOne({ 25 + userid: userID, 26 + guildid: guildID, 27 + }); 28 + return Boolean(find); 29 29 } 30 30 31 31 function clearConnectionAndPlayer( 32 - connection: VoiceConnection, 33 - player: AudioPlayer, 34 - // eslint-disable-next-line no-undef 35 - timeoutId?: NodeJS.Timeout, 32 + connection: VoiceConnection, 33 + player: AudioPlayer, 34 + // eslint-disable-next-line no-undef 35 + timeoutId?: NodeJS.Timeout, 36 36 ) { 37 - if (timeoutId) { 38 - clearTimeout(timeoutId); 39 - } 40 - player.removeAllListeners(); // To be sure noone listens to this anymore 41 - player.stop(); 42 - connection.destroy(); 37 + if (timeoutId) { 38 + clearTimeout(timeoutId); 39 + } 40 + player.removeAllListeners(); // To be sure noone listens to this anymore 41 + player.stop(); 42 + connection.destroy(); 43 43 } 44 44 45 45 export async function onVoiceStateChange( 46 - oldState: VoiceState, 47 - newState: VoiceState, 46 + oldState: VoiceState, 47 + newState: VoiceState, 48 48 ) { 49 - let connection: VoiceConnection; 50 - let player: AudioPlayer; 51 - const newVc = newState.channel; 52 - // check if voice channel actually changed, don't mute bots 53 - if ( 54 - newState.member 49 + let connection: VoiceConnection; 50 + let player: AudioPlayer; 51 + const newVc = newState.channel; 52 + // check if voice channel actually changed, don't mute bots 53 + if ( 54 + newState.member 55 55 && !newState.member.user.bot 56 56 && (!oldState.channel || !newVc || oldState.channel.id !== newVc.id) 57 - ) { 58 - // is muted and joined a vc? maybe still muted from queue 59 - if ( 60 - newState.serverMute 57 + ) { 58 + // is muted and joined a vc? maybe still muted from queue 59 + if ( 60 + newState.serverMute 61 61 && (await isStillMuted(newState.id, newState.guild.id)) 62 - ) { 63 - newState.setMute( 64 - false, 65 - 'was still muted from a queue which user disconnected from', 66 - ); 67 - toggleStillMuted(newState.id, newState.guild.id, false); 68 - } else if ( 69 - !newState.serverMute 62 + ) { 63 + newState.setMute( 64 + false, 65 + 'was still muted from a queue which user disconnected from', 66 + ); 67 + toggleStillMuted(newState.id, newState.guild.id, false); 68 + } else if ( 69 + !newState.serverMute 70 70 && newVc 71 71 && queueVoiceChannels.has(newState.guild.id) 72 72 && queueVoiceChannels.get(newState.guild.id) === newVc.id 73 - ) { 74 - // user joined a muted channel 75 - newState.setMute(true, 'joined active queue voice channel'); 76 - } else if ( 77 - oldState.serverMute 73 + ) { 74 + // user joined a muted channel 75 + newState.setMute(true, 'joined active queue voice channel'); 76 + } else if ( 77 + oldState.serverMute 78 78 && oldState.channel 79 79 && queueVoiceChannels.has(oldState.guild.id) 80 80 && queueVoiceChannels.get(oldState.guild.id) === oldState.channel.id 81 - ) { 82 - // user left a muted channel 83 - if (newVc) { 84 - newState.setMute(false, 'left active queue voice channel'); 85 - } else { 86 - // save the unmute for later 87 - toggleStillMuted(newState.id, newState.guild.id, true); 88 - } 89 - } 90 - // TODO remove/rework mute logic before this comment 91 - const shadowBanned = isShadowBanned( 92 - newState.member.id, 93 - newState.guild.id, 94 - newState.guild.ownerId, 95 - ); 96 - if ( 97 - newVc 81 + ) { 82 + // user left a muted channel 83 + if (newVc) { 84 + newState.setMute(false, 'left active queue voice channel'); 85 + } else { 86 + // save the unmute for later 87 + toggleStillMuted(newState.id, newState.guild.id, true); 88 + } 89 + } 90 + // TODO remove/rework mute logic before this comment 91 + const shadowBanned = isShadowBanned( 92 + newState.member.id, 93 + newState.guild.id, 94 + newState.guild.ownerId, 95 + ); 96 + if ( 97 + newVc 98 98 && newState.guild.me 99 99 && !newState.guild.me.voice.channel 100 100 && newState.id !== newState.guild.me.user.id ··· 102 102 && !(await isBlacklistedUser(newState.id, newState.guild.id)) 103 103 && ((await isJoinableVc(newState.guild.id, newVc.id)) // checks magibot settings 104 104 || shadowBanned === shadowBannedLevel.guild) 105 - ) { 106 - const perms = newVc.permissionsFor(newState.guild.me); 107 - if (perms && perms.has('CONNECT')) { 108 - let sound = await getJoinsoundOfUser(newState.id, newState.guild.id); 109 - if (shadowBanned !== shadowBannedLevel.not) { 110 - sound = shadowBannedSound; 111 - } 112 - if (sound) { 113 - connection = joinVoiceChannel({ 114 - channelId: newVc.id, 115 - guildId: newVc.guild.id, 116 - adapterCreator: newVc.guild 117 - .voiceAdapterCreator as DiscordGatewayAdapterCreator, 118 - }); 119 - player = createAudioPlayer(); 120 - try { 121 - connection.subscribe(player); 122 - const resource = createAudioResource(sound, { 123 - inlineVolume: true, 124 - }); 125 - player.play(resource); 105 + ) { 106 + const perms = newVc.permissionsFor(newState.guild.me); 107 + if (perms && perms.has('CONNECT')) { 108 + let sound = await getJoinsoundOfUser(newState.id, newState.guild.id); 109 + if (shadowBanned !== shadowBannedLevel.not) { 110 + sound = shadowBannedSound; 111 + } 112 + if (sound) { 113 + connection = joinVoiceChannel({ 114 + channelId: newVc.id, 115 + guildId: newVc.guild.id, 116 + adapterCreator: newVc.guild 117 + .voiceAdapterCreator as DiscordGatewayAdapterCreator, 118 + }); 119 + player = createAudioPlayer(); 120 + try { 121 + connection.subscribe(player); 122 + const resource = createAudioResource(sound, { 123 + inlineVolume: true, 124 + }); 125 + player.play(resource); 126 126 resource.volume!.setVolume(0.5); 127 127 saveJoinsoundsPlayedOfShard(-1); // no shard data here atm 128 128 129 129 // 8 seconds is max play time: 130 130 // so when something goes wrong this will time out latest 4 seconds after; 131 131 // this also gives the bot 4 seconds to connect and start playing when it actually works 132 - /* eslint-disable no-mixed-spaces-and-tabs */ 133 132 const timeoutID = setTimeout(() => { 134 - clearConnectionAndPlayer(connection, player); 133 + clearConnectionAndPlayer(connection, player); 135 134 }, 12 * 1000); 136 135 player.on('stateChange', (state) => { 137 - if (state.status === AudioPlayerStatus.Playing) { 138 - if (state.playbackDuration > 0) { 139 - // this occurs *after* the sound has finished 140 - clearConnectionAndPlayer(connection, player, timeoutID); 141 - } 142 - } 136 + if (state.status === AudioPlayerStatus.Playing) { 137 + if (state.playbackDuration > 0) { 138 + // this occurs *after* the sound has finished 139 + clearConnectionAndPlayer(connection, player, timeoutID); 140 + } 141 + } 143 142 }); 144 143 player.on('error', (err) => { 145 - clearConnectionAndPlayer(connection, player, timeoutID); 146 - catchErrorOnDiscord( 147 - `**Dispatcher Error (${ 148 - (err.toString && err.toString()) || 'NONE' 149 - }):**\n\`\`\` 144 + clearConnectionAndPlayer(connection, player, timeoutID); 145 + catchErrorOnDiscord( 146 + `**Dispatcher Error (${ 147 + (err.toString && err.toString()) || 'NONE' 148 + }):**\n\`\`\` 150 149 ${err.stack || 'NO STACK'} 151 150 \`\`\``, 152 - ); 153 - /* eslint-enable no-mixed-spaces-and-tabs */ 151 + ); 154 152 }); 155 - } catch (err) { 156 - console.error(err); 157 - clearConnectionAndPlayer(connection, player); 158 - } 159 - } 160 - } 161 - } 162 - } 153 + } catch (err) { 154 + console.error(err); 155 + clearConnectionAndPlayer(connection, player); 156 + } 157 + } 158 + } 159 + } 160 + } 163 161 }
+38 -38
src/webhooks.ts
··· 2 2 import { doNothingOnError } from './helperFunctions'; 3 3 4 4 const { 5 - WEBHOOK_ID_EX, 6 - WEBHOOK_TOKEN_EX, 7 - WEBHOOK_ID_JOIN, 8 - WEBHOOK_TOKEN_JOIN, 9 - WEBHOOK_ID_BUG, 10 - WEBHOOK_TOKEN_BUG, 11 - WEBHOOK_ID_STARTUP, 12 - WEBHOOK_TOKEN_STARTUP, 5 + WEBHOOK_ID_EX, 6 + WEBHOOK_TOKEN_EX, 7 + WEBHOOK_ID_JOIN, 8 + WEBHOOK_TOKEN_JOIN, 9 + WEBHOOK_ID_BUG, 10 + WEBHOOK_TOKEN_BUG, 11 + WEBHOOK_ID_STARTUP, 12 + WEBHOOK_TOKEN_STARTUP, 13 13 } = process.env; 14 14 if ( 15 - !( 16 - WEBHOOK_ID_EX 15 + !( 16 + WEBHOOK_ID_EX 17 17 && WEBHOOK_TOKEN_EX 18 18 && WEBHOOK_ID_JOIN 19 19 && WEBHOOK_TOKEN_JOIN ··· 21 21 && WEBHOOK_TOKEN_BUG 22 22 && WEBHOOK_ID_STARTUP 23 23 && WEBHOOK_TOKEN_STARTUP 24 - ) 24 + ) 25 25 ) { 26 - throw new Error('Discord Webhook information is missing.'); 26 + throw new Error('Discord Webhook information is missing.'); 27 27 } 28 28 29 29 const exceptionsWebhook = new Discord.WebhookClient({ 30 - id: WEBHOOK_ID_EX, 31 - token: WEBHOOK_TOKEN_EX, 30 + id: WEBHOOK_ID_EX, 31 + token: WEBHOOK_TOKEN_EX, 32 32 }); 33 33 const joinsWebhook = new Discord.WebhookClient({ 34 - id: WEBHOOK_ID_JOIN, 35 - token: WEBHOOK_TOKEN_JOIN, 34 + id: WEBHOOK_ID_JOIN, 35 + token: WEBHOOK_TOKEN_JOIN, 36 36 }); 37 37 const bugreportWebhook = new Discord.WebhookClient({ 38 - id: WEBHOOK_ID_BUG, 39 - token: WEBHOOK_TOKEN_BUG, 38 + id: WEBHOOK_ID_BUG, 39 + token: WEBHOOK_TOKEN_BUG, 40 40 }); 41 41 const startupWebhook = new Discord.WebhookClient({ 42 - id: WEBHOOK_ID_STARTUP, 43 - token: WEBHOOK_TOKEN_STARTUP, 42 + id: WEBHOOK_ID_STARTUP, 43 + token: WEBHOOK_TOKEN_STARTUP, 44 44 }); 45 45 46 46 export async function sendException(value: string, shardId?: number) { 47 - return exceptionsWebhook 48 - .send(`Shard ${shardId}: ${value}`) 49 - .catch(doNothingOnError); 47 + return exceptionsWebhook 48 + .send(`Shard ${shardId}: ${value}`) 49 + .catch(doNothingOnError); 50 50 } 51 51 export async function sendJoinEvent(value: string, shardId?: number) { 52 - return joinsWebhook 53 - .send(`Shard ${shardId}: ${value}`) 54 - .catch(doNothingOnError); 52 + return joinsWebhook 53 + .send(`Shard ${shardId}: ${value}`) 54 + .catch(doNothingOnError); 55 55 } 56 56 export async function sendBugreport(value: string, shardId?: number) { 57 - return bugreportWebhook 58 - .send(`Shard ${shardId}: ${value}`) 59 - .catch(doNothingOnError); 57 + return bugreportWebhook 58 + .send(`Shard ${shardId}: ${value}`) 59 + .catch(doNothingOnError); 60 60 } 61 61 export async function sendStartupEvent( 62 - shardId: number, 63 - justStartingUp = false, 62 + shardId: number, 63 + justStartingUp = false, 64 64 ) { 65 - return startupWebhook 66 - .send( 67 - justStartingUp 68 - ? `Shard ${shardId} is starting...!` 69 - : `Shard ${shardId} is up!`, 70 - ) 71 - .catch(doNothingOnError); 65 + return startupWebhook 66 + .send( 67 + justStartingUp 68 + ? `Shard ${shardId} is starting...!` 69 + : `Shard ${shardId} is up!`, 70 + ) 71 + .catch(doNothingOnError); 72 72 }