diff --git a/backend/src/Queue.ts b/backend/src/Queue.ts index 38ab2560..0f0bb733 100644 --- a/backend/src/Queue.ts +++ b/backend/src/Queue.ts @@ -34,7 +34,7 @@ export class Queue { return; } - const fn = this.queue.shift(); + const fn = this.queue.shift()!; new Promise(resolve => { // Either fn() completes or the timeout is reached fn().then(resolve); diff --git a/backend/src/QueuedEventEmitter.ts b/backend/src/QueuedEventEmitter.ts index 5ac30890..3da957d3 100644 --- a/backend/src/QueuedEventEmitter.ts +++ b/backend/src/QueuedEventEmitter.ts @@ -16,7 +16,7 @@ export class QueuedEventEmitter { this.listeners.set(eventName, []); } - this.listeners.get(eventName).push(listener); + this.listeners.get(eventName)!.push(listener); return listener; } @@ -25,7 +25,7 @@ export class QueuedEventEmitter { return; } - const listeners = this.listeners.get(eventName); + const listeners = this.listeners.get(eventName)!; listeners.splice(listeners.indexOf(listener), 1); } diff --git a/backend/src/RegExpRunner.ts b/backend/src/RegExpRunner.ts index ef5f72dd..80d6d9b6 100644 --- a/backend/src/RegExpRunner.ts +++ b/backend/src/RegExpRunner.ts @@ -47,8 +47,8 @@ export interface RegExpRunner { * Repeatedly failing regexes are put on a cooldown where requests to execute them are ignored. */ export class RegExpRunner extends EventEmitter { - private _worker: RegExpWorker; - private _failedTimesInterval: Timeout; + private _worker: RegExpWorker | null; + private readonly _failedTimesInterval: Timeout; private cooldown: CooldownManager; private failedTimes: Map; @@ -90,13 +90,13 @@ export class RegExpRunner extends EventEmitter { if (isTimeoutError(e)) { if (this.failedTimes.has(regex.source)) { // Regex has failed before, increment fail counter - this.failedTimes.set(regex.source, this.failedTimes.get(regex.source) + 1); + this.failedTimes.set(regex.source, this.failedTimes.get(regex.source)! + 1); } else { // This is the first time this regex failed, init fail counter this.failedTimes.set(regex.source, 1); } - if (this.failedTimes.get(regex.source) >= REGEX_FAIL_TO_COOLDOWN_COUNT) { + if (this.failedTimes.has(regex.source) && this.failedTimes.get(regex.source)! >= REGEX_FAIL_TO_COOLDOWN_COUNT) { // Regex has failed too many times, set it on cooldown this.cooldown.setCooldown(regex.source, REGEX_FAIL_COOLDOWN); this.failedTimes.delete(regex.source); diff --git a/backend/src/SimpleCache.ts b/backend/src/SimpleCache.ts index bf53f936..57a58bf7 100644 --- a/backend/src/SimpleCache.ts +++ b/backend/src/SimpleCache.ts @@ -13,7 +13,10 @@ export class SimpleCache { constructor(retentionTime: number, maxItems?: number) { this.retentionTime = retentionTime; - this.maxItems = maxItems; + + if (maxItems) { + this.maxItems = maxItems; + } this.store = new Map(); } @@ -48,7 +51,7 @@ export class SimpleCache { } } - get(key: string): T { + get(key: string): T | null { const info = this.store.get(key); if (!info) return null; diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index e5566a3f..d66ae078 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -147,7 +147,7 @@ export function initAuth(app: express.Express) { res.json({ valid: true }); }); app.post("/auth/logout", ...apiTokenAuthHandlers(), async (req: Request, res: Response) => { - await apiLogins.expireApiKey(req.user.apiKey); + await apiLogins.expireApiKey(req.user!.apiKey); return ok(res); }); } diff --git a/backend/src/api/guilds.ts b/backend/src/api/guilds.ts index 91096386..213dcb44 100644 --- a/backend/src/api/guilds.ts +++ b/backend/src/api/guilds.ts @@ -19,12 +19,12 @@ export function initGuildsAPI(app: express.Express) { guildRouter.use(...apiTokenAuthHandlers()); guildRouter.get("/available", async (req: Request, res: Response) => { - const guilds = await allowedGuilds.getForApiUser(req.user.userId); + const guilds = await allowedGuilds.getForApiUser(req.user!.userId); res.json(guilds); }); guildRouter.get("/:guildId", async (req: Request, res: Response) => { - if (!(await hasGuildPermission(req.user.userId, req.params.guildId, ApiPermissions.ViewGuild))) { + if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, ApiPermissions.ViewGuild))) { return unauthorized(res); } @@ -34,7 +34,7 @@ export function initGuildsAPI(app: express.Express) { guildRouter.post("/:guildId/check-permission", async (req: Request, res: Response) => { const permission = req.body.permission; - const hasPermission = await hasGuildPermission(req.user.userId, req.params.guildId, permission); + const hasPermission = await hasGuildPermission(req.user!.userId, req.params.guildId, permission); res.json({ result: hasPermission }); }); @@ -54,7 +54,7 @@ export function initGuildsAPI(app: express.Express) { config = config.trim() + "\n"; // Normalize start/end whitespace in the config const currentConfig = await configs.getActiveByKey(`guild-${req.params.guildId}`); - if (config === currentConfig.config) { + if (currentConfig && config === currentConfig.config) { return ok(res); } @@ -81,7 +81,7 @@ export function initGuildsAPI(app: express.Express) { return res.status(422).json({ errors: [error] }); } - await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user.userId); + await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user!.userId); ok(res); }); diff --git a/backend/src/api/permissions.ts b/backend/src/api/permissions.ts index 68e22162..7433734a 100644 --- a/backend/src/api/permissions.ts +++ b/backend/src/api/permissions.ts @@ -24,7 +24,7 @@ export const hasGuildPermission = async (userId: string, guildId: string, permis */ export function requireGuildPermission(permission: ApiPermissions) { return async (req: Request, res: Response, next) => { - if (!(await hasGuildPermission(req.user.userId, req.params.guildId, permission))) { + if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, permission))) { return unauthorized(res); } diff --git a/backend/src/api/staff.ts b/backend/src/api/staff.ts index eb6b0452..82109735 100644 --- a/backend/src/api/staff.ts +++ b/backend/src/api/staff.ts @@ -7,7 +7,7 @@ export function initStaff(app: express.Express) { staffRouter.use(...apiTokenAuthHandlers()); staffRouter.get("/status", (req: Request, res: Response) => { - const userIsStaff = isStaff(req.user.userId); + const userIsStaff = isStaff(req.user!.userId); res.json({ isStaff: userIsStaff }); }); diff --git a/backend/src/commandTypes.ts b/backend/src/commandTypes.ts index e36ce831..6bf0198b 100644 --- a/backend/src/commandTypes.ts +++ b/backend/src/commandTypes.ts @@ -48,7 +48,9 @@ export const commandTypes = { }, async resolvedMember(value, context: CommandContext) { - if (!(context.message.channel instanceof GuildChannel)) return null; + if (!(context.message.channel instanceof GuildChannel)) { + throw new TypeConversionError(`Cannot resolve member for non-guild channels`); + } const result = await resolveMember(context.pluginData.client, context.message.channel.guild, value); if (result == null) { @@ -110,7 +112,7 @@ export const commandTypeHelpers = { delay: createTypeHelper(commandTypes.delay), resolvedUser: createTypeHelper>(commandTypes.resolvedUser), resolvedUserLoose: createTypeHelper>(commandTypes.resolvedUserLoose), - resolvedMember: createTypeHelper>(commandTypes.resolvedMember), + resolvedMember: createTypeHelper>(commandTypes.resolvedMember), messageTarget: createTypeHelper>(commandTypes.messageTarget), anyId: createTypeHelper>(commandTypes.anyId), regex: createTypeHelper(commandTypes.regex), diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 9323de72..ef0af13d 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -34,10 +34,10 @@ export async function validateGuildConfig(config: any): Promise { return `Invalid options specified for plugin ${pluginName}`; } - const plugin = pluginNameToPlugin.get(pluginName); + const plugin = pluginNameToPlugin.get(pluginName)!; try { const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions); - await plugin.configPreprocessor(mergedOptions as PluginOptions); + await plugin.configPreprocessor?.(mergedOptions as PluginOptions); } catch (err) { if (err instanceof ConfigValidationError || err instanceof StrictValidationError) { return `${pluginName}: ${err.message}`; diff --git a/backend/src/data/GuildArchives.ts b/backend/src/data/GuildArchives.ts index 31ca854f..a67048d5 100644 --- a/backend/src/data/GuildArchives.ts +++ b/backend/src/data/GuildArchives.ts @@ -37,7 +37,7 @@ export class GuildArchives extends BaseGuildRepository { .execute(); } - async find(id: string): Promise { + async find(id: string): Promise { return this.archives.findOne({ where: { id }, relations: this.getRelations(), @@ -56,7 +56,7 @@ export class GuildArchives extends BaseGuildRepository { /** * @returns ID of the created entry */ - async create(body: string, expiresAt: moment.Moment = null): Promise { + async create(body: string, expiresAt?: moment.Moment): Promise { if (!expiresAt) { expiresAt = moment.utc().add(DEFAULT_EXPIRY_DAYS, "days"); } @@ -71,7 +71,7 @@ export class GuildArchives extends BaseGuildRepository { } protected async renderLinesFromSavedMessages(savedMessages: SavedMessage[], guild: Guild) { - const msgLines = []; + const msgLines: string[] = []; for (const msg of savedMessages) { const channel = guild.channels.get(msg.channel_id); const user = { ...msg.data.author, id: msg.user_id }; @@ -88,7 +88,7 @@ export class GuildArchives extends BaseGuildRepository { return msgLines; } - async createFromSavedMessages(savedMessages: SavedMessage[], guild: Guild, expiresAt = null) { + async createFromSavedMessages(savedMessages: SavedMessage[], guild: Guild, expiresAt?: moment.Moment) { if (expiresAt == null) { expiresAt = moment.utc().add(DEFAULT_EXPIRY_DAYS, "days"); } @@ -105,6 +105,10 @@ export class GuildArchives extends BaseGuildRepository { const messagesStr = msgLines.join("\n"); const archive = await this.find(archiveId); + if (archive == null) { + throw new Error("Archive not found"); + } + archive.body += "\n" + messagesStr; await this.archives.update({ id: archiveId }, { body: archive.body }); diff --git a/backend/src/data/GuildAutoReactions.ts b/backend/src/data/GuildAutoReactions.ts index 8c556bda..094a1a4a 100644 --- a/backend/src/data/GuildAutoReactions.ts +++ b/backend/src/data/GuildAutoReactions.ts @@ -18,7 +18,7 @@ export class GuildAutoReactions extends BaseGuildRepository { }); } - async getForChannel(channelId: string): Promise { + async getForChannel(channelId: string): Promise { return this.autoReactions.findOne({ where: { guild_id: this.guildId, diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts index 37396c3f..8404f8f1 100644 --- a/backend/src/data/GuildCases.ts +++ b/backend/src/data/GuildCases.ts @@ -29,7 +29,7 @@ export class GuildCases extends BaseGuildRepository { }); } - async find(id: number): Promise { + async find(id: number): Promise { return this.cases.findOne({ relations: this.getRelations(), where: { @@ -39,7 +39,7 @@ export class GuildCases extends BaseGuildRepository { }); } - async findByCaseNumber(caseNumber: number): Promise { + async findByCaseNumber(caseNumber: number): Promise { return this.cases.findOne({ relations: this.getRelations(), where: { @@ -49,7 +49,7 @@ export class GuildCases extends BaseGuildRepository { }); } - async findLatestByModId(modId: string): Promise { + async findLatestByModId(modId: string): Promise { return this.cases.findOne({ relations: this.getRelations(), where: { @@ -62,7 +62,7 @@ export class GuildCases extends BaseGuildRepository { }); } - async findByAuditLogId(auditLogId: string): Promise { + async findByAuditLogId(auditLogId: string): Promise { return this.cases.findOne({ relations: this.getRelations(), where: { @@ -113,7 +113,7 @@ export class GuildCases extends BaseGuildRepository { case_number: () => `(SELECT IFNULL(MAX(case_number)+1, 1) FROM cases AS ma2 WHERE guild_id = ${this.guildId})`, }); - return this.find(result.identifiers[0].id); + return (await this.find(result.identifiers[0].id))!; } update(id, data) { diff --git a/backend/src/data/GuildEvents.ts b/backend/src/data/GuildEvents.ts index 085711bb..3a2b6489 100644 --- a/backend/src/data/GuildEvents.ts +++ b/backend/src/data/GuildEvents.ts @@ -17,12 +17,12 @@ export class GuildEvents extends BaseGuildRepository { this.pluginListeners.set(pluginName, new Map()); } - const pluginListeners = this.pluginListeners.get(pluginName); + const pluginListeners = this.pluginListeners.get(pluginName)!; if (!pluginListeners.has(eventName)) { pluginListeners.set(eventName, []); } - const pluginEventListeners = pluginListeners.get(eventName); + const pluginEventListeners = pluginListeners.get(eventName)!; pluginEventListeners.push(fn); } diff --git a/backend/src/data/GuildLogs.ts b/backend/src/data/GuildLogs.ts index 064b6698..457beec6 100644 --- a/backend/src/data/GuildLogs.ts +++ b/backend/src/data/GuildLogs.ts @@ -16,7 +16,7 @@ export class GuildLogs extends events.EventEmitter { constructor(guildId) { if (guildInstances.has(guildId)) { // Return existing instance for this guild if one exists - return guildInstances.get(guildId); + return guildInstances.get(guildId)!; } super(); @@ -27,7 +27,7 @@ export class GuildLogs extends events.EventEmitter { guildInstances.set(guildId, this); } - log(type: LogType, data: any, ignoreId = null) { + log(type: LogType, data: any, ignoreId?: string) { if (ignoreId && this.isLogIgnored(type, ignoreId)) { this.clearIgnoredLog(type, ignoreId); return; @@ -36,7 +36,7 @@ export class GuildLogs extends events.EventEmitter { this.emit("log", { type, data }); } - ignoreLog(type: LogType, ignoreId: any, timeout: number = null) { + ignoreLog(type: LogType, ignoreId: any, timeout?: number) { this.ignoredLogs.push({ type, ignoreId }); // Clear after expiry (15sec by default) diff --git a/backend/src/data/GuildMutes.ts b/backend/src/data/GuildMutes.ts index 9f0ca9e3..7a408106 100644 --- a/backend/src/data/GuildMutes.ts +++ b/backend/src/data/GuildMutes.ts @@ -20,7 +20,7 @@ export class GuildMutes extends BaseGuildRepository { .getMany(); } - async findExistingMuteForUserId(userId: string): Promise { + async findExistingMuteForUserId(userId: string): Promise { return this.mutes.findOne({ where: { guild_id: this.guildId, @@ -48,7 +48,7 @@ export class GuildMutes extends BaseGuildRepository { expires_at: expiresAt, }); - return this.mutes.findOne({ where: result.identifiers[0] }); + return (await this.mutes.findOne({ where: result.identifiers[0] }))!; } async updateExpiryTime(userId, newExpiryTime) { diff --git a/backend/src/data/GuildNicknameHistory.ts b/backend/src/data/GuildNicknameHistory.ts index 7a10789f..60f8f419 100644 --- a/backend/src/data/GuildNicknameHistory.ts +++ b/backend/src/data/GuildNicknameHistory.ts @@ -39,7 +39,7 @@ export class GuildNicknameHistory extends BaseGuildRepository { }); } - getLastEntry(userId): Promise { + getLastEntry(userId): Promise { return this.nicknameHistory.findOne({ where: { guild_id: this.guildId, diff --git a/backend/src/data/GuildPingableRoles.ts b/backend/src/data/GuildPingableRoles.ts index 8d1f8a3b..fa3eda28 100644 --- a/backend/src/data/GuildPingableRoles.ts +++ b/backend/src/data/GuildPingableRoles.ts @@ -27,7 +27,7 @@ export class GuildPingableRoles extends BaseGuildRepository { }); } - async getByChannelAndRoleId(channelId: string, roleId: string): Promise { + async getByChannelAndRoleId(channelId: string, roleId: string): Promise { return this.pingableRoles.findOne({ where: { guild_id: this.guildId, diff --git a/backend/src/data/GuildReactionRoles.ts b/backend/src/data/GuildReactionRoles.ts index c197bc1d..9b309f03 100644 --- a/backend/src/data/GuildReactionRoles.ts +++ b/backend/src/data/GuildReactionRoles.ts @@ -27,7 +27,7 @@ export class GuildReactionRoles extends BaseGuildRepository { }); } - async getByMessageAndEmoji(messageId: string, emoji: string): Promise { + async getByMessageAndEmoji(messageId: string, emoji: string): Promise { return this.reactionRoles.findOne({ where: { guild_id: this.guildId, @@ -37,7 +37,7 @@ export class GuildReactionRoles extends BaseGuildRepository { }); } - async removeFromMessage(messageId: string, emoji: string = null) { + async removeFromMessage(messageId: string, emoji?: string) { const criteria: any = { guild_id: this.guildId, message_id: messageId, diff --git a/backend/src/data/GuildSavedMessages.ts b/backend/src/data/GuildSavedMessages.ts index 3abac626..501229c0 100644 --- a/backend/src/data/GuildSavedMessages.ts +++ b/backend/src/data/GuildSavedMessages.ts @@ -96,7 +96,7 @@ export class GuildSavedMessages extends BaseGuildRepository { .getMany(); } - getUserMessagesByChannelAfterId(userId, channelId, afterId, limit = null) { + getUserMessagesByChannelAfterId(userId, channelId, afterId, limit?: number) { let query = this.messages .createQueryBuilder() .where("guild_id = :guild_id", { guild_id: this.guildId }) @@ -241,12 +241,12 @@ export class GuildSavedMessages extends BaseGuildRepository { } } - async onceMessageAvailable(id: string, handler: (msg: SavedMessage) => any, timeout: number = 60 * 1000) { + async onceMessageAvailable(id: string, handler: (msg?: SavedMessage) => any, timeout: number = 60 * 1000) { let called = false; let onceEventListener; let timeoutFn; - const callHandler = async (msg: SavedMessage) => { + const callHandler = async (msg?: SavedMessage) => { this.events.off(`create:${id}`, onceEventListener); clearTimeout(timeoutFn); @@ -259,7 +259,7 @@ export class GuildSavedMessages extends BaseGuildRepository { onceEventListener = this.events.once(`create:${id}`, callHandler); timeoutFn = setTimeout(() => { called = true; - callHandler(null); + callHandler(undefined); }, timeout); const messageInDB = await this.find(id); diff --git a/backend/src/data/GuildSlowmodes.ts b/backend/src/data/GuildSlowmodes.ts index ed042021..9e4281db 100644 --- a/backend/src/data/GuildSlowmodes.ts +++ b/backend/src/data/GuildSlowmodes.ts @@ -14,7 +14,7 @@ export class GuildSlowmodes extends BaseGuildRepository { this.slowmodeUsers = getRepository(SlowmodeUser); } - async getChannelSlowmode(channelId): Promise { + async getChannelSlowmode(channelId): Promise { return this.slowmodeChannels.findOne({ where: { guild_id: this.guildId, @@ -51,7 +51,7 @@ export class GuildSlowmodes extends BaseGuildRepository { }); } - async getChannelSlowmodeUser(channelId, userId): Promise { + async getChannelSlowmodeUser(channelId, userId): Promise { return this.slowmodeUsers.findOne({ guild_id: this.guildId, channel_id: channelId, diff --git a/backend/src/data/GuildTags.ts b/backend/src/data/GuildTags.ts index cc879ded..385bfd80 100644 --- a/backend/src/data/GuildTags.ts +++ b/backend/src/data/GuildTags.ts @@ -21,7 +21,7 @@ export class GuildTags extends BaseGuildRepository { }); } - async find(tag): Promise { + async find(tag): Promise { return this.tags.findOne({ where: { guild_id: this.guildId, @@ -61,7 +61,7 @@ export class GuildTags extends BaseGuildRepository { }); } - async findResponseByCommandMessageId(messageId: string): Promise { + async findResponseByCommandMessageId(messageId: string): Promise { return this.tagResponses.findOne({ where: { guild_id: this.guildId, @@ -70,7 +70,7 @@ export class GuildTags extends BaseGuildRepository { }); } - async findResponseByResponseMessageId(messageId: string): Promise { + async findResponseByResponseMessageId(messageId: string): Promise { return this.tagResponses.findOne({ where: { guild_id: this.guildId, diff --git a/backend/src/data/UsernameHistory.ts b/backend/src/data/UsernameHistory.ts index 09a4057c..bcc5647a 100644 --- a/backend/src/data/UsernameHistory.ts +++ b/backend/src/data/UsernameHistory.ts @@ -39,7 +39,7 @@ export class UsernameHistory extends BaseRepository { }); } - getLastEntry(userId): Promise { + getLastEntry(userId): Promise { return this.usernameHistory.findOne({ where: { user_id: userId, diff --git a/backend/src/data/entities/AllowedGuild.ts b/backend/src/data/entities/AllowedGuild.ts index e12c4247..c8de9edd 100644 --- a/backend/src/data/entities/AllowedGuild.ts +++ b/backend/src/data/entities/AllowedGuild.ts @@ -9,8 +9,8 @@ export class AllowedGuild { @Column() name: string; - @Column() - icon: string; + @Column({ nullable: true }) + icon: string | null; @Column() owner_id: string; diff --git a/backend/src/data/entities/ArchiveEntry.ts b/backend/src/data/entities/ArchiveEntry.ts index 5893a66b..7ab37bd6 100644 --- a/backend/src/data/entities/ArchiveEntry.ts +++ b/backend/src/data/entities/ArchiveEntry.ts @@ -17,5 +17,5 @@ export class ArchiveEntry { @Column() created_at: string; - @Column() expires_at: string; + @Column({ nullable: true }) expires_at: string | null; } diff --git a/backend/src/data/entities/Case.ts b/backend/src/data/entities/Case.ts index a492b695..9972c5e9 100644 --- a/backend/src/data/entities/Case.ts +++ b/backend/src/data/entities/Case.ts @@ -13,27 +13,27 @@ export class Case { @Column() user_name: string; - @Column() mod_id: string; + @Column({ nullable: true }) mod_id: string | null; - @Column() mod_name: string; + @Column({ nullable: true }) mod_name: string | null; @Column() type: number; - @Column() audit_log_id: string; + @Column({ nullable: true }) audit_log_id: string | null; @Column() created_at: string; @Column() is_hidden: boolean; - @Column() pp_id: string; + @Column({ nullable: true }) pp_id: string | null; - @Column() pp_name: string; + @Column({ nullable: true }) pp_name: string | null; /** * ID of the channel and message where this case was logged. * Format: "channelid-messageid" */ - @Column() log_message_id: string; + @Column({ nullable: true }) log_message_id: string | null; @OneToMany( type => CaseNote, diff --git a/backend/src/data/entities/Mute.ts b/backend/src/data/entities/Mute.ts index ab1e8d41..0f72e5c0 100644 --- a/backend/src/data/entities/Mute.ts +++ b/backend/src/data/entities/Mute.ts @@ -12,7 +12,7 @@ export class Mute { @Column() created_at: string; - @Column() expires_at: string; + @Column({ nullable: true }) expires_at: string | null; @Column() case_id: number; } diff --git a/backend/src/data/entities/SavedMessage.ts b/backend/src/data/entities/SavedMessage.ts index 8b4f6468..ccccf7a2 100644 --- a/backend/src/data/entities/SavedMessage.ts +++ b/backend/src/data/entities/SavedMessage.ts @@ -1,9 +1,9 @@ import { Column, Entity, PrimaryColumn } from "typeorm"; import { createEncryptedJsonTransformer } from "../encryptedJsonTransformer"; -import { Sticker } from "eris"; +import { Attachment, Sticker } from "eris"; export interface ISavedMessageData { - attachments?: object[]; + attachments?: Attachment[]; author: { username: string; discriminator: string; diff --git a/backend/src/data/entities/ScheduledPost.ts b/backend/src/data/entities/ScheduledPost.ts index 000c309e..30920658 100644 --- a/backend/src/data/entities/ScheduledPost.ts +++ b/backend/src/data/entities/ScheduledPost.ts @@ -20,16 +20,16 @@ export class ScheduledPost { @Column("simple-json") attachments: Attachment[]; - @Column() post_at: string; + @Column({ nullable: true }) post_at: string | null; /** * How often to post the message, in milliseconds */ - @Column() repeat_interval: number; + @Column({ nullable: true }) repeat_interval: number | null; - @Column() repeat_until: string; + @Column({ nullable: true }) repeat_until: string | null; - @Column() repeat_times: number; + @Column({ nullable: true }) repeat_times: number | null; @Column() enable_mentions: boolean; } diff --git a/backend/src/index.ts b/backend/src/index.ts index 3daf475c..2b584fe7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -212,13 +212,13 @@ connect().then(async () => { sendSuccessMessageFn(channel, body) { const guildId = channel instanceof TextChannel ? channel.guild.id : undefined; - const emoji = guildId ? bot.getLoadedGuild(guildId).config.success_emoji : undefined; + const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined; channel.createMessage(successMessage(body, emoji)); }, sendErrorMessageFn(channel, body) { const guildId = channel instanceof TextChannel ? channel.guild.id : undefined; - const emoji = guildId ? bot.getLoadedGuild(guildId).config.error_emoji : undefined; + const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined; channel.createMessage(errorMessage(body, emoji)); }, }, diff --git a/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts b/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts index c3c3a8ff..691167f9 100644 --- a/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts +++ b/backend/src/migrations/1556909512501-MigrateUsernamesToNewHistoryTable.ts @@ -19,8 +19,8 @@ export class MigrateUsernamesToNewHistoryTable1556909512501 implements Migration const migrateNextBatch = (): Promise<{ finished: boolean; migrated?: number }> => { return new Promise(async resolve => { - const toInsert = []; - const toDelete = []; + const toInsert: any[][] = []; + const toDelete: number[] = []; const stream = await queryRunner.stream( `SELECT * FROM name_history WHERE type=1 ORDER BY timestamp ASC LIMIT ${BATCH_SIZE}`, diff --git a/backend/src/migrations/1562838838927-AddMoreIndicesToVCAlerts.ts b/backend/src/migrations/1562838838927-AddMoreIndicesToVCAlerts.ts index dffb505f..fee06468 100644 --- a/backend/src/migrations/1562838838927-AddMoreIndicesToVCAlerts.ts +++ b/backend/src/migrations/1562838838927-AddMoreIndicesToVCAlerts.ts @@ -2,7 +2,7 @@ import { MigrationInterface, QueryRunner, TableIndex } from "typeorm"; export class AddMoreIndicesToVCAlerts1562838838927 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable("vc_alerts"); + const table = (await queryRunner.getTable("vc_alerts"))!; await table.addIndex( new TableIndex({ columnNames: ["requestor_id"], @@ -16,7 +16,7 @@ export class AddMoreIndicesToVCAlerts1562838838927 implements MigrationInterface } public async down(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable("vc_alerts"); + const table = (await queryRunner.getTable("vc_alerts"))!; await table.removeIndex( new TableIndex({ columnNames: ["requestor_id"], diff --git a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts index c921a65a..91a672ba 100644 --- a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts +++ b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts @@ -6,7 +6,7 @@ export class AddTypeAndPermissionsToApiPermissions1573158035867 implements Migra await queryRunner.dropPrimaryKey("api_permissions"); } catch (e) {} // tslint:disable-line - const table = await queryRunner.getTable("api_permissions"); + const table = (await queryRunner.getTable("api_permissions"))!; if (table.indices.length) { await queryRunner.dropIndex("api_permissions", table.indices[0]); } diff --git a/backend/src/paths.ts b/backend/src/paths.ts index ad5dfed0..9d45ca21 100644 --- a/backend/src/paths.ts +++ b/backend/src/paths.ts @@ -3,7 +3,7 @@ import pkgUp from "pkg-up"; const backendPackageJson = pkgUp.sync({ cwd: __dirname, -}); +}) as string; export const backendDir = path.dirname(backendPackageJson); export const rootDir = path.resolve(backendDir, ".."); diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 9cbd5f27..9b48f809 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -32,26 +32,28 @@ export function hasPermission(pluginData: AnyPluginData, permission: string return helpers.hasPermission(config, permission); } -const PluginOverrideCriteriaType = t.recursion("PluginOverrideCriteriaType", () => - t.type({ - channel: tNullable(t.union([t.string, t.array(t.string)])), - category: tNullable(t.union([t.string, t.array(t.string)])), - level: tNullable(t.union([t.string, t.array(t.string)])), - user: tNullable(t.union([t.string, t.array(t.string)])), - role: tNullable(t.union([t.string, t.array(t.string)])), +const PluginOverrideCriteriaType: t.Type> = t.recursion( + "PluginOverrideCriteriaType", + () => + t.partial({ + channel: tNullable(t.union([t.string, t.array(t.string)])), + category: tNullable(t.union([t.string, t.array(t.string)])), + level: tNullable(t.union([t.string, t.array(t.string)])), + user: tNullable(t.union([t.string, t.array(t.string)])), + role: tNullable(t.union([t.string, t.array(t.string)])), - all: tNullable(t.array(PluginOverrideCriteriaType)), - any: tNullable(t.array(PluginOverrideCriteriaType)), - not: tNullable(PluginOverrideCriteriaType), + all: tNullable(t.array(PluginOverrideCriteriaType)), + any: tNullable(t.array(PluginOverrideCriteriaType)), + not: tNullable(PluginOverrideCriteriaType), - extra: t.unknown, - }), + extra: t.unknown, + }), ); const BasicPluginStructureType = t.type({ enabled: tNullable(t.boolean), config: tNullable(t.unknown), - overrides: tNullable(t.array(PluginOverrideCriteriaType)), + overrides: tNullable(t.array(t.union([PluginOverrideCriteriaType, t.type({ config: t.unknown })]))), replaceDefaultOverrides: tNullable(t.boolean), }); @@ -101,7 +103,7 @@ export function getPluginConfigPreprocessor( // 4. Merge with default options and validate/decode the entire config let decodedConfig = {}; - const decodedOverrides = []; + const decodedOverrides: Array & { config: any }> = []; if (options.config) { decodedConfig = blueprint.configSchema diff --git a/backend/src/plugins/AutoDelete/types.ts b/backend/src/plugins/AutoDelete/types.ts index bcb285b5..f0a1eb1a 100644 --- a/backend/src/plugins/AutoDelete/types.ts +++ b/backend/src/plugins/AutoDelete/types.ts @@ -4,6 +4,7 @@ import { tDelayString, MINUTES } from "../../utils"; import { GuildLogs } from "../../data/GuildLogs"; import { GuildSavedMessages } from "../../data/GuildSavedMessages"; import { SavedMessage } from "../../data/entities/SavedMessage"; +import Timeout = NodeJS.Timeout; export const MAX_DELAY = 5 * MINUTES; @@ -25,8 +26,8 @@ export interface AutoDeletePluginType extends BasePluginType { guildLogs: GuildLogs; deletionQueue: IDeletionQueueItem[]; - nextDeletion: number; - nextDeletionTimeout; + nextDeletion: number | null; + nextDeletionTimeout: Timeout | null; maxDelayWarningSent: boolean; diff --git a/backend/src/plugins/AutoDelete/util/onMessageCreate.ts b/backend/src/plugins/AutoDelete/util/onMessageCreate.ts index 20773ed8..0b325c8a 100644 --- a/backend/src/plugins/AutoDelete/util/onMessageCreate.ts +++ b/backend/src/plugins/AutoDelete/util/onMessageCreate.ts @@ -9,7 +9,7 @@ export async function onMessageCreate(pluginData: GuildPluginData MAX_DELAY) { delay = MAX_DELAY; diff --git a/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts b/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts index 60c47088..ebae82a9 100644 --- a/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts +++ b/backend/src/plugins/AutoDelete/util/scheduleNextDeletion.ts @@ -4,11 +4,11 @@ import { deleteNextItem } from "./deleteNextItem"; export function scheduleNextDeletion(pluginData: GuildPluginData) { if (pluginData.state.deletionQueue.length === 0) { - clearTimeout(pluginData.state.nextDeletionTimeout); + clearTimeout(pluginData.state.nextDeletionTimeout!); return; } const firstDeleteAt = pluginData.state.deletionQueue[0].deleteAt; - clearTimeout(pluginData.state.nextDeletionTimeout); + clearTimeout(pluginData.state.nextDeletionTimeout!); pluginData.state.nextDeletionTimeout = setTimeout(() => deleteNextItem(pluginData), firstDeleteAt - Date.now()); } diff --git a/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts b/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts index fc742868..8a4a67e1 100644 --- a/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts +++ b/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts @@ -20,9 +20,9 @@ export const NewAutoReactionsCmd = autoReactionsCmd({ }, async run({ message: msg, args, pluginData }) { - const finalReactions = []; + const finalReactions: string[] = []; - const me = pluginData.guild.members.get(pluginData.client.user.id); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; const missingPermissions = getMissingChannelPermissions(me, args.channel as GuildChannel, requiredPermissions); if (missingPermissions) { sendErrorMessage( diff --git a/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts b/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts index 281f48eb..54f44951 100644 --- a/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts +++ b/backend/src/plugins/AutoReactions/events/AddReactionsEvt.ts @@ -19,7 +19,7 @@ export const AddReactionsEvt = autoReactionsEvt({ const autoReaction = await pluginData.state.autoReactions.getForChannel(message.channel.id); if (!autoReaction) return; - const me = pluginData.guild.members.get(pluginData.client.user.id); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; const missingPermissions = getMissingChannelPermissions( me, message.channel as GuildChannel, diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index c93b884b..84936aa6 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -158,7 +158,7 @@ export const AutomodPlugin = zeppelinGuildPlugin()("automod", configPreprocessor, customOverrideMatcher(pluginData, criteria, matchParams) { - return criteria?.antiraid_level && criteria.antiraid_level === pluginData.state.cachedAntiraidLevel; + return criteria?.antiraid_level ? criteria.antiraid_level === pluginData.state.cachedAntiraidLevel : false; }, events: [ diff --git a/backend/src/plugins/Automod/actions/addRoles.ts b/backend/src/plugins/Automod/actions/addRoles.ts index 8af1b081..4da915e7 100644 --- a/backend/src/plugins/Automod/actions/addRoles.ts +++ b/backend/src/plugins/Automod/actions/addRoles.ts @@ -1,7 +1,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { unique } from "../../../utils"; +import { nonNullish, unique } from "../../../utils"; import { Constants } from "eris"; import { hasDiscordPermissions } from "../../../utils/hasDiscordPermissions"; import { LogsPlugin } from "../../Logs/LogsPlugin"; @@ -17,8 +17,8 @@ export const AddRolesAction = automodAction({ defaultConfig: [], async apply({ pluginData, contexts, actionConfig, ruleName }) { - const members = unique(contexts.map(c => c.member).filter(Boolean)); - const me = pluginData.guild.members.get(pluginData.client.user.id); + const members = unique(contexts.map(c => c.member).filter(nonNullish)); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; const missingPermissions = getMissingPermissions(me.permission, p.manageRoles); if (missingPermissions) { @@ -29,8 +29,8 @@ export const AddRolesAction = automodAction({ return; } - const rolesToAssign = []; - const rolesWeCannotAssign = []; + const rolesToAssign: string[] = []; + const rolesWeCannotAssign: string[] = []; for (const roleId of actionConfig) { if (canAssignRole(pluginData.guild, me, roleId)) { rolesToAssign.push(roleId); diff --git a/backend/src/plugins/Automod/actions/ban.ts b/backend/src/plugins/Automod/actions/ban.ts index 0cc5da22..fe15cc7e 100644 --- a/backend/src/plugins/Automod/actions/ban.ts +++ b/backend/src/plugins/Automod/actions/ban.ts @@ -1,7 +1,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; +import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; @@ -20,14 +20,14 @@ export const BanAction = automodAction({ async apply({ pluginData, contexts, actionConfig, matchResult }) { const reason = actionConfig.reason || "Kicked automatically"; const contactMethods = resolveActionContactMethods(pluginData, actionConfig); - const deleteMessageDays = actionConfig.deleteMessageDays; + const deleteMessageDays = actionConfig.deleteMessageDays || undefined; const caseArgs = { modId: pluginData.client.user.id, - extraNotes: [matchResult.fullSummary], + extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [], }; - const userIdsToBan = unique(contexts.map(c => c.user?.id).filter(Boolean)); + const userIdsToBan = unique(contexts.map(c => c.user?.id).filter(nonNullish)); const modActions = pluginData.getPlugin(ModActionsPlugin); for (const userId of userIdsToBan) { diff --git a/backend/src/plugins/Automod/actions/changeNickname.ts b/backend/src/plugins/Automod/actions/changeNickname.ts index addde6af..7225f794 100644 --- a/backend/src/plugins/Automod/actions/changeNickname.ts +++ b/backend/src/plugins/Automod/actions/changeNickname.ts @@ -2,7 +2,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; import { LogsPlugin } from "../../Logs/LogsPlugin"; -import { unique } from "../../../utils"; +import { nonNullish, unique } from "../../../utils"; export const ChangeNicknameAction = automodAction({ configType: t.union([ @@ -15,7 +15,7 @@ export const ChangeNicknameAction = automodAction({ defaultConfig: {}, async apply({ pluginData, contexts, actionConfig }) { - const members = unique(contexts.map(c => c.member).filter(Boolean)); + const members = unique(contexts.map(c => c.member).filter(nonNullish)); for (const member of members) { if (pluginData.state.recentNicknameChanges.has(member.id)) continue; diff --git a/backend/src/plugins/Automod/actions/clean.ts b/backend/src/plugins/Automod/actions/clean.ts index b3e69bf6..3c2c0c7d 100644 --- a/backend/src/plugins/Automod/actions/clean.ts +++ b/backend/src/plugins/Automod/actions/clean.ts @@ -15,12 +15,12 @@ export const CleanAction = automodAction({ messageIdsToDeleteByChannelId.set(context.message.channel_id, []); } - if (messageIdsToDeleteByChannelId.get(context.message.channel_id).includes(context.message.id)) { + if (messageIdsToDeleteByChannelId.get(context.message.channel_id)!.includes(context.message.id)) { console.warn(`Message ID to delete was already present: ${pluginData.guild.name}, rule ${ruleName}`); continue; } - messageIdsToDeleteByChannelId.get(context.message.channel_id).push(context.message.id); + messageIdsToDeleteByChannelId.get(context.message.channel_id)!.push(context.message.id); } } diff --git a/backend/src/plugins/Automod/actions/kick.ts b/backend/src/plugins/Automod/actions/kick.ts index 7d4f688a..3a1f73c4 100644 --- a/backend/src/plugins/Automod/actions/kick.ts +++ b/backend/src/plugins/Automod/actions/kick.ts @@ -1,7 +1,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; +import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; @@ -22,14 +22,15 @@ export const KickAction = automodAction({ const caseArgs = { modId: pluginData.client.user.id, - extraNotes: [matchResult.fullSummary], + extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [], }; - const userIdsToKick = unique(contexts.map(c => c.user?.id).filter(Boolean)); + const userIdsToKick = unique(contexts.map(c => c.user?.id).filter(nonNullish)); const membersToKick = await asyncMap(userIdsToKick, id => resolveMember(pluginData.client, pluginData.guild, id)); const modActions = pluginData.getPlugin(ModActionsPlugin); for (const member of membersToKick) { + if (!member) continue; await modActions.kickMember(member, reason, { contactMethods, caseArgs }); } }, diff --git a/backend/src/plugins/Automod/actions/mute.ts b/backend/src/plugins/Automod/actions/mute.ts index b26d7fc5..d9d1ee25 100644 --- a/backend/src/plugins/Automod/actions/mute.ts +++ b/backend/src/plugins/Automod/actions/mute.ts @@ -1,7 +1,15 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, convertDelayStringToMS, resolveMember, tDelayString, tNullable, unique } from "../../../utils"; +import { + asyncMap, + convertDelayStringToMS, + nonNullish, + resolveMember, + tDelayString, + tNullable, + unique, +} from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { MutesPlugin } from "../../Mutes/MutesPlugin"; @@ -21,16 +29,16 @@ export const MuteAction = automodAction({ }, async apply({ pluginData, contexts, actionConfig, ruleName, matchResult }) { - const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration) : null; + const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined; const reason = actionConfig.reason || "Muted automatically"; const contactMethods = resolveActionContactMethods(pluginData, actionConfig); const caseArgs = { modId: pluginData.client.user.id, - extraNotes: [matchResult.fullSummary], + extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [], }; - const userIdsToMute = unique(contexts.map(c => c.user?.id).filter(Boolean)); + const userIdsToMute = unique(contexts.map(c => c.user?.id).filter(nonNullish)); const mutes = pluginData.getPlugin(MutesPlugin); for (const userId of userIdsToMute) { diff --git a/backend/src/plugins/Automod/actions/removeRoles.ts b/backend/src/plugins/Automod/actions/removeRoles.ts index 12245f1b..664ee083 100644 --- a/backend/src/plugins/Automod/actions/removeRoles.ts +++ b/backend/src/plugins/Automod/actions/removeRoles.ts @@ -1,7 +1,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; +import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; import { getMissingPermissions } from "../../../utils/getMissingPermissions"; @@ -19,8 +19,8 @@ export const RemoveRolesAction = automodAction({ defaultConfig: [], async apply({ pluginData, contexts, actionConfig, ruleName }) { - const members = unique(contexts.map(c => c.member).filter(Boolean)); - const me = pluginData.guild.members.get(pluginData.client.user.id); + const members = unique(contexts.map(c => c.member).filter(nonNullish)); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; const missingPermissions = getMissingPermissions(me.permission, p.manageRoles); if (missingPermissions) { @@ -31,8 +31,8 @@ export const RemoveRolesAction = automodAction({ return; } - const rolesToRemove = []; - const rolesWeCannotRemove = []; + const rolesToRemove: string[] = []; + const rolesWeCannotRemove: string[] = []; for (const roleId of actionConfig) { if (canAssignRole(pluginData.guild, me, roleId)) { rolesToRemove.push(roleId); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index 9c808a2e..0a451d00 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -28,14 +28,14 @@ export const ReplyAction = automodAction({ async apply({ pluginData, contexts, actionConfig }) { const contextsWithTextChannels = contexts .filter(c => c.message?.channel_id) - .filter(c => pluginData.guild.channels.get(c.message.channel_id) instanceof TextChannel); + .filter(c => pluginData.guild.channels.get(c.message!.channel_id) instanceof TextChannel); const contextsByChannelId = contextsWithTextChannels.reduce((map: Map, context) => { - if (!map.has(context.message.channel_id)) { - map.set(context.message.channel_id, []); + if (!map.has(context.message!.channel_id)) { + map.set(context.message!.channel_id, []); } - map.get(context.message.channel_id).push(context); + map.get(context.message!.channel_id)!.push(context); return map; }, new Map()); @@ -57,7 +57,7 @@ export const ReplyAction = automodAction({ const replyMsg = await channel.createMessage(formatted); if (typeof actionConfig === "object" && actionConfig.auto_delete) { - const delay = convertDelayStringToMS(String(actionConfig.auto_delete)); + const delay = convertDelayStringToMS(String(actionConfig.auto_delete))!; setTimeout(() => replyMsg.delete().catch(noop), delay); } } diff --git a/backend/src/plugins/Automod/actions/setAntiraidLevel.ts b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts index 99a5900b..fca9632f 100644 --- a/backend/src/plugins/Automod/actions/setAntiraidLevel.ts +++ b/backend/src/plugins/Automod/actions/setAntiraidLevel.ts @@ -4,7 +4,7 @@ import { setAntiraidLevel } from "../functions/setAntiraidLevel"; export const SetAntiraidLevelAction = automodAction({ configType: t.string, - defaultConfig: null, + defaultConfig: "", async apply({ pluginData, contexts, actionConfig }) { setAntiraidLevel(pluginData, actionConfig); diff --git a/backend/src/plugins/Automod/actions/warn.ts b/backend/src/plugins/Automod/actions/warn.ts index 972ec9d2..781580b0 100644 --- a/backend/src/plugins/Automod/actions/warn.ts +++ b/backend/src/plugins/Automod/actions/warn.ts @@ -1,7 +1,7 @@ import * as t from "io-ts"; import { automodAction } from "../helpers"; import { LogType } from "../../../data/LogType"; -import { asyncMap, resolveMember, tNullable, unique } from "../../../utils"; +import { asyncMap, nonNullish, resolveMember, tNullable, unique } from "../../../utils"; import { resolveActionContactMethods } from "../functions/resolveActionContactMethods"; import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin"; @@ -22,14 +22,15 @@ export const WarnAction = automodAction({ const caseArgs = { modId: pluginData.client.user.id, - extraNotes: [matchResult.fullSummary], + extraNotes: matchResult.fullSummary ? [matchResult.fullSummary] : [], }; - const userIdsToWarn = unique(contexts.map(c => c.user?.id).filter(Boolean)); + const userIdsToWarn = unique(contexts.map(c => c.user?.id).filter(nonNullish)); const membersToWarn = await asyncMap(userIdsToWarn, id => resolveMember(pluginData.client, pluginData.guild, id)); const modActions = pluginData.getPlugin(ModActionsPlugin); for (const member of membersToWarn) { + if (!member) continue; await modActions.warnMember(member, reason, { contactMethods, caseArgs }); } }, diff --git a/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts index 78ebd0a2..90b8dba6 100644 --- a/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts +++ b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts @@ -4,8 +4,9 @@ import { RECENT_ACTION_EXPIRY_TIME, RecentActionType } from "../constants"; import { getEmojiInString, getRoleMentions, getUrlsInString, getUserMentions } from "../../../utils"; export function addRecentActionsFromMessage(pluginData: GuildPluginData, context: AutomodContext) { - const globalIdentifier = context.message.user_id; - const perChannelIdentifier = `${context.message.channel_id}-${context.message.user_id}`; + const message = context.message!; + const globalIdentifier = message.user_id; + const perChannelIdentifier = `${message.channel_id}-${message.user_id}`; const expiresAt = Date.now() + RECENT_ACTION_EXPIRY_TIME; pluginData.state.recentActions.push({ @@ -23,8 +24,7 @@ export function addRecentActionsFromMessage(pluginData: GuildPluginData, context: AutomodContext) { - const globalIdentifier = context.message.user_id; - const perChannelIdentifier = `${context.message.channel_id}-${context.message.user_id}`; + const message = context.message!; + const globalIdentifier = message.user_id; + const perChannelIdentifier = `${message.channel_id}-${message.user_id}`; pluginData.state.recentActions = pluginData.state.recentActions.filter(act => { return act.identifier !== globalIdentifier && act.identifier !== perChannelIdentifier; diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts index 63839f47..09f4af78 100644 --- a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts +++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts @@ -7,6 +7,7 @@ import { findRecentSpam } from "./findRecentSpam"; import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions"; import * as t from "io-ts"; import { getMessageSpamIdentifier } from "./getSpamIdentifier"; +import { SavedMessage } from "../../../data/entities/SavedMessage"; const MessageSpamTriggerConfig = t.type({ amount: t.number, @@ -29,22 +30,25 @@ export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: return; } - const spamIdentifier = getMessageSpamIdentifier(context.message, triggerConfig.per_channel); + const spamIdentifier = getMessageSpamIdentifier(context.message, Boolean(triggerConfig.per_channel)); const recentSpam = findRecentSpam(pluginData, spamType, spamIdentifier); if (recentSpam) { - await pluginData.state.archives.addSavedMessagesToArchive( - recentSpam.archiveId, - [context.message], - pluginData.guild, - ); + if (recentSpam.archiveId) { + await pluginData.state.archives.addSavedMessagesToArchive( + recentSpam.archiveId, + [context.message], + pluginData.guild, + ); + } return { silentClean: true, + extra: { archiveId: "" }, // FIXME: Fix up automod trigger match() typings so extra is not required when doing a silentClean }; } - const within = convertDelayStringToMS(triggerConfig.within); + const within = convertDelayStringToMS(triggerConfig.within) ?? 0; const matchedSpam = getMatchingMessageRecentActions( pluginData, context.message, @@ -58,7 +62,7 @@ export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: const messages = matchedSpam.recentActions .map(action => action.context.message) .filter(Boolean) - .sort(sorter("posted_at")); + .sort(sorter("posted_at")) as SavedMessage[]; const archiveId = await pluginData.state.archives.createFromSavedMessages(messages, pluginData.guild); diff --git a/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts index c25cd5cc..56d251c2 100644 --- a/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts +++ b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts @@ -16,7 +16,7 @@ export function getMatchingRecentActions( action.type === type && (!identifier || action.identifier === identifier) && action.context.timestamp >= since && - action.context.timestamp <= to && + action.context.timestamp <= to! && !action.context.actioned ); }); diff --git a/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts b/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts index 8983bbd3..9a86dc13 100644 --- a/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts +++ b/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts @@ -10,23 +10,25 @@ export function getTextMatchPartialSummary( context: AutomodContext, ) { if (type === "message") { - const channel = pluginData.guild.channels.get(context.message.channel_id); - const channelMention = channel ? verboseChannelMention(channel) : `\`#${context.message.channel_id}\``; + const message = context.message!; + const channel = pluginData.guild.channels.get(message.channel_id); + const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; - return `message in ${channelMention}:\n${messageSummary(context.message)}`; + return `message in ${channelMention}:\n${messageSummary(message)}`; } else if (type === "embed") { - const channel = pluginData.guild.channels.get(context.message.channel_id); - const channelMention = channel ? verboseChannelMention(channel) : `\`#${context.message.channel_id}\``; + const message = context.message!; + const channel = pluginData.guild.channels.get(message.channel_id); + const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; - return `message embed in ${channelMention}:\n${messageSummary(context.message)}`; + return `message embed in ${channelMention}:\n${messageSummary(message)}`; } else if (type === "username") { - return `username: ${context.user.username}`; + return `username: ${context.user!.username}`; } else if (type === "nickname") { - return `nickname: ${context.member.nick}`; + return `nickname: ${context.member!.nick}`; } else if (type === "visiblename") { - const visibleName = context.member?.nick || context.user.username; + const visibleName = context.member?.nick || context.user!.username; return `visible name: ${visibleName}`; } else if (type === "customstatus") { - return `custom status: ${context.member.game.state}`; + return `custom status: ${context.member!.game!.state}`; } } diff --git a/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts b/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts index 5ce5e799..2309a95a 100644 --- a/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts +++ b/backend/src/plugins/Automod/functions/resolveActionContactMethods.ts @@ -7,10 +7,10 @@ import { AutomodPluginType } from "../types"; export function resolveActionContactMethods( pluginData: GuildPluginData, actionConfig: { - notify?: string; - notifyChannel?: string; + notify?: string | null; + notifyChannel?: string | null; }, -): UserNotificationMethod[] | null { +): UserNotificationMethod[] { if (actionConfig.notify === "dm") { return [{ type: "dm" }]; } else if (actionConfig.notify === "channel") { @@ -28,5 +28,5 @@ export function resolveActionContactMethods( return []; } - return null; + return []; } diff --git a/backend/src/plugins/Automod/functions/runAutomod.ts b/backend/src/plugins/Automod/functions/runAutomod.ts index c8fe9318..661d9f13 100644 --- a/backend/src/plugins/Automod/functions/runAutomod.ts +++ b/backend/src/plugins/Automod/functions/runAutomod.ts @@ -5,13 +5,14 @@ import { availableActions } from "../actions/availableActions"; import { AutomodTriggerMatchResult } from "../helpers"; import { CleanAction } from "../actions/clean"; import { checkAndUpdateCooldown } from "./checkAndUpdateCooldown"; +import { TextChannel } from "eris"; export async function runAutomod(pluginData: GuildPluginData, context: AutomodContext) { const userId = context.user?.id || context.member?.id || context.message?.user_id; const user = context.user || (userId && pluginData.client.users.get(userId)); - const member = context.member || (userId && pluginData.guild.members.get(userId)); + const member = context.member || (userId && pluginData.guild.members.get(userId)) || null; const channelId = context.message?.channel_id; - const channel = channelId && pluginData.guild.channels.get(channelId); + const channel = channelId ? (pluginData.guild.channels.get(channelId) as TextChannel) : null; const categoryId = channel?.parentID; const config = pluginData.config.getMatchingConfig({ @@ -29,8 +30,8 @@ export async function runAutomod(pluginData: GuildPluginData, return; } - let matchResult: AutomodTriggerMatchResult; - let contexts: AutomodContext[]; + let matchResult: AutomodTriggerMatchResult | null | undefined; + let contexts: AutomodContext[] = []; triggerLoop: for (const triggerItem of rule.triggers) { for (const [triggerName, triggerConfig] of Object.entries(triggerItem)) { diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts index 3bda6370..f14e0331 100644 --- a/backend/src/plugins/Automod/helpers.ts +++ b/backend/src/plugins/Automod/helpers.ts @@ -3,9 +3,8 @@ import { Awaitable } from "knub/dist/utils"; import * as t from "io-ts"; import { AutomodContext, AutomodPluginType } from "./types"; -export interface AutomodTriggerMatchResult { +interface BaseAutomodTriggerMatchResult { extraContexts?: AutomodContext[]; - extra?: TExtra; silentClean?: boolean; // TODO: Maybe generalize to a "silent" value in general, which mutes alert/log @@ -13,12 +12,16 @@ export interface AutomodTriggerMatchResult { fullSummary?: string; } +export type AutomodTriggerMatchResult = unknown extends TExtra + ? BaseAutomodTriggerMatchResult + : BaseAutomodTriggerMatchResult & { extra: TExtra }; + type AutomodTriggerMatchFn = (meta: { ruleName: string; pluginData: GuildPluginData; context: AutomodContext; triggerConfig: TConfigType; -}) => Awaitable>; +}) => Awaitable>; type AutomodTriggerRenderMatchInformationFn = (meta: { ruleName: string; diff --git a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts index 308e0c8b..45f29697 100644 --- a/backend/src/plugins/Automod/triggers/matchAttachmentType.ts +++ b/backend/src/plugins/Automod/triggers/matchAttachmentType.ts @@ -73,15 +73,15 @@ export const MatchAttachmentTypeTrigger = automodTrigger()({ }, renderMatchInformation({ pluginData, contexts, matchResult }) { - const channel = pluginData.guild.channels.get(contexts[0].message.channel_id); + const channel = pluginData.guild.channels.get(contexts[0].message!.channel_id)!; const prettyChannel = verboseChannelMention(channel); return ( asSingleLine(` Matched attachment type \`${disableInlineCode(matchResult.extra.matchedType)}\` (${matchResult.extra.mode === "blacklist" ? "(blacklisted)" : "(not in whitelist)"}) - in message (\`${contexts[0].message.id}\`) in ${prettyChannel}: - `) + messageSummary(contexts[0].message) + in message (\`${contexts[0].message!.id}\`) in ${prettyChannel}: + `) + messageSummary(contexts[0].message!) ); }, }); diff --git a/backend/src/plugins/Automod/triggers/matchInvites.ts b/backend/src/plugins/Automod/triggers/matchInvites.ts index 234b4578..8cab2614 100644 --- a/backend/src/plugins/Automod/triggers/matchInvites.ts +++ b/backend/src/plugins/Automod/triggers/matchInvites.ts @@ -1,10 +1,10 @@ import * as t from "io-ts"; -import { GuildInvite } from "eris"; import { automodTrigger } from "../helpers"; import { disableCodeBlocks, disableInlineCode, getInviteCodesInString, + GuildInvite, isGuildInvite, resolveInvite, tNullable, diff --git a/backend/src/plugins/Automod/triggers/memberJoin.ts b/backend/src/plugins/Automod/triggers/memberJoin.ts index 50b84541..210cd8e5 100644 --- a/backend/src/plugins/Automod/triggers/memberJoin.ts +++ b/backend/src/plugins/Automod/triggers/memberJoin.ts @@ -19,7 +19,7 @@ export const MemberJoinTrigger = automodTrigger()({ } if (triggerConfig.only_new) { - const threshold = Date.now() - convertDelayStringToMS(triggerConfig.new_threshold); + const threshold = Date.now() - convertDelayStringToMS(triggerConfig.new_threshold)!; return context.member.createdAt >= threshold ? {} : null; } @@ -27,6 +27,6 @@ export const MemberJoinTrigger = automodTrigger()({ }, renderMatchInformation({ pluginData, contexts, triggerConfig }) { - return null; + return ""; }, }); diff --git a/backend/src/plugins/Automod/triggers/memberJoinSpam.ts b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts index 2003a41c..f0f9b704 100644 --- a/backend/src/plugins/Automod/triggers/memberJoinSpam.ts +++ b/backend/src/plugins/Automod/triggers/memberJoinSpam.ts @@ -25,7 +25,7 @@ export const MemberJoinSpamTrigger = automodTrigger()({ return {}; } - const since = Date.now() - convertDelayStringToMS(triggerConfig.within); + const since = Date.now() - convertDelayStringToMS(triggerConfig.within)!; const matchingActions = getMatchingRecentActions(pluginData, RecentActionType.MemberJoin, null, since); const totalCount = sumRecentActionCounts(matchingActions); @@ -46,6 +46,6 @@ export const MemberJoinSpamTrigger = automodTrigger()({ }, renderMatchInformation({ pluginData, contexts, triggerConfig }) { - return null; + return ""; }, }); diff --git a/backend/src/plugins/Automod/triggers/roleAdded.ts b/backend/src/plugins/Automod/triggers/roleAdded.ts index 7fb89ee5..06dacd2b 100644 --- a/backend/src/plugins/Automod/triggers/roleAdded.ts +++ b/backend/src/plugins/Automod/triggers/roleAdded.ts @@ -9,16 +9,16 @@ interface RoleAddedMatchResult { export const RoleAddedTrigger = automodTrigger()({ configType: t.union([t.string, t.array(t.string)]), - defaultConfig: null, + defaultConfig: [], async match({ triggerConfig, context, pluginData }) { - if (!context.member || !context.rolesChanged || context.rolesChanged.added.length === 0) { + if (!context.member || !context.rolesChanged || context.rolesChanged.added!.length === 0) { return; } const triggerRoles = Array.isArray(triggerConfig) ? triggerConfig : [triggerConfig]; for (const roleId of triggerRoles) { - if (context.rolesChanged.added.includes(roleId)) { + if (context.rolesChanged.added!.includes(roleId)) { if (consumeIgnoredRoleChange(pluginData, context.member.id, roleId)) { continue; } diff --git a/backend/src/plugins/Automod/triggers/roleRemoved.ts b/backend/src/plugins/Automod/triggers/roleRemoved.ts index fa04b3fc..e6268185 100644 --- a/backend/src/plugins/Automod/triggers/roleRemoved.ts +++ b/backend/src/plugins/Automod/triggers/roleRemoved.ts @@ -9,10 +9,10 @@ interface RoleAddedMatchResult { export const RoleRemovedTrigger = automodTrigger()({ configType: t.union([t.string, t.array(t.string)]), - defaultConfig: null, + defaultConfig: [], async match({ triggerConfig, context, pluginData }) { - if (!context.member || !context.rolesChanged || context.rolesChanged.removed.length === 0) { + if (!context.member || !context.rolesChanged || context.rolesChanged.removed!.length === 0) { return; } @@ -22,7 +22,7 @@ export const RoleRemovedTrigger = automodTrigger()({ continue; } - if (context.rolesChanged.removed.includes(roleId)) { + if (context.rolesChanged.removed!.includes(roleId)) { return { extra: { matchedRoleId: roleId, diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts index 2c16269f..6b71e301 100644 --- a/backend/src/plugins/Automod/types.ts +++ b/backend/src/plugins/Automod/types.ts @@ -105,13 +105,13 @@ export interface AutomodContext { export interface RecentAction { type: RecentActionType; - identifier: string; + identifier: string | null; count: number; context: AutomodContext; } export interface RecentSpam { - archiveId: string; + archiveId: string | null; type: RecentActionType; identifiers: string[]; timestamp: number; diff --git a/backend/src/plugins/BotControl/BotControlPlugin.ts b/backend/src/plugins/BotControl/BotControlPlugin.ts index 55d6e93b..5e871612 100644 --- a/backend/src/plugins/BotControl/BotControlPlugin.ts +++ b/backend/src/plugins/BotControl/BotControlPlugin.ts @@ -45,8 +45,9 @@ export const BotControlPlugin = zeppelinGlobalPlugin()("bo pluginData.state.configs = new Configs(); pluginData.state.apiPermissionAssignments = new ApiPermissionAssignments(); - if (getActiveReload()) { - const [guildId, channelId] = getActiveReload(); + const activeReload = getActiveReload(); + if (activeReload) { + const [guildId, channelId] = activeReload; resetActiveReload(); const guild = pluginData.client.guilds.get(guildId); diff --git a/backend/src/plugins/BotControl/activeReload.ts b/backend/src/plugins/BotControl/activeReload.ts index 1303fd0a..996fa152 100644 --- a/backend/src/plugins/BotControl/activeReload.ts +++ b/backend/src/plugins/BotControl/activeReload.ts @@ -1,4 +1,4 @@ -let activeReload: [string, string] = null; +let activeReload: [string, string] | null = null; export function getActiveReload() { return activeReload; diff --git a/backend/src/plugins/BotControl/commands/LeaveServerCmd.ts b/backend/src/plugins/BotControl/commands/LeaveServerCmd.ts index 26035147..51b47d1d 100644 --- a/backend/src/plugins/BotControl/commands/LeaveServerCmd.ts +++ b/backend/src/plugins/BotControl/commands/LeaveServerCmd.ts @@ -19,7 +19,7 @@ export const LeaveServerCmd = botControlCmd({ return; } - const guildToLeave = pluginData.client.guilds.get(args.guildId); + const guildToLeave = pluginData.client.guilds.get(args.guildId)!; const guildName = guildToLeave.name; try { diff --git a/backend/src/plugins/BotControl/commands/ServersCmd.ts b/backend/src/plugins/BotControl/commands/ServersCmd.ts index 6ec71343..7d4be31c 100644 --- a/backend/src/plugins/BotControl/commands/ServersCmd.ts +++ b/backend/src/plugins/BotControl/commands/ServersCmd.ts @@ -21,7 +21,7 @@ export const ServersCmd = botControlCmd({ async run({ pluginData, message: msg, args }) { const showList = Boolean(args.all || args.initialized || args.uninitialized || args.search); - const search = args.search && new RegExp([...args.search].map(s => escapeStringRegexp(s)).join(".*"), "i"); + const search = args.search ? new RegExp([...args.search].map(s => escapeStringRegexp(s)).join(".*"), "i") : null; const joinedGuilds = Array.from(pluginData.client.guilds.values()); const loadedGuilds = pluginData.getKnubInstance().getLoadedGuilds(); @@ -39,7 +39,7 @@ export const ServersCmd = botControlCmd({ } if (args.search) { - filteredGuilds = filteredGuilds.filter(g => search.test(`${g.id} ${g.name}`)); + filteredGuilds = filteredGuilds.filter(g => search!.test(`${g.id} ${g.name}`)); } if (filteredGuilds.length) { diff --git a/backend/src/plugins/Cases/functions/createCase.ts b/backend/src/plugins/Cases/functions/createCase.ts index ff61c6d2..a8f58ba6 100644 --- a/backend/src/plugins/Cases/functions/createCase.ts +++ b/backend/src/plugins/Cases/functions/createCase.ts @@ -12,7 +12,7 @@ export async function createCase(pluginData: GuildPluginData, a const mod = await resolveUser(pluginData.client, args.modId); const modName = `${mod.username}#${mod.discriminator}`; - let ppName = null; + let ppName: string | null = null; if (args.ppId) { const pp = await resolveUser(pluginData.client, args.ppId); ppName = `${pp.username}#${pp.discriminator}`; diff --git a/backend/src/plugins/Cases/functions/getCaseEmbed.ts b/backend/src/plugins/Cases/functions/getCaseEmbed.ts index 21be1035..d9482415 100644 --- a/backend/src/plugins/Cases/functions/getCaseEmbed.ts +++ b/backend/src/plugins/Cases/functions/getCaseEmbed.ts @@ -15,7 +15,9 @@ export async function getCaseEmbed( requestMemberId?: string, ): Promise { const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); - if (!theCase) return null; + if (!theCase) { + throw new Error("Unknown case"); + } const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); diff --git a/backend/src/plugins/Cases/functions/getCaseSummary.ts b/backend/src/plugins/Cases/functions/getCaseSummary.ts index 887b3f9e..a1395f63 100644 --- a/backend/src/plugins/Cases/functions/getCaseSummary.ts +++ b/backend/src/plugins/Cases/functions/getCaseSummary.ts @@ -28,12 +28,13 @@ export async function getCaseSummary( caseOrCaseId: Case | number, withLinks = false, requestMemberId?: string, -) { +): Promise { const config = pluginData.config.get(); const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const caseId = caseOrCaseId instanceof Case ? caseOrCaseId.id : caseOrCaseId; const theCase = await pluginData.state.cases.with("notes").find(caseId); + if (!theCase) return null; const firstNote = theCase.notes[0]; let reason = firstNote ? firstNote.body : ""; @@ -47,7 +48,7 @@ export async function getCaseSummary( if (reason.length > CASE_SUMMARY_REASON_MAX_LENGTH) { const match = reason.slice(CASE_SUMMARY_REASON_MAX_LENGTH, 100).match(/(?:[.,!?\s]|$)/); - const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index : CASE_SUMMARY_REASON_MAX_LENGTH; + const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index! : CASE_SUMMARY_REASON_MAX_LENGTH; if (nextWhitespaceIndex < reason.length) { reason = reason.slice(0, nextWhitespaceIndex - 1) + "..."; } @@ -56,7 +57,7 @@ export async function getCaseSummary( reason = disableLinkPreviews(reason); const timestamp = moment.utc(theCase.created_at, DBDateFormat); - const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff); + const relativeTimeCutoff = convertDelayStringToMS(config.relative_time_cutoff)!; const useRelativeTime = config.show_relative_times && Date.now() - timestamp.valueOf() < relativeTimeCutoff; const timestampWithTz = requestMemberId ? await timeAndDate.inMemberTz(requestMemberId, timestamp) diff --git a/backend/src/plugins/Cases/functions/postToCaseLogChannel.ts b/backend/src/plugins/Cases/functions/postToCaseLogChannel.ts index c311ec1b..b08f0282 100644 --- a/backend/src/plugins/Cases/functions/postToCaseLogChannel.ts +++ b/backend/src/plugins/Cases/functions/postToCaseLogChannel.ts @@ -11,13 +11,13 @@ import { logger } from "../../../logger"; export async function postToCaseLogChannel( pluginData: GuildPluginData, content: MessageContent, - file: MessageFile = null, + file?: MessageFile, ): Promise { const caseLogChannelId = pluginData.config.get().case_log_channel; - if (!caseLogChannelId) return; + if (!caseLogChannelId) return null; const caseLogChannel = pluginData.guild.channels.get(caseLogChannelId); - if (!caseLogChannel || !(caseLogChannel instanceof TextChannel)) return; + if (!caseLogChannel || !(caseLogChannel instanceof TextChannel)) return null; let result; try { @@ -30,7 +30,7 @@ export async function postToCaseLogChannel( pluginData.state.logs.log(LogType.BOT_ALERT, { body: `Missing permissions to post mod cases in <#${caseLogChannel.id}>`, }); - return; + return null; } throw e; @@ -44,17 +44,17 @@ export async function postCaseToCaseLogChannel( caseOrCaseId: Case | number, ): Promise { const theCase = await pluginData.state.cases.find(resolveCaseId(caseOrCaseId)); - if (!theCase) return; + if (!theCase) return null; const caseEmbed = await getCaseEmbed(pluginData, caseOrCaseId); - if (!caseEmbed) return; + if (!caseEmbed) return null; if (theCase.log_message_id) { const [channelId, messageId] = theCase.log_message_id.split("-"); try { await pluginData.client.editMessage(channelId, messageId, caseEmbed); - return; + return null; } catch (e) {} // tslint:disable-line:no-empty } diff --git a/backend/src/plugins/Censor/util/applyFiltersToMsg.ts b/backend/src/plugins/Censor/util/applyFiltersToMsg.ts index c66648bc..7a12639b 100644 --- a/backend/src/plugins/Censor/util/applyFiltersToMsg.ts +++ b/backend/src/plugins/Censor/util/applyFiltersToMsg.ts @@ -1,16 +1,9 @@ import { GuildPluginData } from "knub"; import { CensorPluginType } from "../types"; import { SavedMessage } from "../../../data/entities/SavedMessage"; -import { AnyInvite, Embed, GuildInvite } from "eris"; +import { Embed, Invite } from "eris"; import { ZalgoRegex } from "../../../data/Zalgo"; -import { - getInviteCodesInString, - getUrlsInString, - resolveMember, - resolveInvite, - isGuildInvite, - isRESTGuildInvite, -} from "../../../utils"; +import { getInviteCodesInString, getUrlsInString, resolveMember, resolveInvite, isGuildInvite } from "../../../utils"; import cloneDeep from "lodash.clonedeep"; import { censorMessage } from "./censorMessage"; import escapeStringRegexp from "escape-string-regexp"; @@ -59,7 +52,7 @@ export async function applyFiltersToMsg( const inviteCodes = getInviteCodesInString(messageContent); - const invites: Array = await Promise.all( + const invites: Array = await Promise.all( inviteCodes.map(code => resolveInvite(pluginData.client, code)), ); @@ -75,7 +68,7 @@ export async function applyFiltersToMsg( return true; } - if (isRESTGuildInvite(invite)) { + if (isGuildInvite(invite)) { if (inviteGuildWhitelist && !inviteGuildWhitelist.includes(invite.guild.id)) { censorMessage( pluginData, diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts index 4193e336..373f2aa4 100644 --- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -46,9 +46,9 @@ export const ArchiveChannelCmd = channelArchiverCmd({ const maxMessagesToArchive = args.messages ? Math.min(args.messages, MAX_ARCHIVED_MESSAGES) : MAX_ARCHIVED_MESSAGES; if (maxMessagesToArchive <= 0) return; - const archiveLines = []; + const archiveLines: string[] = []; let archivedMessages = 0; - let previousId; + let previousId: string | undefined; const startTime = Date.now(); const progressMsg = await msg.channel.createMessage("Creating archive..."); @@ -80,7 +80,7 @@ export const ArchiveChannelCmd = channelArchiverCmd({ } if (message.reactions && Object.keys(message.reactions).length > 0) { - const reactionCounts = []; + const reactionCounts: string[] = []; for (const [emoji, info] of Object.entries(message.reactions)) { reactionCounts.push(`${info.count}x ${emoji}`); } diff --git a/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts b/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts index 0b2bded4..9287b080 100644 --- a/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts +++ b/backend/src/plugins/CompanionChannels/functions/getCompanionChannelOptsForVoiceChannelId.ts @@ -15,7 +15,8 @@ export function getCompanionChannelOptsForVoiceChannelId( return Object.values(config.entries) .filter( opts => - opts.voice_channel_ids.includes(voiceChannel.id) || opts.voice_channel_ids.includes(voiceChannel.parentID), + opts.voice_channel_ids.includes(voiceChannel.id) || + (voiceChannel.parentID && opts.voice_channel_ids.includes(voiceChannel.parentID)), ) .map(opts => Object.assign({}, defaultCompanionChannelOpts, opts)); } diff --git a/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts b/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts index ec898db6..0807fe3b 100644 --- a/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts +++ b/backend/src/plugins/CompanionChannels/functions/handleCompanionPermissions.ts @@ -24,7 +24,7 @@ export async function handleCompanionPermissions( export async function handleCompanionPermissions( pluginData: GuildPluginData, userId: string, - voiceChannel?: VoiceChannel, + voiceChannel: VoiceChannel | null, oldChannel?: VoiceChannel, ) { if (pluginData.state.errorCooldownManager.isOnCooldown(ERROR_COOLDOWN_KEY)) { @@ -65,7 +65,7 @@ export async function handleCompanionPermissions( for (const channelId of permsToDelete) { const channel = pluginData.guild.channels.get(channelId); if (!channel || !(channel instanceof TextChannel)) continue; - await channel.deletePermission(userId, `Companion Channel for ${oldChannel.id} | User Left`); + await channel.deletePermission(userId, `Companion Channel for ${oldChannel!.id} | User Left`); } for (const [channelId, permissions] of permsToSet) { @@ -76,7 +76,7 @@ export async function handleCompanionPermissions( permissions, 0, "member", - `Companion Channel for ${voiceChannel.id} | User Joined`, + `Companion Channel for ${voiceChannel!.id} | User Joined`, ); } } catch (e) { diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index 3a5faf01..77e28878 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -25,7 +25,7 @@ export async function addRoleAction( const target = await resolveMember(pluginData.client, pluginData.guild, targetId); if (!target) throw new ActionError(`Unknown target member: ${targetId}`); - if (event.trigger.type === "command" && !canActOn(pluginData, (eventData.msg as Message).member, target)) { + if (event.trigger.type === "command" && !canActOn(pluginData, eventData.msg.member, target)) { throw new ActionError("Missing permissions"); } diff --git a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts index 1aa0515a..d138c61d 100644 --- a/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts +++ b/backend/src/plugins/CustomEvents/actions/moveToVoiceChannelAction.ts @@ -25,7 +25,7 @@ export async function moveToVoiceChannelAction( const target = await resolveMember(pluginData.client, pluginData.guild, targetId); if (!target) throw new ActionError("Unknown target member"); - if (event.trigger.type === "command" && !canActOn(pluginData, (eventData.msg as Message).member, target)) { + if (event.trigger.type === "command" && !canActOn(pluginData, eventData.msg.member, target)) { throw new ActionError("Missing permissions"); } diff --git a/backend/src/plugins/LocateUser/LocateUserPlugin.ts b/backend/src/plugins/LocateUser/LocateUserPlugin.ts index 4d71ea16..3e04139b 100644 --- a/backend/src/plugins/LocateUser/LocateUserPlugin.ts +++ b/backend/src/plugins/LocateUser/LocateUserPlugin.ts @@ -10,6 +10,7 @@ import { DeleteFollowCmd, ListFollowCmd } from "./commands/ListFollowCmd"; import { ChannelJoinAlertsEvt, ChannelLeaveAlertsEvt, ChannelSwitchAlertsEvt } from "./events/SendAlertsEvts"; import { GuildBanRemoveAlertsEvt } from "./events/BanRemoveAlertsEvt"; import { trimPluginDescription } from "../../utils"; +import Timeout = NodeJS.Timeout; const defaultOptions: PluginOptions = { config: { @@ -70,7 +71,7 @@ export const LocateUserPlugin = zeppelinGuildPlugin()("loc }, onUnload(pluginData) { - clearTimeout(pluginData.state.outdatedAlertsTimeout); + clearTimeout(pluginData.state.outdatedAlertsTimeout as Timeout); pluginData.state.unloaded = true; }, }); diff --git a/backend/src/plugins/LocateUser/commands/WhereCmd.ts b/backend/src/plugins/LocateUser/commands/WhereCmd.ts index c8137144..0662e880 100644 --- a/backend/src/plugins/LocateUser/commands/WhereCmd.ts +++ b/backend/src/plugins/LocateUser/commands/WhereCmd.ts @@ -14,7 +14,6 @@ export const WhereCmd = locateUserCmd({ }, async run({ message: msg, args, pluginData }) { - const member = await resolveMember(pluginData.client, pluginData.guild, args.member.id); - sendWhere(pluginData, member, msg.channel, `${msg.member.mention} | `); + sendWhere(pluginData, args.member, msg.channel, `${msg.member.mention} | `); }, }); diff --git a/backend/src/plugins/LocateUser/types.ts b/backend/src/plugins/LocateUser/types.ts index c438caf9..1a42eb26 100644 --- a/backend/src/plugins/LocateUser/types.ts +++ b/backend/src/plugins/LocateUser/types.ts @@ -13,7 +13,7 @@ export interface LocateUserPluginType extends BasePluginType { config: TConfigSchema; state: { alerts: GuildVCAlerts; - outdatedAlertsTimeout: Timeout; + outdatedAlertsTimeout: Timeout | null; usersWithAlerts: string[]; unloaded: boolean; }; diff --git a/backend/src/plugins/LocateUser/utils/sendAlerts.ts b/backend/src/plugins/LocateUser/utils/sendAlerts.ts index f0013492..df49c3d1 100644 --- a/backend/src/plugins/LocateUser/utils/sendAlerts.ts +++ b/backend/src/plugins/LocateUser/utils/sendAlerts.ts @@ -8,6 +8,7 @@ import { moveMember } from "./moveMember"; export async function sendAlerts(pluginData: GuildPluginData, userId: string) { const triggeredAlerts = await pluginData.state.alerts.getAlertsByUserId(userId); const member = await resolveMember(pluginData.client, pluginData.guild, userId); + if (!member) return; triggeredAlerts.forEach(alert => { const prepend = `<@!${alert.requestor_id}>, an alert requested by you has triggered!\nReminder: \`${alert.body}\`\n`; diff --git a/backend/src/plugins/LocateUser/utils/sendWhere.ts b/backend/src/plugins/LocateUser/utils/sendWhere.ts index 87b5f38a..4ab52e45 100644 --- a/backend/src/plugins/LocateUser/utils/sendWhere.ts +++ b/backend/src/plugins/LocateUser/utils/sendWhere.ts @@ -1,4 +1,4 @@ -import { Member, TextableChannel, VoiceChannel } from "eris"; +import { Invite, Member, TextableChannel, VoiceChannel } from "eris"; import { getInviteLink } from "knub/dist/helpers"; import { createOrReuseInvite } from "./createOrReuseInvite"; import { GuildPluginData } from "knub"; @@ -11,12 +11,14 @@ export async function sendWhere( channel: TextableChannel, prepend: string, ) { - const voice = pluginData.guild.channels.get(member.voiceState.channelID) as VoiceChannel; + const voice = member.voiceState.channelID + ? (pluginData.guild.channels.get(member.voiceState.channelID) as VoiceChannel) + : null; if (voice == null) { channel.createMessage(prepend + "That user is not in a channel"); } else { - let invite = null; + let invite: Invite; try { invite = await createOrReuseInvite(voice); } catch (e) { diff --git a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts index f7d71498..b7a731f7 100644 --- a/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts +++ b/backend/src/plugins/Logs/events/LogsGuildMemberAddEvt.ts @@ -28,11 +28,11 @@ export const LogsGuildMemberAddEvt = logsEvt({ cases.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)); if (cases.length) { - const recentCaseLines = []; + const recentCaseLines: string[] = []; const recentCases = cases.slice(0, 2); const casesPlugin = pluginData.getPlugin(CasesPlugin); for (const theCase of recentCases) { - recentCaseLines.push(await casesPlugin.getCaseSummary(theCase)); + recentCaseLines.push((await casesPlugin.getCaseSummary(theCase))!); } let recentCaseSummary = recentCaseLines.join("\n"); diff --git a/backend/src/plugins/Logs/util/getLogMessage.ts b/backend/src/plugins/Logs/util/getLogMessage.ts index 1fd4a809..818acfde 100644 --- a/backend/src/plugins/Logs/util/getLogMessage.ts +++ b/backend/src/plugins/Logs/util/getLogMessage.ts @@ -14,16 +14,17 @@ import { renderTemplate, TemplateParseError } from "../../../templateFormatter"; import { logger } from "../../../logger"; import moment from "moment-timezone"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; +import { MessageContent } from "eris"; export async function getLogMessage( pluginData: GuildPluginData, type: LogType, data: any, opts?: Pick, -): Promise { +): Promise { const config = pluginData.config.get(); const format = opts?.format?.[LogType[type]] || config.format[LogType[type]] || ""; - if (format === "" || format == null) return; + if (format === "" || format == null) return null; // See comment on FORMAT_NO_TIMESTAMP in types.ts const timestampFormat = @@ -45,7 +46,7 @@ export async function getLogMessage( const usersOrMembers = Array.isArray(inputUserOrMember) ? inputUserOrMember : [inputUserOrMember]; - const mentions = []; + const mentions: string[] = []; for (const userOrMember of usersOrMembers) { let user; let member; @@ -91,7 +92,7 @@ export async function getLogMessage( } catch (e) { if (e instanceof TemplateParseError) { logger.error(`Error when parsing template:\nError: ${e.message}\nTemplate: ${format}`); - return; + return null; } else { throw e; } diff --git a/backend/src/plugins/Logs/util/log.ts b/backend/src/plugins/Logs/util/log.ts index 852a224e..c38d8dda 100644 --- a/backend/src/plugins/Logs/util/log.ts +++ b/backend/src/plugins/Logs/util/log.ts @@ -109,13 +109,13 @@ export async function log(pluginData: GuildPluginData, type: Log if (!pluginData.state.batches.has(channel.id)) { pluginData.state.batches.set(channel.id, []); setTimeout(async () => { - const batchedMessage = pluginData.state.batches.get(channel.id).join("\n"); + const batchedMessage = pluginData.state.batches.get(channel.id)!.join("\n"); pluginData.state.batches.delete(channel.id); createChunkedMessage(channel, batchedMessage).catch(noop); }, batchTime); } - pluginData.state.batches.get(channel.id).push(message); + pluginData.state.batches.get(channel.id)!.push(message); } else { // If we're not batching log messages, just send them immediately await createChunkedMessage(channel, message).catch(noop); diff --git a/backend/src/plugins/MessageSaver/saveMessagesToDB.ts b/backend/src/plugins/MessageSaver/saveMessagesToDB.ts index 61a84dae..a2aebde6 100644 --- a/backend/src/plugins/MessageSaver/saveMessagesToDB.ts +++ b/backend/src/plugins/MessageSaver/saveMessagesToDB.ts @@ -7,7 +7,7 @@ export async function saveMessagesToDB( channel: TextChannel, ids: string[], ) { - const failed = []; + const failed: string[] = []; for (const id of ids) { const savedMessage = await pluginData.state.savedMessages.find(id); if (savedMessage) continue; diff --git a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts index 42125e3d..51312393 100644 --- a/backend/src/plugins/ModActions/commands/AddCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/AddCaseCmd.ts @@ -65,7 +65,7 @@ export const AddCaseCmd = modActionsCmd({ modId: mod.id, type: CaseTypes[type], reason, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, }); if (user) { diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index 18ec3078..abd5fb68 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -78,7 +78,7 @@ export const BanCmd = modActionsCmd({ contactMethods, caseArgs: { modId: mod.id, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, }, deleteMessageDays, }); diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts index 2012cceb..519f3a28 100644 --- a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -9,6 +9,7 @@ import { LogsPlugin } from "../../Logs/LogsPlugin"; import { LogType } from "../../../data/LogType"; import moment from "moment-timezone"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; +import { Case } from "../../../data/entities/Case"; export const DeleteCaseCmd = modActionsCmd({ trigger: ["delete_case", "deletecase"], @@ -25,8 +26,8 @@ export const DeleteCaseCmd = modActionsCmd({ }, async run({ pluginData, message, args }) { - const failed = []; - const validCases = []; + const failed: number[] = []; + const validCases: Case[] = []; let cancelled = 0; for (const num of args.caseNumber) { diff --git a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts index 2d9c2c50..703b26d9 100644 --- a/backend/src/plugins/ModActions/commands/ForcebanCmd.ts +++ b/backend/src/plugins/ModActions/commands/ForcebanCmd.ts @@ -77,7 +77,7 @@ export const ForcebanCmd = modActionsCmd({ modId: mod.id, type: CaseTypes.Ban, reason, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, }); // Confirm the action diff --git a/backend/src/plugins/ModActions/commands/HideCaseCmd.ts b/backend/src/plugins/ModActions/commands/HideCaseCmd.ts index ac351c05..0e12521f 100644 --- a/backend/src/plugins/ModActions/commands/HideCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/HideCaseCmd.ts @@ -14,7 +14,7 @@ export const HideCaseCmd = modActionsCmd({ ], async run({ pluginData, message: msg, args }) { - const failed = []; + const failed: number[] = []; for (const num of args.caseNum) { const theCase = await pluginData.state.cases.findByCaseNumber(num); diff --git a/backend/src/plugins/ModActions/commands/MassBanCmd.ts b/backend/src/plugins/ModActions/commands/MassBanCmd.ts index c3ed7732..c4c1860f 100644 --- a/backend/src/plugins/ModActions/commands/MassBanCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassBanCmd.ts @@ -62,7 +62,7 @@ export const MassbanCmd = modActionsCmd({ const loadingMsg = await msg.channel.createMessage("Banning..."); // Ban each user and count failed bans (if any) - const failedBans = []; + const failedBans: string[] = []; const casesPlugin = pluginData.getPlugin(CasesPlugin); for (const userId of args.userIds) { try { diff --git a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts index 1a129ad1..73e329d0 100644 --- a/backend/src/plugins/ModActions/commands/MassmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/MassmuteCmd.ts @@ -62,7 +62,7 @@ export const MassmuteCmd = modActionsCmd({ // Mute everyone and count fails const modId = msg.author.id; - const failedMutes = []; + const failedMutes: string[] = []; const mutesPlugin = pluginData.getPlugin(MutesPlugin); for (const userId of args.userIds) { try { diff --git a/backend/src/plugins/ModActions/commands/UnbanCmd.ts b/backend/src/plugins/ModActions/commands/UnbanCmd.ts index 92ba46a3..6b7531e7 100644 --- a/backend/src/plugins/ModActions/commands/UnbanCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnbanCmd.ts @@ -59,7 +59,7 @@ export const UnbanCmd = modActionsCmd({ modId: mod.id, type: CaseTypes.Unban, reason, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, }); // Confirm the action diff --git a/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts b/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts index 5cea8927..5418b2a5 100644 --- a/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnhideCaseCmd.ts @@ -14,7 +14,7 @@ export const UnhideCaseCmd = modActionsCmd({ ], async run({ pluginData, message: msg, args }) { - const failed = []; + const failed: number[] = []; for (const num of args.caseNum) { const theCase = await pluginData.state.cases.findByCaseNumber(num); diff --git a/backend/src/plugins/ModActions/commands/UpdateCmd.ts b/backend/src/plugins/ModActions/commands/UpdateCmd.ts index 8bb9bbfc..3a55463a 100644 --- a/backend/src/plugins/ModActions/commands/UpdateCmd.ts +++ b/backend/src/plugins/ModActions/commands/UpdateCmd.ts @@ -24,7 +24,7 @@ export const UpdateCmd = modActionsCmd({ ], async run({ pluginData, message: msg, args }) { - let theCase: Case; + let theCase: Case | undefined; if (args.caseNumber != null) { theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber); } else { diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index 964a0327..2f5686d7 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -91,7 +91,7 @@ export const WarnCmd = modActionsCmd({ contactMethods, caseArgs: { modId: mod.id, - ppId: mod.id !== msg.author.id ? msg.author.id : null, + ppId: mod.id !== msg.author.id ? msg.author.id : undefined, reason, }, retryPromptChannel: msg.channel as TextChannel, diff --git a/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts b/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts index e9324508..a899c34c 100644 --- a/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateBanCaseOnManualBanEvt.ts @@ -1,12 +1,12 @@ import { IgnoredEventType, modActionsEvt } from "../types"; import { isEventIgnored } from "../functions/isEventIgnored"; import { clearIgnoredEvents } from "../functions/clearIgnoredEvents"; -import { Constants as ErisConstants } from "eris"; +import { Constants as ErisConstants, User } from "eris"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; import { safeFindRelevantAuditLogEntry } from "../../../utils/safeFindRelevantAuditLogEntry"; import { LogType } from "../../../data/LogType"; -import { stripObjectToScalars, resolveUser } from "../../../utils"; +import { stripObjectToScalars, resolveUser, UnknownUser } from "../../../utils"; /** * Create a BAN case automatically when a user is banned manually. @@ -29,27 +29,27 @@ export const CreateBanCaseOnManualBanEvt = modActionsEvt( const casesPlugin = pluginData.getPlugin(CasesPlugin); let createdCase; - let mod = null; + let mod: User | UnknownUser | null = null; let reason = ""; if (relevantAuditLogEntry) { const modId = relevantAuditLogEntry.user.id; const auditLogId = relevantAuditLogEntry.id; - mod = resolveUser(pluginData.client, modId); - reason = relevantAuditLogEntry.reason; + mod = await resolveUser(pluginData.client, modId); + reason = relevantAuditLogEntry.reason || ""; createdCase = await casesPlugin.createCase({ userId: user.id, modId, type: CaseTypes.Ban, auditLogId, - reason, + reason: reason || undefined, automatic: true, }); } else { createdCase = await casesPlugin.createCase({ userId: user.id, - modId: null, + modId: "0", type: CaseTypes.Ban, }); } diff --git a/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts b/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts index 610767ca..e1527b2d 100644 --- a/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateKickCaseOnManualKickEvt.ts @@ -41,7 +41,7 @@ export const CreateKickCaseOnManualKickEvt = modActionsEvt( modId: kickAuditLogEntry.user.id, type: CaseTypes.Kick, auditLogId: kickAuditLogEntry.id, - reason: kickAuditLogEntry.reason, + reason: kickAuditLogEntry.reason || undefined, automatic: true, }); } diff --git a/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts b/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts index 0d368615..acd7e57f 100644 --- a/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts +++ b/backend/src/plugins/ModActions/events/CreateUnbanCaseOnManualUnbanEvt.ts @@ -1,11 +1,11 @@ import { IgnoredEventType, modActionsEvt } from "../types"; import { isEventIgnored } from "../functions/isEventIgnored"; import { clearIgnoredEvents } from "../functions/clearIgnoredEvents"; -import { Constants as ErisConstants } from "eris"; +import { Constants as ErisConstants, User } from "eris"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; import { safeFindRelevantAuditLogEntry } from "../../../utils/safeFindRelevantAuditLogEntry"; -import { stripObjectToScalars, resolveUser } from "../../../utils"; +import { stripObjectToScalars, resolveUser, UnknownUser } from "../../../utils"; import { LogType } from "../../../data/LogType"; /** @@ -29,13 +29,13 @@ export const CreateUnbanCaseOnManualUnbanEvt = modActionsEvt( const casesPlugin = pluginData.getPlugin(CasesPlugin); let createdCase; - let mod = null; + let mod: User | UnknownUser | null = null; if (relevantAuditLogEntry) { const modId = relevantAuditLogEntry.user.id; const auditLogId = relevantAuditLogEntry.id; - mod = resolveUser(pluginData.client, modId); + mod = await resolveUser(pluginData.client, modId); createdCase = await casesPlugin.createCase({ userId: user.id, modId, @@ -46,7 +46,7 @@ export const CreateUnbanCaseOnManualUnbanEvt = modActionsEvt( } else { createdCase = await casesPlugin.createCase({ userId: user.id, - modId: null, + modId: "0", type: CaseTypes.Unban, automatic: true, }); diff --git a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts index afd83af3..6d640ec6 100644 --- a/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualMuteUserCmd.ts @@ -10,6 +10,7 @@ import { MutesPlugin } from "../../Mutes/MutesPlugin"; import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs"; import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError"; import { logger } from "../../../logger"; +import { GuildMessage } from "knub/dist/helpers"; /** * The actual function run by both !mute and !forcemute. @@ -18,12 +19,12 @@ import { logger } from "../../../logger"; export async function actualMuteUserCmd( pluginData: GuildPluginData, user: User | UnknownUser, - msg: Message, + msg: GuildMessage, args: { time?: number; reason?: string; mod: Member; notify?: string; "notify-channel"?: TextChannel }, ) { // The moderator who did the action is the message author or, if used, the specified -mod - let mod = msg.member; - let pp = null; + let mod: Member = msg.member; + let pp: User | null = null; if (args.mod) { if (!hasPermission(pluginData, "can_act_as_other", { message: msg })) { @@ -36,7 +37,7 @@ export async function actualMuteUserCmd( } const timeUntilUnmute = args.time && humanizeDuration(args.time); - const reason = formatReasonWithAttachments(args.reason, msg.attachments); + const reason = args.reason ? formatReasonWithAttachments(args.reason, msg.attachments) : undefined; let muteResult: MuteResult; const mutesPlugin = pluginData.getPlugin(MutesPlugin); @@ -54,7 +55,7 @@ export async function actualMuteUserCmd( contactMethods, caseArgs: { modId: mod.id, - ppId: pp && pp.id, + ppId: pp ? pp.id : undefined, }, }); } catch (e) { diff --git a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts index e7b89049..0faee47c 100644 --- a/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts +++ b/backend/src/plugins/ModActions/functions/actualUnmuteUserCmd.ts @@ -15,7 +15,7 @@ export async function actualUnmuteCmd( ) { // The moderator who did the action is the message author or, if used, the specified -mod let mod = msg.author; - let pp = null; + let pp: User | null = null; if (args.mod) { if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) { @@ -27,15 +27,20 @@ export async function actualUnmuteCmd( pp = msg.author; } - const reason = formatReasonWithAttachments(args.reason, msg.attachments); + const reason = args.reason ? formatReasonWithAttachments(args.reason, msg.attachments) : undefined; const mutesPlugin = pluginData.getPlugin(MutesPlugin); const result = await mutesPlugin.unmuteUser(user.id, args.time, { modId: mod.id, - ppId: pp && pp.id, + ppId: pp ? pp.id : undefined, reason, }); + if (!result) { + sendErrorMessage(pluginData, msg.channel, "User is not muted!"); + return; + } + // Confirm the action to the moderator if (args.time) { const timeUntilUnmute = args.time && humanizeDuration(args.time); diff --git a/backend/src/plugins/ModActions/functions/banUserId.ts b/backend/src/plugins/ModActions/functions/banUserId.ts index 913b8e7e..65d6a1ec 100644 --- a/backend/src/plugins/ModActions/functions/banUserId.ts +++ b/backend/src/plugins/ModActions/functions/banUserId.ts @@ -1,6 +1,13 @@ import { GuildPluginData } from "knub"; import { BanOptions, BanResult, IgnoredEventType, ModActionsPluginType } from "../types"; -import { notifyUser, resolveUser, stripObjectToScalars, ucfirst, UserNotificationResult } from "../../../utils"; +import { + createUserNotificationError, + notifyUser, + resolveUser, + stripObjectToScalars, + ucfirst, + UserNotificationResult, +} from "../../../utils"; import { User } from "eris"; import { renderTemplate } from "../../../templateFormatter"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; @@ -15,7 +22,7 @@ import { CaseTypes } from "../../../data/CaseTypes"; export async function banUserId( pluginData: GuildPluginData, userId: string, - reason: string = null, + reason?: string, banOptions: BanOptions = {}, ): Promise { const config = pluginData.config.get(); @@ -30,15 +37,22 @@ export async function banUserId( // Attempt to message the user *before* banning them, as doing it after may not be possible let notifyResult: UserNotificationResult = { method: null, success: true }; if (reason && user instanceof User) { - const banMessage = await renderTemplate(config.ban_message, { - guildName: pluginData.guild.name, - reason, - }); - const contactMethods = banOptions?.contactMethods ? banOptions.contactMethods : getDefaultContactMethods(pluginData, "ban"); - notifyResult = await notifyUser(user, banMessage, contactMethods); + + if (contactMethods.length) { + if (config.ban_message) { + const banMessage = await renderTemplate(config.ban_message, { + guildName: pluginData.guild.name, + reason, + }); + + notifyResult = await notifyUser(user, banMessage, contactMethods); + } else { + notifyResult = createUserNotificationError("No ban message specified in config"); + } + } } // (Try to) ban the user @@ -59,18 +73,19 @@ export async function banUserId( } // Create a case for this action + const modId = banOptions.caseArgs?.modId || pluginData.client.user.id; const casesPlugin = pluginData.getPlugin(CasesPlugin); const createdCase = await casesPlugin.createCase({ ...(banOptions.caseArgs || {}), userId, - modId: banOptions.caseArgs?.modId, + modId, type: CaseTypes.Ban, reason, noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [], }); // Log the action - const mod = await resolveUser(pluginData.client, banOptions.caseArgs?.modId); + const mod = await resolveUser(pluginData.client, modId); pluginData.state.serverLogs.log(LogType.MEMBER_BAN, { mod: stripObjectToScalars(mod), user: stripObjectToScalars(user), diff --git a/backend/src/plugins/ModActions/functions/kickMember.ts b/backend/src/plugins/ModActions/functions/kickMember.ts index 497b252f..04ae4969 100644 --- a/backend/src/plugins/ModActions/functions/kickMember.ts +++ b/backend/src/plugins/ModActions/functions/kickMember.ts @@ -1,7 +1,14 @@ import { GuildPluginData } from "knub"; import { IgnoredEventType, KickOptions, KickResult, ModActionsPluginType } from "../types"; import { Member } from "eris"; -import { notifyUser, resolveUser, stripObjectToScalars, ucfirst, UserNotificationResult } from "../../../utils"; +import { + createUserNotificationError, + notifyUser, + resolveUser, + stripObjectToScalars, + ucfirst, + UserNotificationResult, +} from "../../../utils"; import { renderTemplate } from "../../../templateFormatter"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; import { LogType } from "../../../data/LogType"; @@ -15,7 +22,7 @@ import { CasesPlugin } from "../../Cases/CasesPlugin"; export async function kickMember( pluginData: GuildPluginData, member: Member, - reason: string = null, + reason?: string, kickOptions: KickOptions = {}, ): Promise { const config = pluginData.config.get(); @@ -23,15 +30,22 @@ export async function kickMember( // Attempt to message the user *before* kicking them, as doing it after may not be possible let notifyResult: UserNotificationResult = { method: null, success: true }; if (reason) { - const kickMessage = await renderTemplate(config.kick_message, { - guildName: pluginData.guild.name, - reason, - }); - const contactMethods = kickOptions?.contactMethods ? kickOptions.contactMethods : getDefaultContactMethods(pluginData, "kick"); - notifyResult = await notifyUser(member.user, kickMessage, contactMethods); + + if (contactMethods.length) { + if (config.kick_message) { + const kickMessage = await renderTemplate(config.kick_message, { + guildName: pluginData.guild.name, + reason, + }); + + notifyResult = await notifyUser(member.user, kickMessage, contactMethods); + } else { + notifyResult = createUserNotificationError("No kick message specified in the config"); + } + } } // Kick the user @@ -46,19 +60,21 @@ export async function kickMember( }; } + const modId = kickOptions.caseArgs?.modId || pluginData.client.user.id; + // Create a case for this action const casesPlugin = pluginData.getPlugin(CasesPlugin); const createdCase = await casesPlugin.createCase({ ...(kickOptions.caseArgs || {}), userId: member.id, - modId: kickOptions.caseArgs?.modId, + modId, type: CaseTypes.Kick, reason, noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [], }); // Log the action - const mod = await resolveUser(pluginData.client, kickOptions.caseArgs?.modId); + const mod = await resolveUser(pluginData.client, modId); pluginData.state.serverLogs.log(LogType.MEMBER_KICK, { mod: stripObjectToScalars(mod), user: stripObjectToScalars(member.user), diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index fdaf15c7..e3e8e3cf 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -2,7 +2,14 @@ import { GuildPluginData } from "knub"; import { ModActionsPluginType, WarnOptions, WarnResult } from "../types"; import { Member } from "eris"; import { getDefaultContactMethods } from "./getDefaultContactMethods"; -import { notifyUser, resolveUser, stripObjectToScalars, ucfirst } from "../../../utils"; +import { + createUserNotificationError, + notifyUser, + resolveUser, + stripObjectToScalars, + ucfirst, + UserNotificationResult, +} from "../../../utils"; import { waitForReaction } from "knub/dist/helpers"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; @@ -13,14 +20,19 @@ export async function warnMember( member: Member, reason: string, warnOptions: WarnOptions = {}, -): Promise { +): Promise { const config = pluginData.config.get(); - const warnMessage = config.warn_message.replace("{guildName}", pluginData.guild.name).replace("{reason}", reason); - const contactMethods = warnOptions?.contactMethods - ? warnOptions.contactMethods - : getDefaultContactMethods(pluginData, "warn"); - const notifyResult = await notifyUser(member.user, warnMessage, contactMethods); + let notifyResult: UserNotificationResult; + if (config.warn_message) { + const warnMessage = config.warn_message.replace("{guildName}", pluginData.guild.name).replace("{reason}", reason); + const contactMethods = warnOptions?.contactMethods + ? warnOptions.contactMethods + : getDefaultContactMethods(pluginData, "warn"); + notifyResult = await notifyUser(member.user, warnMessage, contactMethods); + } else { + notifyResult = createUserNotificationError("No warn message specified in config"); + } if (!notifyResult.success) { if (warnOptions.retryPromptChannel && pluginData.guild.channels.has(warnOptions.retryPromptChannel.id)) { @@ -43,17 +55,19 @@ export async function warnMember( } } + const modId = warnOptions.caseArgs?.modId ?? pluginData.client.user.id; + const casesPlugin = pluginData.getPlugin(CasesPlugin); const createdCase = await casesPlugin.createCase({ ...(warnOptions.caseArgs || {}), userId: member.id, - modId: warnOptions.caseArgs?.modId, + modId, type: CaseTypes.Warn, reason, noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [], }); - const mod = await resolveUser(pluginData.client, warnOptions.caseArgs?.modId); + const mod = await resolveUser(pluginData.client, modId); pluginData.state.serverLogs.log(LogType.MEMBER_WARN, { mod: stripObjectToScalars(mod), member: stripObjectToScalars(member, ["user", "roles"]), diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index a8cce398..b4db3610 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -98,9 +98,9 @@ export type BanResult = export type WarnMemberNotifyRetryCallback = () => boolean | Promise; export interface WarnOptions { - caseArgs?: Partial; - contactMethods?: UserNotificationMethod[]; - retryPromptChannel?: TextChannel; + caseArgs?: Partial | null; + contactMethods?: UserNotificationMethod[] | null; + retryPromptChannel?: TextChannel | null; } export interface KickOptions { diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts index 14b73e61..d580325e 100644 --- a/backend/src/plugins/Mutes/MutesPlugin.ts +++ b/backend/src/plugins/Mutes/MutesPlugin.ts @@ -17,6 +17,7 @@ import { CaseArgs } from "../Cases/types"; import { Member } from "eris"; import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt"; import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt"; +import { mapToPublicFn } from "../../pluginUtils"; const defaultOptions = { config: { @@ -82,19 +83,12 @@ export const MutesPlugin = zeppelinGuildPlugin()("mutes", { ], public: { - muteUser(pluginData) { - return (userId: string, muteTime: number = null, reason: string = null, muteOptions: MuteOptions = {}) => { - return muteUser(pluginData, userId, muteTime, reason, muteOptions); - }; - }, - unmuteUser(pluginData) { - return (userId: string, unmuteTime: number = null, args: Partial) => { - return unmuteUser(pluginData, userId, unmuteTime, args); - }; - }, + muteUser: mapToPublicFn(muteUser), + unmuteUser: mapToPublicFn(unmuteUser), hasMutedRole(pluginData) { return (member: Member) => { - return member.roles.includes(pluginData.config.get().mute_role); + const muteRole = pluginData.config.get().mute_role; + return muteRole ? member.roles.includes(muteRole) : false; }; }, }, diff --git a/backend/src/plugins/Mutes/commands/ClearMutesCmd.ts b/backend/src/plugins/Mutes/commands/ClearMutesCmd.ts index 028266dc..7a07efb7 100644 --- a/backend/src/plugins/Mutes/commands/ClearMutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/ClearMutesCmd.ts @@ -12,7 +12,7 @@ export const ClearMutesCmd = mutesCmd({ }, async run({ pluginData, message: msg, args }) { - const failed = []; + const failed: string[] = []; for (const id of args.userIds) { const mute = await pluginData.state.mutes.findExistingMuteForUserId(id); if (!mute) { diff --git a/backend/src/plugins/Mutes/commands/MutesCmd.ts b/backend/src/plugins/Mutes/commands/MutesCmd.ts index c2d53436..9636141a 100644 --- a/backend/src/plugins/Mutes/commands/MutesCmd.ts +++ b/backend/src/plugins/Mutes/commands/MutesCmd.ts @@ -4,6 +4,7 @@ import { DBDateFormat, isFullMessage, MINUTES, noop, resolveMember } from "../.. import moment from "moment-timezone"; import { humanizeDurationShort } from "../../../humanizeDurationShort"; import { getBaseUrl } from "../../../pluginUtils"; +import { Member } from "eris"; export const MutesCmd = mutesCmd({ trigger: "mutes", @@ -31,7 +32,7 @@ export const MutesCmd = mutesCmd({ let clearReactionsTimeout; const clearReactionsDebounce = 5 * MINUTES; - let lines = []; + let lines: string[] = []; // Active, logged mutes const activeMutes = await pluginData.state.mutes.getActiveMutes(); @@ -41,13 +42,13 @@ export const MutesCmd = mutesCmd({ if (a.expires_at == null && b.expires_at == null) { return a.created_at > b.created_at ? -1 : 1; } - return a.expires_at > b.expires_at ? 1 : -1; + return a.expires_at! > b.expires_at! ? 1 : -1; }); if (args.manual) { // Show only manual mutes (i.e. "Muted" role added without a logged mute) const muteUserIds = new Set(activeMutes.map(m => m.user_id)); - const manuallyMutedMembers = []; + const manuallyMutedMembers: Member[] = []; const muteRole = pluginData.config.get().mute_role; if (muteRole) { @@ -65,7 +66,7 @@ export const MutesCmd = mutesCmd({ } else { // Show filtered active mutes (but not manual mutes) let filteredMutes: IMuteWithDetails[] = activeMutes; - let bannedIds: string[] = null; + let bannedIds: string[] | null = null; // Filter: mute age if (args.age) { diff --git a/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts b/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts index cc5a4df0..890636a6 100644 --- a/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts +++ b/backend/src/plugins/Mutes/events/ReapplyActiveMuteOnJoinEvt.ts @@ -10,9 +10,11 @@ export const ReapplyActiveMuteOnJoinEvt = mutesEvt("guildMemberAdd", async ({ pl if (mute) { const muteRole = pluginData.config.get().mute_role; - const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`); - await member.addRole(muteRole); - memberRolesLock.unlock(); + if (muteRole) { + const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`); + await member.addRole(muteRole); + memberRolesLock.unlock(); + } pluginData.state.serverLogs.log(LogType.MEMBER_MUTE_REJOIN, { member: stripObjectToScalars(member, ["user", "roles"]), diff --git a/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts b/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts index 86851637..ae5b7e04 100644 --- a/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts +++ b/backend/src/plugins/Mutes/functions/clearExpiredMutes.ts @@ -10,7 +10,10 @@ export async function clearExpiredMutes(pluginData: GuildPluginData, member: Member) { - return member.roles.includes(pluginData.config.get().mute_role); +export function memberHasMutedRole(pluginData: GuildPluginData, member: Member): boolean { + const muteRole = pluginData.config.get().mute_role; + return muteRole ? member.roles.includes(muteRole) : false; } diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts index 168bc198..6daf275d 100644 --- a/backend/src/plugins/Mutes/functions/muteUser.ts +++ b/backend/src/plugins/Mutes/functions/muteUser.ts @@ -9,6 +9,7 @@ import { ucfirst, UserNotificationResult, resolveMember, + UserNotificationMethod, } from "../../../utils"; import { renderTemplate } from "../../../templateFormatter"; import { TextChannel, User } from "eris"; @@ -20,8 +21,8 @@ import { Case } from "../../../data/entities/Case"; export async function muteUser( pluginData: GuildPluginData, userId: string, - muteTime: number = null, - reason: string = null, + muteTime?: number, + reason?: string, muteOptions: MuteOptions = {}, ) { const lock = await pluginData.locks.acquire(`mute-${userId}`); @@ -90,7 +91,7 @@ export async function muteUser( })); if (muteMessage && user instanceof User) { - let contactMethods = []; + let contactMethods: UserNotificationMethod[] = []; if (muteOptions?.contactMethods) { contactMethods = muteOptions.contactMethods; @@ -112,18 +113,21 @@ export async function muteUser( // Create/update a case const casesPlugin = pluginData.getPlugin(CasesPlugin); - let theCase: Case; + let theCase: Case | undefined = + existingMute && existingMute.case_id ? await pluginData.state.cases.find(existingMute.case_id) : undefined; - if (existingMute && existingMute.case_id) { + if (theCase) { // Update old case // Since mutes can often have multiple notes (extraNotes), we won't post each case note individually, // but instead we'll post the entire case afterwards - theCase = await pluginData.state.cases.find(existingMute.case_id); const noteDetails = [`Mute updated to ${muteTime ? timeUntilUnmute : "indefinite"}`]; - const reasons = [reason, ...(muteOptions.caseArgs?.extraNotes || [])]; + const reasons = reason ? [reason] : []; + if (muteOptions.caseArgs?.extraNotes) { + reasons.push(...muteOptions.caseArgs.extraNotes); + } for (const noteReason of reasons) { await casesPlugin.createCaseNote({ - caseId: existingMute.case_id, + caseId: existingMute!.case_id, modId: muteOptions.caseArgs?.modId, body: noteReason, noteDetails, @@ -132,7 +136,7 @@ export async function muteUser( } if (muteOptions.caseArgs?.postInCaseLogOverride !== false) { - casesPlugin.postCaseToCaseLogChannel(existingMute.case_id); + casesPlugin.postCaseToCaseLogChannel(existingMute!.case_id); } } else { // Create new case diff --git a/backend/src/plugins/Mutes/functions/unmuteUser.ts b/backend/src/plugins/Mutes/functions/unmuteUser.ts index a734de6a..ac85a726 100644 --- a/backend/src/plugins/Mutes/functions/unmuteUser.ts +++ b/backend/src/plugins/Mutes/functions/unmuteUser.ts @@ -7,18 +7,20 @@ import humanizeDuration from "humanize-duration"; import { CasesPlugin } from "../../Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; +import { WithRequiredProps } from "../../../utils/typeUtils"; export async function unmuteUser( pluginData: GuildPluginData, userId: string, - unmuteTime: number = null, + unmuteTime?: number, caseArgs: Partial = {}, -): Promise { +): Promise { const existingMute = await pluginData.state.mutes.findExistingMuteForUserId(userId); const user = await resolveUser(pluginData.client, userId); const member = await resolveMember(pluginData.client, pluginData.guild, userId); // Grab the fresh member so we don't have stale role info + const modId = caseArgs.modId || pluginData.client.user.id; - if (!existingMute && !memberHasMutedRole(pluginData, member)) return; + if (!existingMute && member && !memberHasMutedRole(pluginData, member)) return null; if (unmuteTime) { // Schedule timed unmute (= just set the mute's duration) @@ -31,7 +33,7 @@ export async function unmuteUser( // Unmute immediately if (member) { const muteRole = pluginData.config.get().mute_role; - if (member.roles.includes(muteRole)) { + if (muteRole && member.roles.includes(muteRole)) { await member.removeRole(muteRole); } } else { @@ -47,7 +49,7 @@ export async function unmuteUser( const timeUntilUnmute = unmuteTime && humanizeDuration(unmuteTime); // Create a case - const noteDetails = []; + const noteDetails: string[] = []; if (unmuteTime) { noteDetails.push(`Scheduled unmute in ${timeUntilUnmute}`); } else { @@ -61,13 +63,13 @@ export async function unmuteUser( const createdCase = await casesPlugin.createCase({ ...caseArgs, userId, - modId: caseArgs.modId, + modId, type: CaseTypes.Unmute, noteDetails, }); // Log the action - const mod = pluginData.client.users.get(caseArgs.modId); + const mod = pluginData.client.users.get(modId); if (unmuteTime) { pluginData.state.serverLogs.log(LogType.MEMBER_TIMED_UNMUTE, { mod: stripObjectToScalars(mod), diff --git a/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts index 3c6ca433..dc3d451b 100644 --- a/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts +++ b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts @@ -13,6 +13,6 @@ export const MessageCreateEvt = nameHistoryEvt({ event: "messageCreate", async listener(meta) { - meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.message.member)); + meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.message.member!)); }, }); diff --git a/backend/src/plugins/Persist/events/LoadDataEvt.ts b/backend/src/plugins/Persist/events/LoadDataEvt.ts index e89e51be..e88cb333 100644 --- a/backend/src/plugins/Persist/events/LoadDataEvt.ts +++ b/backend/src/plugins/Persist/events/LoadDataEvt.ts @@ -27,10 +27,10 @@ export const LoadDataEvt = persistEvt({ const toRestore: MemberOptions = {}; const config = pluginData.config.getForMember(member); - const restoredData = []; + const restoredData: string[] = []; // Check permissions - const me = pluginData.guild.members.get(pluginData.client.user.id); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; let requiredPermissions = 0; if (config.persist_nicknames) requiredPermissions |= p.manageNicknames; if (config.persisted_roles) requiredPermissions |= p.manageRoles; diff --git a/backend/src/plugins/PingableRoles/utils/getPingableRolesForChannel.ts b/backend/src/plugins/PingableRoles/utils/getPingableRolesForChannel.ts index 5714632b..9b3f5819 100644 --- a/backend/src/plugins/PingableRoles/utils/getPingableRolesForChannel.ts +++ b/backend/src/plugins/PingableRoles/utils/getPingableRolesForChannel.ts @@ -10,5 +10,5 @@ export async function getPingableRolesForChannel( pluginData.state.cache.set(channelId, await pluginData.state.pingableRoles.getForChannel(channelId)); } - return pluginData.state.cache.get(channelId); + return pluginData.state.cache.get(channelId)!; } diff --git a/backend/src/plugins/Post/commands/EditEmbedCmd.ts b/backend/src/plugins/Post/commands/EditEmbedCmd.ts index 9a79bd57..edd1a49c 100644 --- a/backend/src/plugins/Post/commands/EditEmbedCmd.ts +++ b/backend/src/plugins/Post/commands/EditEmbedCmd.ts @@ -31,7 +31,7 @@ export const EditEmbedCmd = postCmd({ const content = args.content || args.maincontent; - let color = null; + let color: number | null = null; if (args.color) { const colorRgb = parseColor(args.color); if (colorRgb) { @@ -42,8 +42,7 @@ export const EditEmbedCmd = postCmd({ } } - const embed: Embed = savedMessage.data.embeds[0] as Embed; - embed.type = "rich"; + const embed: Embed = savedMessage.data.embeds![0] as Embed; if (args.title) embed.title = args.title; if (content) embed.description = formatContent(content); if (color) embed.color = color; diff --git a/backend/src/plugins/Post/commands/PostEmbedCmd.ts b/backend/src/plugins/Post/commands/PostEmbedCmd.ts index 40eac84e..324effdd 100644 --- a/backend/src/plugins/Post/commands/PostEmbedCmd.ts +++ b/backend/src/plugins/Post/commands/PostEmbedCmd.ts @@ -35,7 +35,7 @@ export const PostEmbedCmd = postCmd({ return; } - let color = null; + let color: number | null = null; if (args.color) { const colorRgb = parseColor(args.color); if (colorRgb) { diff --git a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts index 399ca6f0..bc61a4ba 100644 --- a/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts +++ b/backend/src/plugins/Post/commands/ScheduledPostsListCmd.ts @@ -39,7 +39,7 @@ export const ScheduledPostsListCmd = postCmd({ const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); const prettyPostAt = timeAndDate - .inGuildTz(moment.utc(p.post_at, DBDateFormat)) + .inGuildTz(moment.utc(p.post_at!, DBDateFormat)) .format(timeAndDate.getDateFormat("pretty_datetime")); const parts = [`\`#${i++}\` \`[${prettyPostAt}]\` ${previewText}${isTruncated ? "..." : ""}`]; if (p.attachments.length) parts.push("*(with attachment)*"); diff --git a/backend/src/plugins/Post/util/actualPostCmd.ts b/backend/src/plugins/Post/util/actualPostCmd.ts index 16046ffd..5c74aa7f 100644 --- a/backend/src/plugins/Post/util/actualPostCmd.ts +++ b/backend/src/plugins/Post/util/actualPostCmd.ts @@ -19,13 +19,13 @@ export async function actualPostCmd( msg: Message, targetChannel: Channel, content: StrictMessageContent, - opts?: { + opts: { "enable-mentions"?: boolean; schedule?: string; repeat?: number; "repeat-until"?: string; "repeat-times"?: number; - }, + } = {}, ) { if (!(targetChannel instanceof TextChannel)) { msg.channel.createMessage(errorMessage("Channel is not a text channel")); @@ -63,9 +63,9 @@ export async function actualPostCmd( } // For repeated posts, make sure repeat-until or repeat-times is specified - let repeatUntil: moment.Moment = null; - let repeatTimes: number = null; - let repeatDetailsStr: string = null; + let repeatUntil: moment.Moment | null = null; + let repeatTimes: number | null = null; + let repeatDetailsStr: string | null = null; if (opts["repeat-until"]) { repeatUntil = await parseScheduleTime(pluginData, msg.author.id, opts["repeat-until"]); diff --git a/backend/src/plugins/Post/util/parseScheduleTime.ts b/backend/src/plugins/Post/util/parseScheduleTime.ts index e6f7fa9a..26d115f2 100644 --- a/backend/src/plugins/Post/util/parseScheduleTime.ts +++ b/backend/src/plugins/Post/util/parseScheduleTime.ts @@ -8,7 +8,7 @@ export async function parseScheduleTime( pluginData: GuildPluginData, memberId: string, str: string, -): Promise { +): Promise { const tz = await pluginData.getPlugin(TimeAndDatePlugin).getMemberTz(memberId); const dt1 = moment.tz(str, "YYYY-MM-DD HH:mm:ss", tz); diff --git a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts index 423633fd..17c2e067 100644 --- a/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts +++ b/backend/src/plugins/ReactionRoles/commands/InitReactionRolesCmd.ts @@ -111,7 +111,7 @@ export const InitReactionRolesCmd = reactionRolesCmd({ reactionRoles, ); - if (errors.length) { + if (errors?.length) { sendErrorMessage(pluginData, msg.channel, `Errors while adding reaction roles:\n${errors.join("\n")}`); } else { sendSuccessMessage(pluginData, msg.channel, "Reaction roles added"); diff --git a/backend/src/plugins/ReactionRoles/types.ts b/backend/src/plugins/ReactionRoles/types.ts index 9ea9db20..3212a33e 100644 --- a/backend/src/plugins/ReactionRoles/types.ts +++ b/backend/src/plugins/ReactionRoles/types.ts @@ -14,7 +14,7 @@ export type TConfigSchema = t.TypeOf; export type RoleChangeMode = "+" | "-"; export type PendingMemberRoleChanges = { - timeout: NodeJS.Timeout; + timeout: NodeJS.Timeout | null; applyFn: () => void; changes: Array<{ mode: RoleChangeMode; diff --git a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts index e682ff49..2b2dfd0d 100644 --- a/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts +++ b/backend/src/plugins/ReactionRoles/util/addMemberPendingRoleChange.ts @@ -48,7 +48,7 @@ export async function addMemberPendingRoleChange( pluginData.state.pendingRoleChanges.set(memberId, newPendingRoleChangeObj); } - const pendingRoleChangeObj = pluginData.state.pendingRoleChanges.get(memberId); + const pendingRoleChangeObj = pluginData.state.pendingRoleChanges.get(memberId)!; pendingRoleChangeObj.changes.push({ mode, roleId }); if (pendingRoleChangeObj.timeout) clearTimeout(pendingRoleChangeObj.timeout); diff --git a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts index 3d612cc6..7de45269 100644 --- a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts +++ b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts @@ -17,11 +17,11 @@ export async function applyReactionRoleReactionsToMessage( channelId: string, messageId: string, reactionRoles: ReactionRole[], -): Promise { +): Promise { const channel = pluginData.guild.channels.get(channelId) as TextChannel; if (!channel) return; - const errors = []; + const errors: string[] = []; const logs = pluginData.getPlugin(LogsPlugin); let targetMessage; diff --git a/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts index a4bb8f48..4f0f458c 100644 --- a/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts @@ -4,6 +4,7 @@ import { rolesCmd } from "../types"; import { resolveMember, resolveRoleId, stripObjectToScalars, successMessage } from "../../../utils"; import { LogType } from "../../../data/LogType"; import { logger } from "../../../logger"; +import { Member } from "eris"; export const MassAddRoleCmd = rolesCmd({ trigger: "massaddrole", @@ -17,8 +18,8 @@ export const MassAddRoleCmd = rolesCmd({ async run({ message: msg, args, pluginData }) { msg.channel.createMessage(`Resolving members...`); - const members = []; - const unknownMembers = []; + const members: Member[] = []; + const unknownMembers: string[] = []; for (const memberId of args.members) { const member = await resolveMember(pluginData.client, pluginData.guild, memberId); if (member) members.push(member); @@ -55,7 +56,7 @@ export const MassAddRoleCmd = rolesCmd({ const membersWithoutTheRole = members.filter(m => !m.roles.includes(roleId)); let assigned = 0; - const failed = []; + const failed: string[] = []; const alreadyHadRole = members.length - membersWithoutTheRole.length; msg.channel.createMessage( diff --git a/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts index c4009b59..593e4c07 100644 --- a/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts @@ -4,6 +4,7 @@ import { rolesCmd } from "../types"; import { resolveMember, stripObjectToScalars, successMessage, resolveRoleId } from "../../../utils"; import { LogType } from "../../../data/LogType"; import { logger } from "../../../logger"; +import { Member } from "eris"; export const MassRemoveRoleCmd = rolesCmd({ trigger: "massremoverole", @@ -17,8 +18,8 @@ export const MassRemoveRoleCmd = rolesCmd({ async run({ message: msg, args, pluginData }) { msg.channel.createMessage(`Resolving members...`); - const members = []; - const unknownMembers = []; + const members: Member[] = []; + const unknownMembers: string[] = []; for (const memberId of args.members) { const member = await resolveMember(pluginData.client, pluginData.guild, memberId); if (member) members.push(member); @@ -55,7 +56,7 @@ export const MassRemoveRoleCmd = rolesCmd({ const membersWithTheRole = members.filter(m => m.roles.includes(roleId)); let assigned = 0; - const failed = []; + const failed: string[] = []; const didNotHaveRole = members.length - membersWithTheRole.length; msg.channel.createMessage( diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts index 7aa26fa2..2c9a1d0f 100644 --- a/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts +++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleAddCmd.ts @@ -30,7 +30,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({ const hasUnknownRoles = matchedRoleIds.length !== roleNames.length; const rolesToAdd: Map = Array.from(matchedRoleIds.values()) - .map(id => pluginData.guild.roles.get(id)) + .map(id => pluginData.guild.roles.get(id)!) .filter(Boolean) .reduce((map, role) => { map.set(role.id, role); @@ -68,9 +68,9 @@ export const RoleAddCmd = selfGrantableRolesCmd({ rolesToAdd.delete(roleId); if (msg.member.roles.includes(roleId)) { - removed.add(pluginData.guild.roles.get(roleId)); + removed.add(pluginData.guild.roles.get(roleId)!); } else { - skipped.add(pluginData.guild.roles.get(roleId)); + skipped.add(pluginData.guild.roles.get(roleId)!); } } } @@ -94,7 +94,7 @@ export const RoleAddCmd = selfGrantableRolesCmd({ const addedRolesStr = Array.from(rolesToAdd.values()).map(r => (mentionRoles ? `<@&${r.id}>` : `**${r.name}**`)); const addedRolesWord = rolesToAdd.size === 1 ? "role" : "roles"; - const messageParts = []; + const messageParts: string[] = []; messageParts.push(`Granted you the ${addedRolesStr.join(", ")} ${addedRolesWord}`); if (skipped.size || removed.size) { diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleHelpCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleHelpCmd.ts index 5bd35283..02854fad 100644 --- a/backend/src/plugins/SelfGrantableRoles/commands/RoleHelpCmd.ts +++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleHelpCmd.ts @@ -10,7 +10,7 @@ export const RoleHelpCmd = selfGrantableRolesCmd({ const applyingEntries = getApplyingEntries(pluginData, msg); if (applyingEntries.length === 0) return; - const allPrimaryAliases = []; + const allPrimaryAliases: string[] = []; for (const entry of applyingEntries) { for (const aliases of Object.values(entry.roles)) { if (aliases[0]) { diff --git a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts index af0b8bc5..7cabb3c4 100644 --- a/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts +++ b/backend/src/plugins/SelfGrantableRoles/commands/RoleRemoveCmd.ts @@ -26,7 +26,7 @@ export const RoleRemoveCmd = selfGrantableRolesCmd({ const roleNames = normalizeRoleNames(splitRoleNames(args.roleNames)); const matchedRoleIds = findMatchingRoles(roleNames, applyingEntries); - const rolesToRemove = Array.from(matchedRoleIds.values()).map(id => pluginData.guild.roles.get(id)); + const rolesToRemove = Array.from(matchedRoleIds.values()).map(id => pluginData.guild.roles.get(id)!); const roleIdsToRemove = rolesToRemove.map(r => r.id); // Remove the roles diff --git a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts index 5f1a4b15..c650cd8c 100644 --- a/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts +++ b/backend/src/plugins/Slowmode/commands/SlowmodeClearCmd.ts @@ -25,7 +25,7 @@ export const SlowmodeClearCmd = slowmodeCmd({ return; } - const me = pluginData.guild.members.get(pluginData.client.user.id); + const me = pluginData.guild.members.get(pluginData.client.user.id)!; const missingPermissions = getMissingChannelPermissions(me, args.channel, BOT_SLOWMODE_CLEAR_PERMISSIONS); if (missingPermissions) { sendErrorMessage( diff --git a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts index f8abccc8..cd29bc08 100644 --- a/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts +++ b/backend/src/plugins/Slowmode/util/actualDisableSlowmodeCmd.ts @@ -29,7 +29,7 @@ export async function actualDisableSlowmodeCmd(msg: Message, args, pluginData) { const initMsg = await msg.channel.createMessage("Disabling slowmode..."); // Disable bot-maintained slowmode - let failedUsers = []; + let failedUsers: string[] = []; if (botSlowmode) { const result = await disableBotSlowmodeForChannel(pluginData, args.channel); failedUsers = result.failedUsers; diff --git a/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts b/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts index a52b9ab8..f1814922 100644 --- a/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts +++ b/backend/src/plugins/Slowmode/util/disableBotSlowmodeForChannel.ts @@ -12,7 +12,7 @@ export async function disableBotSlowmodeForChannel( // Remove currently applied slowmodes const users = await pluginData.state.slowmodes.getChannelSlowmodeUsers(channel.id); - const failedUsers = []; + const failedUsers: string[] = []; for (const slowmodeUser of users) { try { diff --git a/backend/src/plugins/Slowmode/util/onMessageCreate.ts b/backend/src/plugins/Slowmode/util/onMessageCreate.ts index 1500e614..8e1b8564 100644 --- a/backend/src/plugins/Slowmode/util/onMessageCreate.ts +++ b/backend/src/plugins/Slowmode/util/onMessageCreate.ts @@ -31,7 +31,7 @@ export async function onMessageCreate(pluginData: GuildPluginData= savedMessage.id) return; } } @@ -75,10 +75,11 @@ export async function logAndDetectMessageSpam( const recentActions = getRecentActions(pluginData, type, savedMessage.user_id, savedMessage.channel_id, since); // Start by muting them, if enabled - let muteResult: MuteResult; + let muteResult: MuteResult | null = null; if (spamConfig.mute && member) { const mutesPlugin = pluginData.getPlugin(MutesPlugin); - const muteTime = spamConfig.mute_time ? convertDelayStringToMS(spamConfig.mute_time.toString()) : 120 * 1000; + const muteTime = + (spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000; try { muteResult = await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", { @@ -120,15 +121,17 @@ export async function logAndDetectMessageSpam( // Store the ID of the last handled message const uniqueMessages = Array.from(new Set([...savedMessages, ...additionalMessages])); uniqueMessages.sort((a, b) => (a.id > b.id ? 1 : -1)); - const lastHandledMsgId = uniqueMessages.reduce((last: string, m: SavedMessage): string => { - return !last || m.id > last ? m.id : last; - }, null); + const lastHandledMsgId = uniqueMessages + .map(m => m.id) + .reduce((last, id): string => { + return id > last ? id : last; + }); if (!pluginData.state.lastHandledMsgIds.has(savedMessage.user_id)) { pluginData.state.lastHandledMsgIds.set(savedMessage.user_id, new Map()); } - const channelMap = pluginData.state.lastHandledMsgIds.get(savedMessage.user_id); + const channelMap = pluginData.state.lastHandledMsgIds.get(savedMessage.user_id)!; channelMap.set(savedMessage.channel_id, lastHandledMsgId); // Clear the handled actions from recentActions @@ -150,7 +153,7 @@ export async function logAndDetectMessageSpam( `); casesPlugin.createCaseNote({ caseId: muteResult.case.id, - modId: muteResult.case.mod_id, + modId: muteResult.case.mod_id || "0", body: updateText, automatic: true, }); diff --git a/backend/src/plugins/Spam/util/logAndDetectOtherSpam.ts b/backend/src/plugins/Spam/util/logAndDetectOtherSpam.ts index 471257d4..ee904218 100644 --- a/backend/src/plugins/Spam/util/logAndDetectOtherSpam.ts +++ b/backend/src/plugins/Spam/util/logAndDetectOtherSpam.ts @@ -37,7 +37,8 @@ export async function logAndDetectOtherSpam( if (spamConfig.mute && member) { const mutesPlugin = pluginData.getPlugin(MutesPlugin); - const muteTime = spamConfig.mute_time ? convertDelayStringToMS(spamConfig.mute_time.toString()) : 120 * 1000; + const muteTime = + (spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000; try { await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", { diff --git a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts index 06731eb0..43bdf063 100644 --- a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts +++ b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts @@ -41,7 +41,7 @@ export const StarboardReactionAddEvt = starboardEvt({ .filter(board => board.channel_id !== msg.channel.id) // Matching emoji .filter(board => { - return board.star_emoji.some((boardEmoji: string) => { + return board.star_emoji!.some((boardEmoji: string) => { if (emoji.id) { // Custom emoji const customEmojiMatch = boardEmoji.match(/^?$/); diff --git a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts index d098e72d..bdd9339b 100644 --- a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts +++ b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts @@ -2,7 +2,7 @@ import { GuildPluginData } from "knub"; import { StarboardPluginType, TStarboardOpts } from "../types"; import { Message, GuildChannel, TextChannel, Embed } from "eris"; import moment from "moment-timezone"; -import { EMPTY_CHAR, messageLink } from "../../../utils"; +import { EmbedWith, EMPTY_CHAR, messageLink } from "../../../utils"; import path from "path"; export async function saveMessageToStarboard( @@ -13,7 +13,7 @@ export async function saveMessageToStarboard( const channel = pluginData.guild.channels.get(starboard.channel_id); if (!channel) return; - const embed: Embed = { + const embed: EmbedWith<"footer" | "author" | "fields" | "timestamp"> = { footer: { text: `#${(msg.channel as GuildChannel).name}`, }, @@ -22,7 +22,6 @@ export async function saveMessageToStarboard( }, fields: [], timestamp: new Date(msg.timestamp).toISOString(), - type: "rich", }; if (msg.author.avatarURL) { diff --git a/backend/src/plugins/Tags/TagsPlugin.ts b/backend/src/plugins/Tags/TagsPlugin.ts index 4dc3e32d..83f00116 100644 --- a/backend/src/plugins/Tags/TagsPlugin.ts +++ b/backend/src/plugins/Tags/TagsPlugin.ts @@ -130,7 +130,7 @@ export const TagsPlugin = zeppelinGuildPlugin()("tags", { delay = args[0]; } - const delayMS = convertDelayStringToMS(delay); + const delayMS = convertDelayStringToMS(delay) ?? 0; return moment .utc(reference, "x") .add(delayMS) @@ -151,7 +151,7 @@ export const TagsPlugin = zeppelinGuildPlugin()("tags", { delay = args[0]; } - const delayMS = convertDelayStringToMS(delay); + const delayMS = convertDelayStringToMS(delay) ?? 0; return moment .utc(reference, "x") .subtract(delayMS) diff --git a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts index cd38124d..08d831a5 100644 --- a/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts +++ b/backend/src/plugins/Tags/util/matchAndRenderTagFromString.ts @@ -6,13 +6,23 @@ import { convertDelayStringToMS, StrictMessageContent } from "../../../utils"; import escapeStringRegexp from "escape-string-regexp"; import { Member } from "eris"; -interface Result { +interface BaseResult { renderedContent: StrictMessageContent; tagName: string; - categoryName: string | null; - category: TTagCategory | null; } +type ResultWithCategory = BaseResult & { + categoryName: string; + category: TTagCategory; +}; + +type ResultWithoutCategory = BaseResult & { + categoryName: null; + category: null; +}; + +type Result = ResultWithCategory | ResultWithoutCategory; + export async function matchAndRenderTagFromString( pluginData: GuildPluginData, str: string, @@ -46,6 +56,10 @@ export async function matchAndRenderTagFromString( member, ); + if (renderedContent == null) { + return null; + } + return { renderedContent, tagName, @@ -85,6 +99,11 @@ export async function matchAndRenderTagFromString( dynamicTag.body, member, ); + + if (renderedDynamicTagContent == null) { + return null; + } + return { renderedContent: renderedDynamicTagContent, tagName: dynamicTagName, diff --git a/backend/src/plugins/Tags/util/onMessageCreate.ts b/backend/src/plugins/Tags/util/onMessageCreate.ts index 6e62d89d..52d58164 100644 --- a/backend/src/plugins/Tags/util/onMessageCreate.ts +++ b/backend/src/plugins/Tags/util/onMessageCreate.ts @@ -31,7 +31,7 @@ export async function onMessageCreate(pluginData: GuildPluginData, body: t.TypeOf, - args = [], + args: any[] = [], extraData = {}, subTagPermissionMatchParams?: ExtendedMatchParams, ): Promise { diff --git a/backend/src/plugins/Utility/commands/AboutCmd.ts b/backend/src/plugins/Utility/commands/AboutCmd.ts index 95bcd372..3f1285b4 100644 --- a/backend/src/plugins/Utility/commands/AboutCmd.ts +++ b/backend/src/plugins/Utility/commands/AboutCmd.ts @@ -1,6 +1,6 @@ import { utilityCmd } from "../types"; -import { multiSorter, resolveMember, sorter } from "../../../utils"; -import { GuildChannel, MessageContent } from "eris"; +import { EmbedWith, multiSorter, resolveMember, sorter } from "../../../utils"; +import { GuildChannel, MessageContent, Role } from "eris"; import { getCurrentUptime } from "../../../uptime"; import humanizeDuration from "humanize-duration"; import LCL from "last-commit-log"; @@ -40,7 +40,7 @@ export const AboutCmd = utilityCmd({ version = "?"; } - const shard = pluginData.client.shards.get(pluginData.client.guildShardMap[pluginData.guild.id]); + const shard = pluginData.client.shards.get(pluginData.client.guildShardMap[pluginData.guild.id])!; const lastReload = humanizeDuration(Date.now() - pluginData.state.lastReload, { largest: 2, @@ -59,12 +59,12 @@ export const AboutCmd = utilityCmd({ const loadedPlugins = Array.from( pluginData .getKnubInstance() - .getLoadedGuild(pluginData.guild.id) + .getLoadedGuild(pluginData.guild.id)! .loadedPlugins.keys(), ); loadedPlugins.sort(); - const aboutContent: MessageContent = { + const aboutContent: MessageContent & { embed: EmbedWith<"title" | "fields"> } = { embed: { title: `About ${pluginData.client.user.username}`, fields: [ @@ -101,7 +101,7 @@ export const AboutCmd = utilityCmd({ // For the embed color, find the highest colored role the bot has - this is their color on the server as well const botMember = await resolveMember(pluginData.client, pluginData.guild, pluginData.client.user.id); - let botRoles = botMember.roles.map(r => (msg.channel as GuildChannel).guild.roles.get(r)); + let botRoles = botMember?.roles.map(r => (msg.channel as GuildChannel).guild.roles.get(r)!) || []; botRoles = botRoles.filter(r => !!r); // Drop any unknown roles botRoles = botRoles.filter(r => r.color); // Filter to those with a color botRoles.sort(sorter("position", "DESC")); // Sort by position (highest first) diff --git a/backend/src/plugins/Utility/commands/CleanCmd.ts b/backend/src/plugins/Utility/commands/CleanCmd.ts index 717f0ddc..d2bc4ccf 100644 --- a/backend/src/plugins/Utility/commands/CleanCmd.ts +++ b/backend/src/plugins/Utility/commands/CleanCmd.ts @@ -92,7 +92,7 @@ export const CleanCmd = utilityCmd({ const cleaningMessage = msg.channel.createMessage("Cleaning..."); - const messagesToClean = []; + const messagesToClean: SavedMessage[] = []; let beforeId = msg.id; const timeCutoff = msg.timestamp - MAX_CLEAN_TIME; @@ -104,7 +104,7 @@ export const CleanCmd = utilityCmd({ ); if (potentialMessagesToClean.length === 0) break; - const filtered = []; + const filtered: SavedMessage[] = []; for (const message of potentialMessagesToClean) { const contentString = message.data.content || ""; if (args.user && message.user_id !== args.user) continue; diff --git a/backend/src/plugins/Utility/commands/HelpCmd.ts b/backend/src/plugins/Utility/commands/HelpCmd.ts index 76067c86..8d4af9cc 100644 --- a/backend/src/plugins/Utility/commands/HelpCmd.ts +++ b/backend/src/plugins/Utility/commands/HelpCmd.ts @@ -22,7 +22,7 @@ export const HelpCmd = utilityCmd({ command: PluginCommandDefinition; }> = []; - const guildData = pluginData.getKnubInstance().getLoadedGuild(pluginData.guild.id); + const guildData = pluginData.getKnubInstance().getLoadedGuild(pluginData.guild.id)!; for (const plugin of guildData.loadedPlugins.values()) { const registeredCommands = plugin.pluginData.commands.getAll(); for (const registeredCommand of registeredCommands) { @@ -55,8 +55,8 @@ export const HelpCmd = utilityCmd({ : originalTrigger.source : ""; - const description = command.config.extra.blueprint.description; - const usage = command.config.extra.blueprint.usage; + const description = command.config!.extra!.blueprint.description; + const usage = command.config!.extra!.blueprint.usage; const commandSlug = trigger .trim() .toLowerCase() diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index 2fd2256a..12af56e3 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -32,7 +32,7 @@ export const InfoCmd = utilityCmd({ const channelId = getChannelId(value); const channel = channelId && pluginData.guild.channels.get(channelId); if (channel) { - const embed = await getChannelInfoEmbed(pluginData, channelId, message.author.id); + const embed = await getChannelInfoEmbed(pluginData, channelId!, message.author.id); if (embed) { message.channel.createMessage({ embed }); return; diff --git a/backend/src/plugins/Utility/commands/PingCmd.ts b/backend/src/plugins/Utility/commands/PingCmd.ts index a7d7cc3f..b8304182 100644 --- a/backend/src/plugins/Utility/commands/PingCmd.ts +++ b/backend/src/plugins/Utility/commands/PingCmd.ts @@ -10,9 +10,9 @@ export const PingCmd = utilityCmd({ permission: "can_ping", async run({ message: msg, pluginData }) { - const times = []; + const times: number[] = []; const messages: Message[] = []; - let msgToMsgDelay = null; + let msgToMsgDelay: number | undefined; for (let i = 0; i < 4; i++) { const start = performance.now(); @@ -20,7 +20,7 @@ export const PingCmd = utilityCmd({ times.push(performance.now() - start); messages.push(message); - if (msgToMsgDelay === null) { + if (msgToMsgDelay === undefined) { msgToMsgDelay = message.timestamp - msg.timestamp; } } @@ -29,7 +29,7 @@ export const PingCmd = utilityCmd({ const lowest = Math.round(Math.min(...times)); const mean = Math.round(times.reduce((total, ms) => total + ms, 0) / times.length); - const shard = pluginData.client.shards.get(pluginData.client.guildShardMap[pluginData.guild.id]); + const shard = pluginData.client.shards.get(pluginData.client.guildShardMap[pluginData.guild.id])!; msg.channel.createMessage( trimLines(` @@ -37,7 +37,7 @@ export const PingCmd = utilityCmd({ Lowest: **${lowest}ms** Highest: **${highest}ms** Mean: **${mean}ms** - Time between ping command and first reply: **${msgToMsgDelay}ms** + Time between ping command and first reply: **${msgToMsgDelay!}ms** Shard latency: **${shard.latency}ms** `), ); diff --git a/backend/src/plugins/Utility/commands/RolesCmd.ts b/backend/src/plugins/Utility/commands/RolesCmd.ts index 8ba0b08f..5aa52452 100644 --- a/backend/src/plugins/Utility/commands/RolesCmd.ts +++ b/backend/src/plugins/Utility/commands/RolesCmd.ts @@ -51,8 +51,8 @@ export const RolesCmd = utilityCmd({ if (!sort) sort = "-memberCount"; roles.sort((a, b) => { - if (a._memberCount > b._memberCount) return -1; - if (a._memberCount < b._memberCount) return 1; + if (a._memberCount! > b._memberCount!) return -1; + if (a._memberCount! < b._memberCount!) return 1; return 0; }); } else { diff --git a/backend/src/plugins/Utility/commands/VcmoveCmd.ts b/backend/src/plugins/Utility/commands/VcmoveCmd.ts index bd21b792..af18c41d 100644 --- a/backend/src/plugins/Utility/commands/VcmoveCmd.ts +++ b/backend/src/plugins/Utility/commands/VcmoveCmd.ts @@ -25,6 +25,8 @@ export const VcmoveCmd = utilityCmd({ async run({ message: msg, args, pluginData }) { let channel: VoiceChannel; + const foo = args.member; + if (isSnowflake(args.channel)) { // Snowflake -> resolve channel directly const potentialChannel = pluginData.guild.channels.get(args.channel); @@ -36,7 +38,7 @@ export const VcmoveCmd = utilityCmd({ channel = potentialChannel; } else if (channelMentionRegex.test(args.channel)) { // Channel mention -> parse channel id and resolve channel from that - const channelId = args.channel.match(channelMentionRegex)[1]; + const channelId = args.channel.match(channelMentionRegex)![1]; const potentialChannel = pluginData.guild.channels.get(channelId); if (!potentialChannel || !(potentialChannel instanceof VoiceChannel)) { sendErrorMessage(pluginData, msg.channel, "Unknown or non-voice channel"); diff --git a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts index 95b91d15..1795cf3d 100644 --- a/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getChannelInfoEmbed.ts @@ -3,7 +3,7 @@ import { UtilityPluginType } from "../types"; import { Constants, EmbedOptions } from "eris"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; -import { formatNumber, preEmbedPadding, trimLines } from "../../../utils"; +import { EmbedWith, formatNumber, preEmbedPadding, trimLines } from "../../../utils"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; const TEXT_CHANNEL_ICON = @@ -23,7 +23,7 @@ export async function getChannelInfoEmbed( return null; } - const embed: EmbedOptions = { + const embed: EmbedWith<"fields"> = { fields: [], }; diff --git a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts index 8e2b671d..c25b3a06 100644 --- a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts @@ -1,15 +1,16 @@ import { GuildPluginData } from "knub"; import { UtilityPluginType } from "../types"; -import { BaseInvite, Constants, EmbedOptions, RESTChannelInvite, RESTPrivateInvite } from "eris"; +import { Constants, EmbedOptions } from "eris"; import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { embedPadding, + EmbedWith, emptyEmbedValue, formatNumber, - isRESTGroupDMInvite, - isRESTGuildInvite, + isGroupDMInvite, + isGuildInvite, preEmbedPadding, resolveInvite, trimLines, @@ -24,8 +25,8 @@ export async function getInviteInfoEmbed( return null; } - if (isRESTGuildInvite(invite)) { - const embed: EmbedOptions = { + if (isGuildInvite(invite)) { + const embed: EmbedWith<"fields"> = { fields: [], }; @@ -102,8 +103,8 @@ export async function getInviteInfoEmbed( return embed; } - if (isRESTGroupDMInvite(invite)) { - const embed: EmbedOptions = { + if (isGroupDMInvite(invite)) { + const embed: EmbedWith<"fields"> = { fields: [], }; diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts index b6003026..3c74fc3a 100644 --- a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts @@ -3,7 +3,7 @@ import { UtilityPluginType } from "../types"; import { Constants, EmbedOptions } from "eris"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; -import { chunkMessageLines, messageLink, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils"; +import { chunkMessageLines, EmbedWith, messageLink, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils"; import { getDefaultPrefix } from "knub/dist/commands/commandUtils"; import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; @@ -22,7 +22,7 @@ export async function getMessageInfoEmbed( const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - const embed: EmbedOptions = { + const embed: EmbedWith<"fields"> = { fields: [], }; @@ -93,10 +93,12 @@ export async function getMessageInfoEmbed( }); const authorJoinedAt = message.member && moment.utc(message.member.joinedAt, "x"); - const tzAuthorJoinedAt = requestMemberId - ? await timeAndDate.inMemberTz(requestMemberId, authorJoinedAt) - : timeAndDate.inGuildTz(authorJoinedAt); - const prettyAuthorJoinedAt = tzAuthorJoinedAt.format(timeAndDate.getDateFormat("pretty_datetime")); + const tzAuthorJoinedAt = authorJoinedAt + ? requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, authorJoinedAt) + : timeAndDate.inGuildTz(authorJoinedAt) + : null; + const prettyAuthorJoinedAt = tzAuthorJoinedAt?.format(timeAndDate.getDateFormat("pretty_datetime")); const authorServerAge = message.member && humanizeDuration(Date.now() - message.member.joinedAt, { diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index 6d5e2af5..4486d5f5 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -1,7 +1,17 @@ import { GuildPluginData } from "knub"; import { UtilityPluginType } from "../types"; -import { embedPadding, formatNumber, memoize, MINUTES, preEmbedPadding, resolveUser, trimLines } from "../../../utils"; -import { CategoryChannel, EmbedOptions, Guild, RESTChannelInvite, TextChannel, VoiceChannel } from "eris"; +import { + embedPadding, + EmbedWith, + formatNumber, + memoize, + MINUTES, + preEmbedPadding, + resolveInvite, + resolveUser, + trimLines, +} from "../../../utils"; +import { CategoryChannel, EmbedOptions, Guild, TextChannel, VoiceChannel } from "eris"; import moment from "moment-timezone"; import humanizeDuration from "humanize-duration"; import { getGuildPreview } from "./getGuildPreview"; @@ -11,7 +21,7 @@ export async function getServerInfoEmbed( pluginData: GuildPluginData, serverId: string, requestMemberId?: string, -): Promise { +): Promise { const thisServer = serverId === pluginData.guild.id ? pluginData.guild : null; const [restGuild, guildPreview] = await Promise.all([ thisServer @@ -24,23 +34,23 @@ export async function getServerInfoEmbed( return null; } - const features = (restGuild || guildPreview).features; + const features = (restGuild || guildPreview)!.features; if (!thisServer && !features.includes("DISCOVERABLE")) { return null; } - const embed: EmbedOptions = { + const embed: EmbedWith<"fields"> = { fields: [], }; embed.author = { - name: `Server: ${(guildPreview || restGuild).name}`, - icon_url: (guildPreview || restGuild).iconURL, + name: `Server: ${(guildPreview || restGuild)!.name}`, + icon_url: (guildPreview || restGuild)!.iconURL ?? undefined, }; // BASIC INFORMATION const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); - const createdAt = moment.utc((guildPreview || restGuild).createdAt, "x"); + const createdAt = moment.utc((guildPreview || restGuild)!.createdAt, "x"); const tzCreatedAt = requestMemberId ? await timeAndDate.inMemberTz(requestMemberId, createdAt) : timeAndDate.inGuildTz(createdAt); @@ -50,7 +60,7 @@ export async function getServerInfoEmbed( round: true, }); - const basicInformation = []; + const basicInformation: string[] = []; basicInformation.push(`Created: **${serverAge} ago** (\`${prettyCreatedAt}\`)`); if (thisServer) { @@ -79,16 +89,11 @@ export async function getServerInfoEmbed( thisServer?.members.size || 0; - let onlineMemberCount = guildPreview?.approximatePresenceCount || restGuild?.approximatePresenceCount; + let onlineMemberCount = (guildPreview?.approximatePresenceCount || restGuild?.approximatePresenceCount)!; if (onlineMemberCount == null && restGuild?.vanityURL) { // For servers with a vanity URL, we can also use the numbers from the invite for online count - const invite = (await memoize( - () => pluginData.client.getInvite(restGuild.vanityURL, true), - `getInvite_${restGuild.vanityURL}`, - 10 * MINUTES, - )) as RESTChannelInvite; - + const invite = await resolveInvite(pluginData.client, restGuild.vanityURL!, true); if (invite) { onlineMemberCount = invite.presenceCount; } @@ -140,7 +145,7 @@ export async function getServerInfoEmbed( } // OTHER STATS - const otherStats = []; + const otherStats: string[] = []; if (thisServer) { otherStats.push(`Roles: **${thisServer.roles.size}** / 250`); @@ -156,7 +161,7 @@ export async function getServerInfoEmbed( }[restGuild.premiumTier] || 50; otherStats.push(`Emojis: **${restGuild.emojis.length}** / ${maxEmojis * 2}`); } else { - otherStats.push(`Emojis: **${guildPreview.emojis.length}**`); + otherStats.push(`Emojis: **${guildPreview!.emojis.length}**`); } if (thisServer) { diff --git a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts index 5cfa9b1d..5a9e9dc4 100644 --- a/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getSnowflakeInfoEmbed.ts @@ -1,7 +1,15 @@ import { Message, GuildTextableChannel, EmbedOptions } from "eris"; import { GuildPluginData } from "knub"; import { UtilityPluginType } from "../types"; -import { UnknownUser, trimLines, embedPadding, resolveMember, resolveUser, preEmbedPadding } from "../../../utils"; +import { + UnknownUser, + trimLines, + embedPadding, + resolveMember, + resolveUser, + preEmbedPadding, + EmbedWith, +} from "../../../utils"; import moment from "moment-timezone"; import { CaseTypes } from "../../../data/CaseTypes"; import humanizeDuration from "humanize-duration"; @@ -16,7 +24,7 @@ export async function getSnowflakeInfoEmbed( showUnknownWarning = false, requestMemberId?: string, ): Promise { - const embed: EmbedOptions = { + const embed: EmbedWith<"fields"> = { fields: [], }; diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index 55cb2c0f..c3acdda3 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -1,4 +1,4 @@ -import { Message, GuildTextableChannel, EmbedOptions } from "eris"; +import { Message, GuildTextableChannel, EmbedOptions, Role } from "eris"; import { GuildPluginData } from "knub"; import { UtilityPluginType } from "../types"; import { @@ -10,6 +10,7 @@ import { preEmbedPadding, sorter, messageLink, + EmbedWith, } from "../../../utils"; import moment from "moment-timezone"; import { CaseTypes } from "../../../data/CaseTypes"; @@ -29,7 +30,7 @@ export async function getUserInfoEmbed( const member = await resolveMember(pluginData.client, pluginData.guild, user.id); - const embed: EmbedOptions = { + const embed: EmbedWith<"fields"> = { fields: [], }; @@ -101,7 +102,7 @@ export async function getUserInfoEmbed( largest: 2, round: true, }); - const roles = member.roles.map(id => pluginData.guild.roles.get(id)).filter(r => !!r); + const roles = member.roles.map(id => pluginData.guild.roles.get(id)).filter(r => r != null) as Role[]; roles.sort(sorter("position", "DESC")); embed.fields.push({ diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 311513f7..ca3829dd 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -13,6 +13,7 @@ import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; import { allowTimeout } from "../../RegExpRunner"; import { inputPatternToRegExp, InvalidRegexError } from "../../validatorUtils"; import { asyncFilter } from "../../utils/async"; +import Timeout = NodeJS.Timeout; const SEARCH_RESULTS_PER_PAGE = 15; const SEARCH_ID_RESULTS_PER_PAGE = 50; @@ -47,12 +48,12 @@ export async function displaySearch( msg: Message, ) { // If we're not exporting, load 1 page of search results at a time and allow the user to switch pages with reactions - let originalSearchMsg: Message = null; + let originalSearchMsg: Message; let searching = false; let currentPage = args.page || 1; let hasReactions = false; - let clearReactionsFn = null; - let clearReactionsTimeout = null; + let clearReactionsFn: () => void; + let clearReactionsTimeout: Timeout; const perPage = args.ids ? SEARCH_ID_RESULTS_PER_PAGE : SEARCH_RESULTS_PER_PAGE; @@ -335,7 +336,7 @@ async function performMemberSearch( } } - const [, sortDir, sortBy] = args.sort ? args.sort.match(/^(-?)(.*)$/) : [null, "ASC", "name"]; + const [, sortDir, sortBy] = (args.sort && args.sort.match(/^(-?)(.*)$/)) ?? [null, "ASC", "name"]; const realSortDir = sortDir === "-" ? "DESC" : "ASC"; if (sortBy === "id") { @@ -392,7 +393,7 @@ async function performBanSearch( }); } - const [, sortDir, sortBy] = args.sort ? args.sort.match(/^(-?)(.*)$/) : [null, "ASC", "name"]; + const [, sortDir, sortBy] = (args.sort && args.sort.match(/^(-?)(.*)$/)) ?? [null, "ASC", "name"]; const realSortDir = sortDir === "-" ? "DESC" : "ASC"; if (sortBy === "id") { diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts index 483ec3cb..d441a60c 100644 --- a/backend/src/plugins/ZeppelinPluginBlueprint.ts +++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts @@ -1,4 +1,12 @@ -import { BasePluginType, globalPlugin, GlobalPluginBlueprint, guildPlugin, GuildPluginBlueprint } from "knub"; +import { + BasePluginType, + globalPlugin, + GlobalPluginBlueprint, + GlobalPluginData, + guildPlugin, + GuildPluginBlueprint, + GuildPluginData, +} from "knub"; import * as t from "io-ts"; import { getPluginConfigPreprocessor } from "../pluginUtils"; import { TMarkdown } from "../types"; @@ -7,8 +15,8 @@ import { TMarkdown } from "../types"; * GUILD PLUGINS */ -export interface ZeppelinGuildPluginBlueprint - extends GuildPluginBlueprint { +export interface ZeppelinGuildPluginBlueprint = GuildPluginData> + extends GuildPluginBlueprint { configSchema: t.TypeC; showInDocs?: boolean; @@ -26,13 +34,13 @@ export function zeppelinGuildPlugin(): < - TPartialBlueprint extends Omit, "name"> + TPartialBlueprint extends Omit>, "name"> >( name: string, blueprint: TPartialBlueprint, ) => TPartialBlueprint & { name: string; - configPreprocessor: ZeppelinGuildPluginBlueprint["configPreprocessor"]; + configPreprocessor: ZeppelinGuildPluginBlueprint>["configPreprocessor"]; }; export function zeppelinGuildPlugin(...args) { @@ -52,7 +60,7 @@ export function zeppelinGuildPlugin(...args) { */ export interface ZeppelinGlobalPluginBlueprint - extends GlobalPluginBlueprint { + extends GlobalPluginBlueprint> { configSchema: t.TypeC; } diff --git a/backend/src/regExpRunners.ts b/backend/src/regExpRunners.ts index 2be713e3..0395a11c 100644 --- a/backend/src/regExpRunners.ts +++ b/backend/src/regExpRunners.ts @@ -16,7 +16,7 @@ export function getRegExpRunner(key: string) { }); } - const info = runners.get(key); + const info = runners.get(key)!; info.users++; return info.runner; @@ -27,7 +27,7 @@ export function discardRegExpRunner(key: string) { throw new Error(`No runners with key ${key}, cannot discard`); } - const info = runners.get(key); + const info = runners.get(key)!; info.users--; if (info.users <= 0) { diff --git a/backend/src/templateFormatter.ts b/backend/src/templateFormatter.ts index 1a3d2f3c..1963c816 100644 --- a/backend/src/templateFormatter.ts +++ b/backend/src/templateFormatter.ts @@ -11,11 +11,11 @@ interface ITemplateVar { args: Array; _state: { currentArg: string | ITemplateVar; - currentArgType: "string" | "number" | "var"; + currentArgType: "string" | "number" | "var" | null; inArg: boolean; inQuote: boolean; }; - _parent: ITemplateVar; + _parent: ITemplateVar | null; } function newTemplateVar(): ITemplateVar { @@ -52,8 +52,8 @@ export function parseTemplate(str: string): ParsedTemplate { let inVar = false; let currentString = ""; - let currentVar: ITemplateVar; - let rootVar: ITemplateVar; + let currentVar: ITemplateVar | null = null; + let rootVar: ITemplateVar | null = null; let escapeNext = false; @@ -227,7 +227,7 @@ async function evaluateTemplateVariable(theVar: ITemplateVar, values) { return ""; } - const args = []; + const args: any[] = []; for (const arg of theVar.args) { if (typeof arg === "object") { const argValue = await evaluateTemplateVariable(arg as ITemplateVar, values); @@ -372,7 +372,7 @@ export async function renderTemplate(template: string, values = {}, includeBaseV let parseResult: ParsedTemplate; if (templateCache.has(template)) { - parseResult = templateCache.get(template); + parseResult = templateCache.get(template)!; } else { parseResult = parseTemplate(template); diff --git a/backend/src/types.ts b/backend/src/types.ts index 5e0d8b8a..d9fc3803 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,5 +1,6 @@ import { BaseConfig, Knub } from "knub"; import * as t from "io-ts"; +import { Message } from "eris"; export interface ZeppelinGuildConfig extends BaseConfig { success_emoji?: string; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 582f77aa..fb16742e 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1,9 +1,7 @@ import { - AnyInvite, Attachment, - BaseInvite, - ChannelInvite, Client, + Constants, Embed, EmbedOptions, Emoji, @@ -11,13 +9,13 @@ import { GuildAuditLog, GuildAuditLogEntry, GuildChannel, - GuildInvite, + Invite, + InvitePartialChannel, + InviteWithMetadata, Member, Message, MessageContent, PossiblyUncachedMessage, - RESTChannelInvite, - RESTPrivateInvite, TextableChannel, TextChannel, User, @@ -178,6 +176,17 @@ export const tPartialDictionary = ( return unsafeCoerce(t.record(t.union([domain, t.undefined]), codomain, name)); }; +export function nonNullish(v: V): v is NonNullable { + return v != null; +} + +export type GuildInvite = Invite & { guild: Guild }; +export type GroupDMInvite = Invite & { channel: InvitePartialChannel; type: typeof Constants.ChannelTypes.GROUP_DM }; +export type WithInviteCounts = { + memberCount: number; + presenceCount: number; +}; + /** * Mirrors EmbedOptions from Eris */ @@ -242,6 +251,8 @@ export const tEmbed = t.type({ ), }); +export type EmbedWith = EmbedOptions & Pick, T>; + export type StrictMessageContent = { content?: string; tts?: boolean; disableEveryone?: boolean; embed?: EmbedOptions }; export const tStrictMessageContent = t.type({ @@ -303,7 +314,7 @@ const MAX_DELAY_STRING_AMOUNT = 100 * 365 * DAYS; /** * Turns a "delay string" such as "1h30m" to milliseconds */ -export function convertDelayStringToMS(str, defaultUnit = "m"): number { +export function convertDelayStringToMS(str, defaultUnit = "m"): number | null { const regex = /^([0-9]+)\s*([wdhms])?[a-z]*\s*/; let match; let ms = 0; @@ -419,14 +430,14 @@ export async function findRelevantAuditLogEntry( userId: string, attempts: number = 3, attemptDelay: number = 3000, -): Promise { - if (auditLogNextAttemptAfterFail.has(guild.id) && auditLogNextAttemptAfterFail.get(guild.id) > Date.now()) { +): Promise { + if (auditLogNextAttemptAfterFail.has(guild.id) && auditLogNextAttemptAfterFail.get(guild.id)! > Date.now()) { return null; } - let auditLogs: GuildAuditLog; + let auditLogs: GuildAuditLog | null = null; try { - auditLogs = await guild.getAuditLogs(5, null, actionType); + auditLogs = await guild.getAuditLogs(5, undefined, actionType); } catch (e) { if (isDiscordRESTError(e) && e.code === 50013) { // If we don't have permission to read audit log, set audit log requests on cooldown @@ -480,7 +491,7 @@ export function getUrlsInString(str: string, onlyUnique = false): MatchedURL[] { matches = unique(matches); } - return matches.reduce((urls, match) => { + return matches.reduce((urls, match) => { const withProtocol = protocolRegex.test(match) ? match : `https://${match}`; let matchUrl: MatchedURL; @@ -572,7 +583,7 @@ export function trimEmptyStartEndLines(str: string) { } } - return lines.slice(emptyLinesAtStart, emptyLinesAtEnd ? -1 * emptyLinesAtEnd : null).join("\n"); + return lines.slice(emptyLinesAtStart, emptyLinesAtEnd ? -1 * emptyLinesAtEnd : undefined).join("\n"); } export function trimIndents(str: string, indentLength: number) { @@ -603,7 +614,7 @@ export const channelMentionRegex = /<#([0-9]+)>/g; export function getUserMentions(str: string) { const regex = new RegExp(userMentionRegex.source, "g"); - const userIds = []; + const userIds: string[] = []; let match; // tslint:disable-next-line @@ -616,7 +627,7 @@ export function getUserMentions(str: string) { export function getRoleMentions(str: string) { const regex = new RegExp(roleMentionRegex.source, "g"); - const roleIds = []; + const roleIds: string[] = []; let match; // tslint:disable-next-line @@ -659,7 +670,7 @@ export function useMediaUrls(content: string): string { export function chunkArray(arr: T[], chunkSize): T[][] { const chunks: T[][] = []; - let currentChunk = []; + let currentChunk: T[] = []; for (let i = 0; i < arr.length; i++) { currentChunk.push(arr[i]); @@ -677,7 +688,7 @@ export function chunkLines(str: string, maxChunkLength = 2000): string[] { return [str]; } - const chunks = []; + const chunks: string[] = []; while (str.length) { if (str.length <= maxChunkLength) { @@ -780,11 +791,17 @@ export function downloadFile(attachmentUrl: string, retries = 3): Promise<{ path } type ItemWithRanking = [T, number]; -export function simpleClosestStringMatch(searchStr, haystack: T[], getter = null): T { +export function simpleClosestStringMatch(searchStr: string, haystack: string[]): string | null; +export function simpleClosestStringMatch>( + searchStr, + haystack: T[], + getter: (item: T) => string, +): T | null; +export function simpleClosestStringMatch(searchStr, haystack, getter?) { const normalizedSearchStr = searchStr.toLowerCase(); // See if any haystack item contains a part of the search string - const itemsWithRankings: Array> = haystack.map(item => { + const itemsWithRankings: Array> = haystack.map(item => { const itemStr: string = getter ? getter(item) : item; const normalizedItemStr = itemStr.toLowerCase(); @@ -799,7 +816,7 @@ export function simpleClosestStringMatch(searchStr, haystack: T[], getter = n i += 0.5; } - return [item, i] as ItemWithRanking; + return [item, i] as ItemWithRanking; }); // Sort by best match @@ -872,6 +889,14 @@ export interface UserNotificationResult { text?: string; } +export function createUserNotificationError(text: string): UserNotificationResult { + return { + method: null, + success: false, + text, + }; +} + /** * Attempts to notify the user using one of the specified methods. Only the first one that succeeds will be used. * @param methods List of methods to try, in priority order @@ -885,7 +910,7 @@ export async function notifyUser( return { method: null, success: true }; } - let lastError: Error = null; + let lastError: Error | null = null; for (const method of methods) { if (method.type === "dm") { @@ -928,7 +953,7 @@ export function ucfirst(str) { } export class UnknownUser { - public id: string = null; + public id: string; public username = "Unknown"; public discriminator = "0000"; @@ -1063,13 +1088,13 @@ export async function resolveUser(bot, value) { * Resolves a guild Member from the passed user id, user mention, or full username (with discriminator). * If the member is not found in the cache, it's fetched from the API. */ -export async function resolveMember(bot: Client, guild: Guild, value: string): Promise { +export async function resolveMember(bot: Client, guild: Guild, value: string): Promise { const userId = resolveUserId(bot, value); if (!userId) return null; // If we have the member cached, return that directly if (guild.members.has(userId)) { - return guild.members.get(userId); + return guild.members.get(userId) || null; } // We don't want to spam the API by trying to fetch unknown members again and again, @@ -1123,19 +1148,25 @@ export async function resolveRoleId(bot: Client, guildId: string, value: string) return null; } -const inviteCache = new SimpleCache>(10 * MINUTES, 200); +const inviteCache = new SimpleCache>(10 * MINUTES, 200); -export async function resolveInvite(client: Client, code: string, withCounts?: boolean): Promise { +type ResolveInviteReturnType = Promise<(T extends true ? Invite & WithInviteCounts : Invite) | null>; +export async function resolveInvite( + client: Client, + code: string, + withCounts?: T, +): ResolveInviteReturnType { const key = `${code}:${withCounts ? 1 : 0}`; if (inviteCache.has(key)) { - return inviteCache.get(key); + return inviteCache.get(key) as ResolveInviteReturnType; } + // @ts-ignore: the getInvite() withCounts typings are blergh const promise = client.getInvite(code, withCounts).catch(() => null); inviteCache.set(key, promise); - return promise; + return promise as ResolveInviteReturnType; } export async function confirm(bot: Client, channel: TextableChannel, userId: string, content: MessageContent) { @@ -1222,7 +1253,7 @@ export function memoize(fn: (...args: any[]) => T, key?, time?): T { const realKey = key ?? fn; if (memoizeCache.has(realKey)) { - const memoizedItem = memoizeCache.get(realKey); + const memoizedItem = memoizeCache.get(realKey)!; if (!time || memoizedItem.createdAt > Date.now() - time) { return memoizedItem.value; } @@ -1243,7 +1274,7 @@ type RecursiveRenderFn = (str: string) => string | Promise; export async function renderRecursively(value, fn: RecursiveRenderFn) { if (Array.isArray(value)) { - const result = []; + const result: any[] = []; for (const item of value) { result.push(await renderRecursively(item, fn)); } @@ -1279,6 +1310,8 @@ export function canUseEmoji(client: Client, emoji: string): boolean { } else { throw new Error(`Invalid emoji ${emoji}`); } + + return false; } export function trimPluginDescription(str) { @@ -1292,16 +1325,12 @@ export function isFullMessage(msg: PossiblyUncachedMessage): msg is Message { return (msg as Message).createdAt != null; } -export function isGuildInvite(invite: AnyInvite): invite is GuildInvite { - return (invite as GuildInvite).guild != null; +export function isGuildInvite(invite: Invite): invite is GuildInvite { + return invite.guild != null; } -export function isRESTGuildInvite(invite: BaseInvite): invite is RESTChannelInvite { - return (invite as any).guild != null; -} - -export function isRESTGroupDMInvite(invite: BaseInvite): invite is RESTPrivateInvite { - return (invite as any).guild == null && (invite as any).channel != null; +export function isGroupDMInvite(invite: Invite): invite is GroupDMInvite { + return invite.guild == null && invite.channel?.type === Constants.ChannelTypes.GROUP_DM; } export function asyncMap(arr: T[], fn: (item: T) => Promise): Promise { diff --git a/backend/src/utils/async.ts b/backend/src/utils/async.ts index ee877c1a..5479436a 100644 --- a/backend/src/utils/async.ts +++ b/backend/src/utils/async.ts @@ -26,7 +26,7 @@ export function asyncFilter( arr: T[], callback: (element: T, index: number, array: T[]) => Awaitable, ): Promise { - return asyncReduce( + return asyncReduce( arr, async (newArray, element, i, _arr) => { if (await callback(element, i, _arr)) { @@ -43,7 +43,7 @@ export function asyncMap( arr: T[], callback: (currentValue: T, index: number, array: T[]) => Awaitable, ): Promise { - return asyncReduce( + return asyncReduce( arr, async (newArray, element, i, _arr) => { newArray.push(await callback(element, i, _arr)); diff --git a/backend/src/utils/canAssignRole.ts b/backend/src/utils/canAssignRole.ts index e7d7d514..f42778bd 100644 --- a/backend/src/utils/canAssignRole.ts +++ b/backend/src/utils/canAssignRole.ts @@ -16,13 +16,13 @@ export function canAssignRole(guild: Guild, member: Member, roleId: string) { return false; } - const memberRoles = member.roles.map(_roleId => guild.roles.get(_roleId)); - const highestRoleWithManageRoles: Role = memberRoles.reduce((highest, role) => { + const memberRoles = member.roles.map(_roleId => guild.roles.get(_roleId)!); + const highestRoleWithManageRoles = memberRoles.reduce((highest, role) => { if (!hasDiscordPermissions(role.permissions, Constants.Permissions.manageRoles)) return highest; if (highest == null) return role; if (role.position > highest.position) return role; return highest; }, null); - return highestRoleWithManageRoles.position > targetRole.position; + return highestRoleWithManageRoles && highestRoleWithManageRoles.position > targetRole.position; } diff --git a/backend/src/utils/getPermissionNames.ts b/backend/src/utils/getPermissionNames.ts index fecef3c5..48e057bc 100644 --- a/backend/src/utils/getPermissionNames.ts +++ b/backend/src/utils/getPermissionNames.ts @@ -19,7 +19,7 @@ for (const key in Constants.Permissions) { * @param permissions Bitmask of permissions to get the names for */ export function getPermissionNames(permissions: number | bigint): string[] { - const permissionNames = []; + const permissionNames: string[] = []; for (const [permissionNumber, permissionName] of permissionNumberToName.entries()) { if (BigInt(permissions) & permissionNumber) { permissionNames.push(permissionName); diff --git a/backend/src/utils/sendDM.ts b/backend/src/utils/sendDM.ts index ba830e76..8e8261bf 100644 --- a/backend/src/utils/sendDM.ts +++ b/backend/src/utils/sendDM.ts @@ -1,9 +1,10 @@ import { MessageContent, MessageFile, User } from "eris"; import { createChunkedMessage, HOURS, isDiscordRESTError } from "../utils"; import { logger } from "../logger"; +import Timeout = NodeJS.Timeout; let dmsDisabled = false; -let dmsDisabledTimeout = null; +let dmsDisabledTimeout: Timeout; function disableDMs(duration) { dmsDisabled = true; diff --git a/backend/src/utils/typeUtils.ts b/backend/src/utils/typeUtils.ts index fe327f8e..f206795d 100644 --- a/backend/src/utils/typeUtils.ts +++ b/backend/src/utils/typeUtils.ts @@ -1,2 +1,7 @@ // From https://stackoverflow.com/a/56370310/316944 export type Tail = ((...t: T) => void) extends (h: any, ...r: infer R) => void ? R : never; + +export declare type WithRequiredProps = T & + { + [PK in K]-?: Exclude; + }; diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index 8c1e1615..1db917c0 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -88,12 +88,14 @@ const report = fold((errors: any): StrictValidationError | void => { export function validate(schema: t.Type, value: any): StrictValidationError | null { const validationResult = schema.decode(value); - return pipe( - validationResult, - fold( - err => report(validationResult), - result => null, - ), + return ( + pipe( + validationResult, + fold( + err => report(validationResult), + result => null, + ), + ) || null ); } diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 04db9f5e..d20c0579 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -21,7 +21,10 @@ }, "sourceMap": true, "alwaysStrict": true, - "noImplicitThis": true + "noImplicitThis": true, + "skipLibCheck": true, + "strict": true, + "strictPropertyInitialization": false }, "include": [ "src/**/*.ts"