diff --git a/src/commands/ban.ts b/src/commands/ban.ts index f9d97f0..cdc1502 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -13,9 +13,14 @@ export function command (message: discord.Message) { state.logChannel.send(`${message.author.toString()} has banned ${user} ${user.toString()} [${count}].`); state.bans.push(new UserBan(user.id, user.username, message.author.id, message.author.username, count)); - - message.guild.member(user).ban().catch(function (error) { - state.logChannel.send(`Error banning ${user} ${user.username}`); + let member = message.guild?.member(user); + if (!member) { + state.logChannel.send(`Error banning ${user} ${user.username}: user not found.`); + logger.error(`User not found: ${user.toString()} ${user} ${user.username} when executing a ban`); + // we don't need a return here, because of the optional chaining below + } + member?.ban().catch(function (error) { + state.logChannel.send(`Error banning ${user.toString()} ${user.username}`); logger.error(`Error banning ${user.toString()} ${user} ${user.username}.`, error); }); diff --git a/src/commands/game.ts b/src/commands/game.ts index 33e575d..e617432 100644 --- a/src/commands/game.ts +++ b/src/commands/game.ts @@ -23,6 +23,11 @@ const compatStrings: ICompatList = { async function updateDatabase () { let body; + if (!targetServer) { + logger.error('Unable to download latest games list!'); + return; + } + try { let response = await fetch(targetServer); body = await response.json(); @@ -50,7 +55,7 @@ export async function command (message: discord.Message) { // Update remote list of games locally. const waitMessage = message.channel.send('This will take a second...'); - if (state.gameDBPromise == null) { + if (!state.gameDBPromise) { state.gameDBPromise = updateDatabase(); } @@ -68,16 +73,18 @@ export async function command (message: discord.Message) { const game = message.content.substr(message.content.indexOf(' ') + 1); // Search all games. This is only linear time, so /shrug? - let bestGame: IGameDBEntry; + let bestGame: IGameDBEntry | null = null; let bestScore = 0.5; // Game names must have at least a 50% similarity to be matched - state.gameDB.forEach(testGame => { + // for is faster than forEach + for (let index = 0; index < state.gameDB.length; index++) { + const testGame = state.gameDB[index]; const newDistance = stringSimilarity.compareTwoStrings(game.toLowerCase(), testGame.title.toLowerCase()); if (newDistance > bestScore) { bestGame = testGame; bestScore = newDistance; } - }); + } if (!bestGame) { message.channel.send('Game could not be found.'); diff --git a/src/commands/grantDeveloper.ts b/src/commands/grantDeveloper.ts index ab5a208..d5866a3 100644 --- a/src/commands/grantDeveloper.ts +++ b/src/commands/grantDeveloper.ts @@ -5,19 +5,30 @@ import discord = require('discord.js'); export const roles = ['Admins', 'Moderators', 'CitraBot']; export function command (message: discord.Message) { const role = process.env.DISCORD_DEVELOPER_ROLE; + + if (!role) { + logger.error('DISCORD_DEVELOPER_ROLE suddenly became undefined?!'); + return; + } + message.mentions.users.map((user) => { - const member = message.guild.member(user); - const alreadyJoined = member.roles.cache.has(role); + const member = message.guild?.member(user); + const alreadyJoined = member?.roles.cache.has(role); + + if (!member) { + message.channel.send(`User ${user.toString()} was not found in the channel.`); + return; + } if (alreadyJoined) { - member.roles.remove(role).then(() => { + member?.roles.remove(role).then(() => { message.channel.send(`${user.toString()}'s speech has been revoked in the #development channel.`); }).catch(() => { state.logChannel.send(`Error revoking ${user.toString()}'s developer speech...`); logger.error(`Error revoking ${user} ${user.username}'s developer speech...`); }); } else { - member.roles.add(role).then(() => { + member?.roles.add(role).then(() => { message.channel.send(`${user.toString()} has been granted speech in the #development channel.`); }).catch(() => { state.logChannel.send(`Error granting ${user.toString()}'s developer speech...`); diff --git a/src/models/interfaces.ts b/src/models/interfaces.ts index 9019d50..79a88fb 100644 --- a/src/models/interfaces.ts +++ b/src/models/interfaces.ts @@ -1,3 +1,5 @@ +import { Message } from 'discord.js'; + export interface IGameDBEntry { directory: string; title: string; @@ -14,8 +16,13 @@ export interface ICompatList { } export interface IResponses { - pmReply: string, - quotes: { - [key: string]: { reply: string } + readonly pmReply: string, + readonly quotes: { + readonly [key: string]: { readonly reply: string } } } + +export interface IModule { + readonly roles?: string[], + command: (message: Message, args: string) => void | Promise +} diff --git a/src/server.ts b/src/server.ts index 03df4e2..e68df54 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,10 +18,17 @@ interface IModuleMap { let cachedModules: IModuleMap = {}; const client = new discord.Client(); - +const rulesTrigger = process.env.DISCORD_RULES_TRIGGER; +const rluesRole = process.env.DISCORD_RULES_ROLE; const mediaUsers = new Map(); logger.info('Application startup. Configuring environment.'); +if (!rulesTrigger) { + throw new Error('DISCORD_RULES_TRIGGER somehow became undefined.'); +} +if (!rluesRole) { + throw new Error('DISCORD_RULES_ROLE somehow became undefined.'); +} function findArray(haystack: string | any[], arr: any[]) { return arr.some(function (v: any) { @@ -36,6 +43,9 @@ function IsIgnoredCategory(categoryName: string) { client.on('ready', async () => { // Initialize app channels. + if (!process.env.DISCORD_LOG_CHANNEL || !process.env.DISCORD_MSGLOG_CHANNEL) { + throw new Error('DISCORD_LOG_CHANNEL or DISCORD_MSGLOG_CHANNEL not defined.'); + } let logChannel = await client.channels.fetch(process.env.DISCORD_LOG_CHANNEL) as discord.TextChannel; let msglogChannel = await client.channels.fetch(process.env.DISCORD_MSGLOG_CHANNEL) as discord.TextChannel; if (!logChannel.send) throw new Error('DISCORD_LOG_CHANNEL is not a text channel!'); @@ -62,15 +72,16 @@ client.on('disconnect', () => { }); client.on('guildMemberAdd', (member) => { - member.roles.add(process.env.DISCORD_RULES_ROLE); + if (process.env.DISCORD_RULES_ROLE) + member.roles.add(process.env.DISCORD_RULES_ROLE); }); client.on('messageDelete', message => { let parent = (message.channel as discord.TextChannel).parent; if (parent && IsIgnoredCategory(parent.name) === false) { - if (message.content && message.content.startsWith('.') === false && message.author.bot === false) { + if (message.content && message.content.startsWith('.') === false && message.author?.bot === false) { const deletionEmbed = new discord.MessageEmbed() - .setAuthor(message.author.tag, message.author.displayAvatarURL()) + .setAuthor(message.author?.tag, message.author?.displayAvatarURL()) .setDescription(`Message deleted in ${message.channel.toString()}`) .addField('Content', message.cleanContent, false) .setTimestamp() @@ -84,14 +95,19 @@ client.on('messageDelete', message => { client.on('messageUpdate', (oldMessage, newMessage) => { const AllowedRoles = ['Administrators', 'Moderators', 'Team', 'VIP']; - if (!findArray(oldMessage.member.roles.cache.map(x => x.name), AllowedRoles)) { + let authorRoles = oldMessage.member?.roles?.cache?.map(x => x.name); + if (!authorRoles) { + logger.error(`Unable to get the roles for ${oldMessage.author}`); + return; + } + if (!findArray(authorRoles, AllowedRoles)) { let parent = (oldMessage.channel as discord.TextChannel).parent; if (parent && IsIgnoredCategory(parent.name) === false) { const oldM = oldMessage.cleanContent; const newM = newMessage.cleanContent; if (oldMessage.content !== newMessage.content && oldM && newM) { const editedEmbed = new discord.MessageEmbed() - .setAuthor(oldMessage.author.tag, oldMessage.author.displayAvatarURL()) + .setAuthor(oldMessage.author?.tag, oldMessage.author?.displayAvatarURL()) .setDescription(`Message edited in ${oldMessage.channel.toString()} [Jump To Message](${newMessage.url})`) .addField('Before', oldM, false) .addField('After', newM, false) @@ -99,7 +115,7 @@ client.on('messageUpdate', (oldMessage, newMessage) => { .setColor('GREEN'); state.msglogChannel.send(editedEmbed); - logger.info(`${oldMessage.author.username} ${oldMessage.author} edited message from: ${oldM} to: ${newM}.`); + logger.info(`${oldMessage.author?.username} ${oldMessage.author} edited message from: ${oldM} to: ${newM}.`); } } } @@ -118,9 +134,15 @@ client.on('message', message => { logger.verbose(`${message.author.username} ${message.author} [Channel: ${(message.channel as discord.TextChannel).name} ${message.channel}]: ${message.content}`); + let authorRoles = message.member?.roles?.cache?.map(x => x.name); + if (message.channel.id === process.env.DISCORD_MEDIA_CHANNEL && !message.author.bot) { const AllowedMediaRoles = ['Administrators', 'Moderators', 'Team', 'VIP']; - if (!findArray(message.member.roles.cache.map(x => x.name), AllowedMediaRoles)) { + if (!authorRoles) { + logger.error(`Unable to get the roles for ${message.author}`); + return; + } + if (!findArray(authorRoles, AllowedMediaRoles)) { const urlRegex = new RegExp(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/gi); if (message.attachments.size > 0 || message.content.match(urlRegex)) { mediaUsers.set(message.author.id, true); @@ -135,10 +157,10 @@ client.on('message', message => { // Check if the channel is #rules, if so we want to follow a different logic flow. if (message.channel.id === process.env.DISCORD_RULES_CHANNEL) { - if (message.content.toLowerCase().includes(process.env.DISCORD_RULES_TRIGGER)) { + if (message.content.toLowerCase().includes(rulesTrigger)) { // We want to remove the 'Unauthorized' role from them once they agree to the rules. logger.verbose(`${message.author.username} ${message.author} has accepted the rules, removing role ${process.env.DISCORD_RULES_ROLE}.`); - message.member.roles.remove(process.env.DISCORD_RULES_ROLE, 'Accepted the rules.'); + message.member?.roles.remove(rluesRole, 'Accepted the rules.'); } // Delete the message in the channel to force a cleanup. @@ -155,7 +177,11 @@ client.on('message', message => { if (!cachedModule && !quoteResponse) return; // Not a valid command. // Check access permissions. - if (cachedModule && cachedModule.roles && !findArray(message.member.roles.cache.map(x => x.name), cachedModule.roles)) { + if (!authorRoles) { + logger.error(`Unable to get the roles for ${message.author}`); + return; + } + if (cachedModule && cachedModule.roles && !findArray(authorRoles, cachedModule.roles)) { state.logChannel.send(`${message.author.toString()} attempted to use admin command: ${message.content}`); logger.info(`${message.author.username} ${message.author} attempted to use admin command: ${message.content}`); return; @@ -168,7 +194,7 @@ client.on('message', message => { if (!!cachedModule) { cachedModule.command(message); } else if (cachedModules['quote']) { - cachedModules['quote'].command(message, quoteResponse.reply); + cachedModules['quote'].command(message, quoteResponse?.reply); } } catch (err) { logger.error(err); } diff --git a/src/state.ts b/src/state.ts index 5694b60..6570bc3 100644 --- a/src/state.ts +++ b/src/state.ts @@ -13,13 +13,13 @@ class State { stats: { joins: number; ruleAccepts: number; leaves: number; warnings: number; }; lastGameDBUpdate: number; gameDB: IGameDBEntry[]; - gameDBPromise: Promise; + gameDBPromise: Promise | null; constructor () { - this.logChannel = null; - this.msglogChannel = null; + this.logChannel; + this.msglogChannel; this.warnings = []; - this.responses = null; + this.responses; this.bans = []; this.stats = { joins: 0, diff --git a/tsconfig.json b/tsconfig.json index c55dc71..e5065a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "module": "CommonJS", "noImplicitAny": true, + "strictNullChecks": true, "removeComments": true, "preserveConstEnums": true, "outDir": "dist/",