From d82f5fbc46c26b9293065735a072cf288be233f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Bl=C3=B6meke?= Date: Sat, 9 Nov 2019 00:48:38 +0100 Subject: [PATCH 01/67] Finished Starboard (Pre Override test) --- backend/src/data/GuildStarboardMessages.ts | 54 +++ backend/src/data/GuildStarboardReactions.ts | 43 ++ backend/src/data/entities/Starboard.ts | 23 - backend/src/data/entities/StarboardMessage.ts | 15 +- .../src/data/entities/StarboardReaction.ts | 22 + .../1573248462469-MoveStarboardsToConfig.ts | 103 +++++ ...248794313-CreateStarboardReactionsTable.ts | 44 ++ backend/src/plugins/Starboard.ts | 402 +++++++++--------- backend/src/utils.ts | 2 +- 9 files changed, 464 insertions(+), 244 deletions(-) create mode 100644 backend/src/data/GuildStarboardMessages.ts create mode 100644 backend/src/data/GuildStarboardReactions.ts delete mode 100644 backend/src/data/entities/Starboard.ts create mode 100644 backend/src/data/entities/StarboardReaction.ts create mode 100644 backend/src/migrations/1573248462469-MoveStarboardsToConfig.ts create mode 100644 backend/src/migrations/1573248794313-CreateStarboardReactionsTable.ts diff --git a/backend/src/data/GuildStarboardMessages.ts b/backend/src/data/GuildStarboardMessages.ts new file mode 100644 index 00000000..d98a4414 --- /dev/null +++ b/backend/src/data/GuildStarboardMessages.ts @@ -0,0 +1,54 @@ +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { getRepository, Repository } from "typeorm"; +import { StarboardMessage } from "./entities/StarboardMessage"; + +export class GuildStarboardMessages extends BaseGuildRepository { + private allStarboardMessages: Repository; + + constructor(guildId) { + super(guildId); + this.allStarboardMessages = getRepository(StarboardMessage); + } + + async getStarboardMessagesForMessageId(messageId: string) { + return this.allStarboardMessages + .createQueryBuilder() + .where("guild_id = :gid", { gid: this.guildId }) + .andWhere("message_id = :msgid", { msgid: messageId }) + .getMany(); + } + + async getStarboardMessagesForStarboardMessageId(starboardMessageId: string) { + return this.allStarboardMessages + .createQueryBuilder() + .where("guild_id = :gid", { gid: this.guildId }) + .andWhere("starboard_message_id = :messageId", { messageId: starboardMessageId }) + .getMany(); + } + + async getMessagesForStarboardIdAndSourceMessageId(starboardId: string, sourceMessageId: string) { + return this.allStarboardMessages + .createQueryBuilder() + .where("guild_id = :gid", { gid: this.guildId }) + .andWhere("message_id = :msgId", { msgId: sourceMessageId }) + .andWhere("starboard_channel_id = :sbId", { sbId: starboardId }) + .getMany(); + } + + async createStarboardMessage(starboardId: string, messageId: string, starboardMessageId: string) { + await this.allStarboardMessages.insert({ + message_id: messageId, + starboard_message_id: starboardMessageId, + starboard_channel_id: starboardId, + guild_id: this.guildId, + }); + } + + async deleteStarboardMessage(starboardMessageId: string, starboardChannelId: string) { + await this.allStarboardMessages.delete({ + guild_id: this.guildId, + starboard_message_id: starboardMessageId, + starboard_channel_id: starboardChannelId, + }); + } +} diff --git a/backend/src/data/GuildStarboardReactions.ts b/backend/src/data/GuildStarboardReactions.ts new file mode 100644 index 00000000..6f81f735 --- /dev/null +++ b/backend/src/data/GuildStarboardReactions.ts @@ -0,0 +1,43 @@ +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { Repository, getRepository } from "typeorm"; +import { StarboardReaction } from "./entities/StarboardReaction"; + +export class GuildStarboardReactions extends BaseGuildRepository { + private allStarboardReactions: Repository; + + constructor(guildId) { + super(guildId); + this.allStarboardReactions = getRepository(StarboardReaction); + } + + async getAllReactionsForMessageId(messageId: string) { + return this.allStarboardReactions + .createQueryBuilder() + .where("guild_id = :gid", { gid: this.guildId }) + .andWhere("message_id = :msgid", { msgid: messageId }) + .getMany(); + } + + async createStarboardReaction(messageId: string, reactorId: string) { + await this.allStarboardReactions.insert({ + message_id: messageId, + reactor_id: reactorId, + guild_id: this.guildId, + }); + } + + async deleteAllStarboardReactionsForMessageId(messageId: string) { + await this.allStarboardReactions.delete({ + guild_id: this.guildId, + message_id: messageId, + }); + } + + async deleteStarboardReaction(messageId: string, reactorId: string) { + await this.allStarboardReactions.delete({ + guild_id: this.guildId, + reactor_id: reactorId, + message_id: messageId, + }); + } +} diff --git a/backend/src/data/entities/Starboard.ts b/backend/src/data/entities/Starboard.ts deleted file mode 100644 index af170f48..00000000 --- a/backend/src/data/entities/Starboard.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm"; -import { CaseNote } from "./CaseNote"; -import { StarboardMessage } from "./StarboardMessage"; - -@Entity("starboards") -export class Starboard { - @Column() - @PrimaryColumn() - id: number; - - @Column() guild_id: string; - - @Column() channel_id: string; - - @Column() channel_whitelist: string; - - @Column() emoji: string; - - @Column() reactions_required: number; - - @OneToMany(type => StarboardMessage, msg => msg.starboard) - starboardMessages: StarboardMessage[]; -} diff --git a/backend/src/data/entities/StarboardMessage.ts b/backend/src/data/entities/StarboardMessage.ts index 0b1b37a9..50c9c241 100644 --- a/backend/src/data/entities/StarboardMessage.ts +++ b/backend/src/data/entities/StarboardMessage.ts @@ -1,23 +1,20 @@ import { Entity, Column, PrimaryColumn, OneToMany, ManyToOne, JoinColumn, OneToOne } from "typeorm"; -import { Starboard } from "./Starboard"; -import { Case } from "./Case"; import { SavedMessage } from "./SavedMessage"; @Entity("starboard_messages") export class StarboardMessage { @Column() - @PrimaryColumn() - starboard_id: number; + message_id: string; @Column() @PrimaryColumn() - message_id: string; + starboard_message_id: string; - @Column() starboard_message_id: string; + @Column() + starboard_channel_id: string; - @ManyToOne(type => Starboard, sb => sb.starboardMessages) - @JoinColumn({ name: "starboard_id" }) - starboard: Starboard; + @Column() + guild_id: string; @OneToOne(type => SavedMessage) @JoinColumn({ name: "message_id" }) diff --git a/backend/src/data/entities/StarboardReaction.ts b/backend/src/data/entities/StarboardReaction.ts new file mode 100644 index 00000000..020b3a03 --- /dev/null +++ b/backend/src/data/entities/StarboardReaction.ts @@ -0,0 +1,22 @@ +import { Entity, Column, PrimaryColumn, JoinColumn, OneToOne } from "typeorm"; +import { SavedMessage } from "./SavedMessage"; + +@Entity("starboard_reactions") +export class StarboardReaction { + @Column() + @PrimaryColumn() + id: string; + + @Column() + guild_id: string; + + @Column() + message_id: string; + + @Column() + reactor_id: string; + + @OneToOne(type => SavedMessage) + @JoinColumn({ name: "message_id" }) + message: SavedMessage; +} diff --git a/backend/src/migrations/1573248462469-MoveStarboardsToConfig.ts b/backend/src/migrations/1573248462469-MoveStarboardsToConfig.ts new file mode 100644 index 00000000..39313a0f --- /dev/null +++ b/backend/src/migrations/1573248462469-MoveStarboardsToConfig.ts @@ -0,0 +1,103 @@ +import { MigrationInterface, QueryRunner, Table, TableColumn, createQueryBuilder } from "typeorm"; + +export class MoveStarboardsToConfig1573248462469 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Create the new column for the channels id + const chanid_column = new TableColumn({ + name: "starboard_channel_id", + type: "bigint", + unsigned: true, + }); + await queryRunner.addColumn("starboard_messages", chanid_column); + + // Since we are removing the guild_id with the starboards table, we might want it here + const guid_column = new TableColumn({ + name: "guild_id", + type: "bigint", + unsigned: true, + }); + await queryRunner.addColumn("starboard_messages", guid_column); + + // Migrate the old starboard_id to the new starboard_channel_id + await queryRunner.query(` + UPDATE starboard_messages AS sm + JOIN starboards AS sb + ON sm.starboard_id = sb.id + SET sm.starboard_channel_id = sb.channel_id, sm.guild_id = sb.guild_id; + `); + + // Drop the starboard_id column as it is now obsolete + await queryRunner.dropColumn("starboard_messages", "starboard_id"); + // Set new Primary Key + await queryRunner.dropPrimaryKey("starboard_messages"); + await queryRunner.createPrimaryKey("starboard_messages", ["starboard_message_id"]); + // Finally, drop the starboards channel as it is now obsolete + await queryRunner.dropTable("starboards", true); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("starboard_messages", "starboard_channel_id"); + await queryRunner.dropColumn("starboard_messages", "guild_id"); + + const sbId = new TableColumn({ + name: "starboard_id", + type: "int", + unsigned: true, + }); + await queryRunner.addColumn("starboard_messages", sbId); + + await queryRunner.dropPrimaryKey("starboard_messages"); + await queryRunner.createPrimaryKey("starboard_messages", ["starboard_id", "message_id"]); + + await queryRunner.createTable( + new Table({ + name: "starboards", + columns: [ + { + name: "id", + type: "int", + unsigned: true, + isGenerated: true, + generationStrategy: "increment", + isPrimary: true, + }, + { + name: "guild_id", + type: "bigint", + unsigned: true, + }, + { + name: "channel_id", + type: "bigint", + unsigned: true, + }, + { + name: "channel_whitelist", + type: "text", + isNullable: true, + default: null, + }, + { + name: "emoji", + type: "varchar", + length: "64", + }, + { + name: "reactions_required", + type: "smallint", + unsigned: true, + }, + ], + indices: [ + { + columnNames: ["guild_id", "emoji"], + }, + { + columnNames: ["guild_id", "channel_id"], + isUnique: true, + }, + ], + }), + ); + } +} diff --git a/backend/src/migrations/1573248794313-CreateStarboardReactionsTable.ts b/backend/src/migrations/1573248794313-CreateStarboardReactionsTable.ts new file mode 100644 index 00000000..cd9ec5bc --- /dev/null +++ b/backend/src/migrations/1573248794313-CreateStarboardReactionsTable.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateStarboardReactionsTable1573248794313 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "starboard_reactions", + columns: [ + { + name: "id", + type: "int", + isGenerated: true, + generationStrategy: "increment", + isPrimary: true, + }, + { + name: "guild_id", + type: "bigint", + unsigned: true, + }, + { + name: "message_id", + type: "bigint", + unsigned: true, + }, + { + name: "reactor_id", + type: "bigint", + unsigned: true, + }, + ], + indices: [ + { + columnNames: ["reactor_id", "message_id"], + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("starboard_reactions", true, false, true); + } +} diff --git a/backend/src/plugins/Starboard.ts b/backend/src/plugins/Starboard.ts index 2556acc3..897cecce 100644 --- a/backend/src/plugins/Starboard.ts +++ b/backend/src/plugins/Starboard.ts @@ -1,58 +1,147 @@ -import { decorators as d, waitForReply, utils as knubUtils, IBasePluginConfig, IPluginOptions } from "knub"; -import { ZeppelinPlugin } from "./ZeppelinPlugin"; -import { GuildStarboards } from "../data/GuildStarboards"; +import { decorators as d, IPluginOptions } from "knub"; +import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; import { GuildChannel, Message, TextChannel } from "eris"; -import { - customEmojiRegex, - errorMessage, - getEmojiInString, - getUrlsInString, - noop, - snowflakeRegex, - successMessage, -} from "../utils"; -import { Starboard } from "../data/entities/Starboard"; +import { errorMessage, getUrlsInString, noop, successMessage, tNullable } from "../utils"; import path from "path"; import moment from "moment-timezone"; import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { SavedMessage } from "../data/entities/SavedMessage"; import * as t from "io-ts"; +import { GuildStarboardMessages } from "../data/GuildStarboardMessages"; +import { StarboardMessage } from "../data/entities/StarboardMessage"; +import { GuildStarboardReactions } from "../data/GuildStarboardReactions"; + +const StarboardOpts = t.type({ + source_channel_ids: t.array(t.string), + starboard_channel_id: t.string, + positive_emojis: tNullable(t.array(t.string)), + positive_required: tNullable(t.number), + enabled: tNullable(t.boolean), +}); +type TStarboardOpts = t.TypeOf; const ConfigSchema = t.type({ - can_manage: t.boolean, + entries: t.record(t.string, StarboardOpts), + + can_migrate: t.boolean, }); type TConfigSchema = t.TypeOf; +const defaultStarboardOpts: Partial = { + positive_emojis: ["⭐"], + positive_required: 5, + enabled: true, +}; + export class StarboardPlugin extends ZeppelinPlugin { public static pluginName = "starboard"; public static showInDocs = false; public static configSchema = ConfigSchema; - protected starboards: GuildStarboards; + public static pluginInfo = { + prettyName: "Starboards", + description: trimPluginDescription(` + This plugin contains all functionality needed to use discord channels as starboards. + `), + configurationGuide: trimPluginDescription(` + You can customize multiple settings for starboards. + Any emoji that you want available needs to be put into the config in its raw form. + To obtain a raw form of an emoji, please write out the emoji and put a backslash in front of it. + Example with default emoji: "\:star:" => "⭐" + Example with custom emoji: "\:mrvnSmile:" => "<:mrvnSmile:543000534102310933>" + Now, past the result into the config, but make sure to exclude all less-than and greater-than signs like in the second example. + + + ### Starboard with one source channel + All messages in the source channel that get enough positive reactions will be posted into the starboard channel. + The only positive reaction counted here is the default emoji "⭐". + Only users with a role matching the allowed_roles role-id will be counted. + + ~~~yml + starboard: + config: + entries: + exampleOne: + source_channel_ids: ["604342623569707010"] + starboard_channel_id: "604342689038729226" + positive_emojis: ["⭐"] + positive_required: 5 + allowed_roles: ["556110793058287637"] + enabled: true + ~~~ + + ### Starboard with two sources and two emoji + All messages in any of the source channels that get enough positive reactions will be posted into the starboard channel. + Both the default emoji "⭐" and the custom emoji ":mrvnSmile:543000534102310933" are counted. + + ~~~yml + starboard: + config: + entries: + exampleTwo: + source_channel_ids: ["604342623569707010", "604342649251561487"] + starboard_channel_id: "604342689038729226" + positive_emojis: ["⭐", ":mrvnSmile:543000534102310933"] + positive_required: 10 + enabled: true + ~~~ + `), + }; + protected savedMessages: GuildSavedMessages; + protected starboardMessages: GuildStarboardMessages; + protected starboardReactions: GuildStarboardReactions; private onMessageDeleteFn; public static getStaticDefaultOptions(): IPluginOptions { return { config: { - can_manage: false, + can_migrate: false, + entries: {}, }, overrides: [ { level: ">=100", config: { - can_manage: true, + can_migrate: true, }, }, ], }; } + protected getStarboardOptsForSourceChannel(sourceChannel): TStarboardOpts[] { + const config = this.getConfigForChannel(sourceChannel); + + const configs = Object.values(config.entries).filter(opts => opts.source_channel_ids.includes(sourceChannel.id)); + configs.forEach(cfg => { + if (cfg.enabled == null) cfg.enabled = defaultStarboardOpts.enabled; + if (cfg.positive_emojis == null) cfg.positive_emojis = defaultStarboardOpts.positive_emojis; + if (cfg.positive_required == null) cfg.positive_required = defaultStarboardOpts.positive_required; + }); + + return configs; + } + + protected getStarboardOptsForStarboardChannel(starboardChannel): TStarboardOpts[] { + const config = this.getConfigForChannel(starboardChannel); + + const configs = Object.values(config.entries).filter(opts => opts.starboard_channel_id === starboardChannel.id); + configs.forEach(cfg => { + if (cfg.enabled == null) cfg.enabled = defaultStarboardOpts.enabled; + if (cfg.positive_emojis == null) cfg.positive_emojis = defaultStarboardOpts.positive_emojis; + if (cfg.positive_required == null) cfg.positive_required = defaultStarboardOpts.positive_required; + }); + + return configs; + } + onLoad() { - this.starboards = GuildStarboards.getGuildInstance(this.guildId); this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId); + this.starboardMessages = GuildStarboardMessages.getGuildInstance(this.guildId); + this.starboardReactions = GuildStarboardReactions.getGuildInstance(this.guildId); this.onMessageDeleteFn = this.onMessageDelete.bind(this); this.savedMessages.events.on("delete", this.onMessageDeleteFn); @@ -62,143 +151,13 @@ export class StarboardPlugin extends ZeppelinPlugin { this.savedMessages.events.off("delete", this.onMessageDeleteFn); } - /** - * An interactive setup for creating a starboard - */ - @d.command("starboard create") - @d.permission("can_manage") - async setupCmd(msg: Message) { - const cancelMsg = () => msg.channel.createMessage("Cancelled"); - - msg.channel.createMessage( - `⭐ Let's make a starboard! What channel should we use as the board? ("cancel" to cancel)`, - ); - - let starboardChannel; - do { - const reply = await waitForReply(this.bot, msg.channel as TextChannel, msg.author.id, 60000); - if (reply.content == null || reply.content === "cancel") return cancelMsg(); - - starboardChannel = knubUtils.resolveChannel(this.guild, reply.content || ""); - if (!starboardChannel) { - msg.channel.createMessage("Invalid channel. Try again?"); - continue; - } - - const existingStarboard = await this.starboards.getStarboardByChannelId(starboardChannel.id); - if (existingStarboard) { - msg.channel.createMessage("That channel already has a starboard. Try again?"); - starboardChannel = null; - continue; - } - } while (starboardChannel == null); - - msg.channel.createMessage(`Ok. Which emoji should we use as the trigger? ("cancel" to cancel)`); - - let emoji; - do { - const reply = await waitForReply(this.bot, msg.channel as TextChannel, msg.author.id); - if (reply.content == null || reply.content === "cancel") return cancelMsg(); - - const allEmojis = getEmojiInString(reply.content || ""); - if (!allEmojis.length) { - msg.channel.createMessage("Invalid emoji. Try again?"); - continue; - } - - emoji = allEmojis[0]; - - const customEmojiMatch = emoji.match(customEmojiRegex); - if (customEmojiMatch) { - // <:name:id> to name:id, as Eris puts them in the message reactions object - emoji = `${customEmojiMatch[1]}:${customEmojiMatch[2]}`; - } - } while (emoji == null); - - msg.channel.createMessage( - `And how many reactions are required to immortalize a message in the starboard? ("cancel" to cancel)`, - ); - - let requiredReactions; - do { - const reply = await waitForReply(this.bot, msg.channel as TextChannel, msg.author.id); - if (reply.content == null || reply.content === "cancel") return cancelMsg(); - - requiredReactions = parseInt(reply.content || "", 10); - - if (Number.isNaN(requiredReactions)) { - msg.channel.createMessage("Invalid number. Try again?"); - continue; - } - - if (typeof requiredReactions === "number") { - if (requiredReactions <= 0) { - msg.channel.createMessage("The number must be higher than 0. Try again?"); - continue; - } else if (requiredReactions > 65536) { - msg.channel.createMessage("The number must be smaller than 65536. Try again?"); - continue; - } - } - } while (requiredReactions == null); - - msg.channel.createMessage( - `And finally, which channels can messages be starred in? "All" for any channel. ("cancel" to cancel)`, - ); - - let channelWhitelist; - do { - const reply = await waitForReply(this.bot, msg.channel as TextChannel, msg.author.id); - if (reply.content == null || reply.content === "cancel") return cancelMsg(); - - if (reply.content.toLowerCase() === "all") { - channelWhitelist = null; - break; - } - - channelWhitelist = reply.content.match(new RegExp(snowflakeRegex, "g")); - - let hasInvalidChannels = false; - for (const id of channelWhitelist) { - const channel = this.guild.channels.get(id); - if (!channel || !(channel instanceof TextChannel)) { - msg.channel.createMessage(`Couldn't recognize channel <#${id}> (\`${id}\`). Try again?`); - hasInvalidChannels = true; - break; - } - } - if (hasInvalidChannels) continue; - } while (channelWhitelist == null); - - await this.starboards.create(starboardChannel.id, channelWhitelist, emoji, requiredReactions); - - msg.channel.createMessage(successMessage("Starboard created!")); - } - - /** - * Deletes the starboard from the specified channel. The already-posted starboard messages are retained. - */ - @d.command("starboard delete", "") - @d.permission("can_manage") - async deleteCmd(msg: Message, args: { channelId: string }) { - const starboard = await this.starboards.getStarboardByChannelId(args.channelId); - if (!starboard) { - msg.channel.createMessage(errorMessage(`Channel <#${args.channelId}> doesn't have a starboard!`)); - return; - } - - await this.starboards.delete(starboard.channel_id); - - msg.channel.createMessage(successMessage(`Starboard deleted from <#${args.channelId}>!`)); - } - /** * When a reaction is added to a message, check if there are any applicable starboards and if the reactions reach * the required threshold. If they do, post the message in the starboard channel. */ @d.event("messageReactionAdd") @d.lock("starboardReaction") - async onMessageReactionAdd(msg: Message, emoji: { id: string; name: string }) { + async onMessageReactionAdd(msg: Message, emoji: { id: string; name: string }, userId: string) { if (!msg.author) { // Message is not cached, fetch it try { @@ -209,52 +168,46 @@ export class StarboardPlugin extends ZeppelinPlugin { } } - const emojiStr = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name; - const applicableStarboards = await this.starboards.getStarboardsByEmoji(emojiStr); + const applicableStarboards = await this.getStarboardOptsForSourceChannel(msg.channel); for (const starboard of applicableStarboards) { + // Instantly continue if the starboard is disabled + if (!starboard.enabled) continue; // Can't star messages in the starboard channel itself - if (msg.channel.id === starboard.channel_id) continue; - - if (starboard.channel_whitelist) { - const allowedChannelIds = starboard.channel_whitelist.split(","); - if (!allowedChannelIds.includes(msg.channel.id)) continue; - } - + if (msg.channel.id === starboard.starboard_channel_id) continue; + // Move reaction into DB at this point + await this.starboardReactions.createStarboardReaction(msg.id, userId).catch(); // If the message has already been posted to this starboard, we don't need to do anything else here - const existingSavedMessage = await this.starboards.getStarboardMessageByStarboardIdAndMessageId( - starboard.id, + const starboardMessages = await this.starboardMessages.getMessagesForStarboardIdAndSourceMessageId( + starboard.starboard_channel_id, msg.id, ); - if (existingSavedMessage) return; + if (starboardMessages.length > 0) continue; - const reactionsCount = await this.countReactions(msg, emojiStr); - - if (reactionsCount >= starboard.reactions_required) { - await this.saveMessageToStarboard(msg, starboard); + const reactions = await this.starboardReactions.getAllReactionsForMessageId(msg.id); + const reactionsCount = reactions.length; + if (reactionsCount >= starboard.positive_required) { + await this.saveMessageToStarboard(msg, starboard.starboard_channel_id); } } } - /** - * Counts the specific reactions in the message, ignoring the message author - */ - async countReactions(msg: Message, reaction) { - let reactionsCount = (msg.reactions[reaction] && msg.reactions[reaction].count) || 0; + @d.event("messageReactionRemove") + async onStarboardReactionRemove(msg: Message, emoji: { id: string; name: string }, userId: string) { + await this.starboardReactions.deleteStarboardReaction(msg.id, userId); + } - // Ignore self-stars - const reactors = await msg.getReaction(reaction); - if (reactors.some(u => u.id === msg.author.id)) reactionsCount--; - - return reactionsCount; + @d.event("messageReactionRemoveAll") + async onMessageReactionRemoveAll(msg: Message) { + await this.starboardReactions.deleteAllStarboardReactionsForMessageId(msg.id); } /** * Saves/posts a message to the specified starboard. The message is posted as an embed and image attachments are * included as the embed image. */ - async saveMessageToStarboard(msg: Message, starboard: Starboard) { - const channel = this.guild.channels.get(starboard.channel_id); + async saveMessageToStarboard(msg: Message, starboardChannelId: string) { + const channel = this.guild.channels.get(starboardChannelId); if (!channel) return; const time = moment(msg.timestamp, "x").format("YYYY-MM-DD [at] HH:mm:ss [UTC]"); @@ -308,18 +261,18 @@ export class StarboardPlugin extends ZeppelinPlugin { content: `https://discordapp.com/channels/${this.guildId}/${msg.channel.id}/${msg.id}`, embed, }); - await this.starboards.createStarboardMessage(starboard.id, msg.id, starboardMessage.id); + await this.starboardMessages.createStarboardMessage(channel.id, msg.id, starboardMessage.id); } /** * Remove a message from the specified starboard */ - async removeMessageFromStarboard(msgId: string, starboard: Starboard) { - const starboardMessage = await this.starboards.getStarboardMessageByStarboardIdAndMessageId(starboard.id, msgId); - if (!starboardMessage) return; + async removeMessageFromStarboard(msg: StarboardMessage) { + await this.bot.deleteMessage(msg.starboard_channel_id, msg.starboard_message_id).catch(noop); + } - await this.bot.deleteMessage(starboard.channel_id, starboardMessage.starboard_message_id).catch(noop); - await this.starboards.deleteStarboardMessage(starboard.id, msgId); + async removeMessageFromStarboardMessages(starboard_message_id: string, starboard_channel_id: string) { + await this.starboardMessages.deleteStarboardMessage(starboard_message_id, starboard_channel_id); } /** @@ -328,44 +281,71 @@ export class StarboardPlugin extends ZeppelinPlugin { * TODO: When a message is removed from the starboard itself, i.e. the bot's embed is removed, also remove that message from the starboard_messages database table */ async onMessageDelete(msg: SavedMessage) { - const starboardMessages = await this.starboards.with("starboard").getStarboardMessagesByMessageId(msg.id); - if (!starboardMessages.length) return; + let messages = await this.starboardMessages.getStarboardMessagesForMessageId(msg.id); + if (messages.length > 0) { + for (const starboardMessage of messages) { + if (!starboardMessage.starboard_message_id) continue; + this.removeMessageFromStarboard(starboardMessage).catch(noop); + } + } else { + messages = await this.starboardMessages.getStarboardMessagesForStarboardMessageId(msg.id); + if (messages.length === 0) return; - for (const starboardMessage of starboardMessages) { - if (!starboardMessage.starboard) continue; - this.removeMessageFromStarboard(starboardMessage.message_id, starboardMessage.starboard); + for (const starboardMessage of messages) { + if (!starboardMessage.starboard_channel_id) continue; + this.removeMessageFromStarboardMessages( + starboardMessage.starboard_message_id, + starboardMessage.starboard_channel_id, + ).catch(noop); + } } } - @d.command("starboard migrate_pins", " ") + @d.command("starboard migrate_pins", " ", { + extra: { + info: { + description: + "Migrates all of a channels pins to starboard messages, posting them in the starboard channel. The old pins are not unpinned.", + }, + }, + }) + @d.permission("can_migrate") async migratePinsCmd(msg: Message, args: { pinChannelId: string; starboardChannelId }) { - const starboard = await this.starboards.getStarboardByChannelId(args.starboardChannelId); - if (!starboard) { - msg.channel.createMessage(errorMessage("The specified channel doesn't have a starboard!")); - return; - } + try { + const starboards = await this.getStarboardOptsForStarboardChannel(this.bot.getChannel(args.starboardChannelId)); + if (!starboards) { + msg.channel.createMessage(errorMessage("The specified channel doesn't have a starboard!")).catch(noop); + return; + } - const channel = (await this.guild.channels.get(args.pinChannelId)) as GuildChannel & TextChannel; - if (!channel) { - msg.channel.createMessage(errorMessage("Could not find the specified channel to migrate pins from!")); - return; - } + const channel = (await this.guild.channels.get(args.pinChannelId)) as GuildChannel & TextChannel; + if (!channel) { + msg.channel + .createMessage(errorMessage("Could not find the specified channel to migrate pins from!")) + .catch(noop); + return; + } - msg.channel.createMessage(`Migrating pins from <#${channel.id}> to <#${args.starboardChannelId}>...`); + msg.channel.createMessage(`Migrating pins from <#${channel.id}> to <#${args.starboardChannelId}>...`).catch(noop); - const pins = await channel.getPins(); - pins.reverse(); // Migrate pins starting from the oldest message + const pins = await channel.getPins(); + pins.reverse(); // Migrate pins starting from the oldest message - for (const pin of pins) { - const existingStarboardMessage = await this.starboards.getStarboardMessageByStarboardIdAndMessageId( - starboard.id, - pin.id, + for (const pin of pins) { + const existingStarboardMessage = await this.starboardMessages.getMessagesForStarboardIdAndSourceMessageId( + args.starboardChannelId, + pin.id, + ); + if (existingStarboardMessage.length > 0) continue; + await this.saveMessageToStarboard(pin, args.starboardChannelId); + } + + msg.channel.createMessage(successMessage("Pins migrated!")).catch(noop); + } catch (error) { + this.sendErrorMessage( + msg.channel, + "Sorry, but something went wrong!\nSyntax: `starboard migrate_pins `", ); - if (existingStarboardMessage) continue; - - await this.saveMessageToStarboard(pin, starboard); } - - msg.channel.createMessage(successMessage("Pins migrated!")); } } diff --git a/backend/src/utils.ts b/backend/src/utils.ts index dee6b8d9..b04d25e3 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -189,7 +189,7 @@ const urlRegex = /(\S+\.\S+)/g; const protocolRegex = /^[a-z]+:\/\//; export function getUrlsInString(str: string, unique = false): url.URL[] { - let matches = (str.match(urlRegex) || []).map(m => m[0]); + let matches = str.match(urlRegex) || []; if (unique) matches = Array.from(new Set(matches)); return matches.reduce((urls, match) => { From cec2c74eaa6d4480b23c7921546e2180ed67304e Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 20:30:36 +0200 Subject: [PATCH 02/67] Disable unneeded tslint warning --- backend/src/api/guilds.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/api/guilds.ts b/backend/src/api/guilds.ts index de5d9597..7ba01ca5 100644 --- a/backend/src/api/guilds.ts +++ b/backend/src/api/guilds.ts @@ -53,6 +53,7 @@ export function initGuildsAPI(app: express.Express) { return res.status(400).json({ errors: [e.message] }); } + // tslint:disable-next-line:no-console console.error("Error when loading YAML: " + e.message); return serverError(res, "Server error"); } From 682d8e91539baaa7d382f515e47e19c40737bc35 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 20:30:50 +0200 Subject: [PATCH 03/67] Don't apply multiple automod rules to the same message --- backend/src/plugins/Automod.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index cddc2257..e8982687 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -1311,6 +1311,7 @@ export class AutomodPlugin extends ZeppelinPlugin { const matchResult = await this.matchRuleToMessage(rule, msg); if (matchResult) { await this.applyActionsOnMatch(rule, matchResult); + break; // Don't apply multiple rules to the same message } } }); From 9164bcd04512dd0f92a3cbde5d6e8f984578ae8f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 20:41:45 +0200 Subject: [PATCH 04/67] Fix URL matching in automod, censor, and spam plugin --- backend/src/plugins/Automod.ts | 1 + backend/src/utils.test.ts | 21 +++++++++++++++++++++ backend/src/utils.ts | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 backend/src/utils.test.ts diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index e8982687..1ddac0a8 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -640,6 +640,7 @@ export class AutomodPlugin extends ZeppelinPlugin { protected evaluateMatchLinksTrigger(trigger: TMatchLinksTrigger, str: string): boolean { const links = getUrlsInString(str, true); + for (const link of links) { const normalizedHostname = link.hostname.toLowerCase(); diff --git a/backend/src/utils.test.ts b/backend/src/utils.test.ts new file mode 100644 index 00000000..cc0eca24 --- /dev/null +++ b/backend/src/utils.test.ts @@ -0,0 +1,21 @@ +import { getUrlsInString } from "./utils"; + +import test from "ava"; + +test("Detects full links", t => { + const urls = getUrlsInString("foo https://google.com/ bar"); + t.is(urls.length, 1); + t.is(urls[0].hostname, "google.com"); +}); + +test("Detects partial links", t => { + const urls = getUrlsInString("foo google.com bar"); + t.is(urls.length, 1); + t.is(urls[0].hostname, "google.com"); +}); + +test("Detects subdomains", t => { + const urls = getUrlsInString("foo photos.google.com bar"); + t.is(urls.length, 1); + t.is(urls[0].hostname, "photos.google.com"); +}); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index dee6b8d9..b04d25e3 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -189,7 +189,7 @@ const urlRegex = /(\S+\.\S+)/g; const protocolRegex = /^[a-z]+:\/\//; export function getUrlsInString(str: string, unique = false): url.URL[] { - let matches = (str.match(urlRegex) || []).map(m => m[0]); + let matches = str.match(urlRegex) || []; if (unique) matches = Array.from(new Set(matches)); return matches.reduce((urls, match) => { From 02e53aa35848ec8c9c197a6c5d74af92dc7a53c5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:19:18 +0200 Subject: [PATCH 05/67] nodemon: set watch delay of 300 to prevent mass-restarts on pulls/refactors --- backend/nodemon-api.json | 1 + backend/nodemon-bot.json | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/nodemon-api.json b/backend/nodemon-api.json index 759b4e97..8f3d7153 100644 --- a/backend/nodemon-api.json +++ b/backend/nodemon-api.json @@ -1,6 +1,7 @@ { "watch": ["src", "../shared/src"], "ignore": ["src/migrations/*"], + "delay": 300, "ext": "ts", "exec": "node -r ts-node/register -r tsconfig-paths/register ./src/api/index.ts" } diff --git a/backend/nodemon-bot.json b/backend/nodemon-bot.json index 773cda65..fdfb19e9 100644 --- a/backend/nodemon-bot.json +++ b/backend/nodemon-bot.json @@ -1,6 +1,7 @@ { "watch": ["src", "../shared/src"], "ignore": ["src/api/*", "src/migrations/*"], + "delay": 300, "ext": "ts", "exec": "node -r ts-node/register -r tsconfig-paths/register ./src/index.ts" } From 00e047f70150744b451f52cb0a066cbb6a31a55d Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 21:43:39 +0200 Subject: [PATCH 06/67] Change target in tsconfig from esnext to es2018 to support optional chaining/nullish coalescing on Node.js 12 --- backend/tsconfig.json | 3 +-- dashboard/tsconfig.json | 2 +- shared/tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 2b555b99..1bc9478e 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -6,9 +6,8 @@ "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "target": "esnext", + "target": "es2018", "lib": [ - "es2017", "esnext" ], "baseUrl": ".", diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index bc0389ac..42745a2a 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "module": "esnext", - "target": "esnext", + "target": "es2018", "sourceMap": true, "noImplicitAny": false, "allowSyntheticDefaultImports": true, diff --git a/shared/tsconfig.json b/shared/tsconfig.json index d4ca702c..a9c70fd7 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -6,7 +6,7 @@ "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "target": "esnext", + "target": "es2018", "lib": [ "esnext" ], From 969e06e00e8ffbadb6843ddcecd48a8299e8990b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:02:11 +0200 Subject: [PATCH 07/67] utils: add messageLink() and isValidEmbed() --- backend/src/utils.ts | 92 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index b04d25e3..bb7599ab 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -9,6 +9,7 @@ import { GuildAuditLogEntry, GuildChannel, Member, + Message, MessageContent, TextableChannel, TextChannel, @@ -27,6 +28,7 @@ import https from "https"; import tmp from "tmp"; import { logger, waitForReaction } from "knub"; import { SavedMessage } from "./data/entities/SavedMessage"; +import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; const delayStringMultipliers = { w: 1000 * 60 * 60 * 24 * 7, @@ -42,10 +44,74 @@ export const MINUTES = 60 * SECONDS; export const HOURS = 60 * MINUTES; export const DAYS = 24 * HOURS; -export function tNullable>(type: T) { +export function tNullable>(type: T) { return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); } +/** + * Mirrors EmbedOptions from Eris + */ +export const tEmbed = t.type({ + title: tNullable(t.string), + description: tNullable(t.string), + url: tNullable(t.string), + timestamp: tNullable(t.string), + color: tNullable(t.number), + footer: tNullable( + t.type({ + text: t.string, + icon_url: tNullable(t.string), + proxy_icon_url: tNullable(t.string), + }), + ), + image: tNullable( + t.type({ + url: tNullable(t.string), + proxy_url: tNullable(t.string), + width: tNullable(t.number), + height: tNullable(t.number), + }), + ), + thumbnail: tNullable( + t.type({ + url: tNullable(t.string), + proxy_url: tNullable(t.string), + width: tNullable(t.number), + height: tNullable(t.number), + }), + ), + video: tNullable( + t.type({ + url: tNullable(t.string), + width: tNullable(t.number), + height: tNullable(t.number), + }), + ), + provider: tNullable( + t.type({ + name: t.string, + url: tNullable(t.string), + }), + ), + fields: tNullable( + t.array( + t.type({ + name: tNullable(t.string), + value: tNullable(t.string), + inline: tNullable(t.boolean), + }), + ), + ), + author: tNullable( + t.type({ + name: t.string, + url: tNullable(t.string), + width: tNullable(t.number), + height: tNullable(t.number), + }), + ), +}); + export function dropPropertiesByName(obj, propName) { if (obj.hasOwnProperty(propName)) delete obj[propName]; for (const value of Object.values(obj)) { @@ -805,3 +871,27 @@ export function verboseUserName(user: User | UnknownUser): string { export function verboseChannelMention(channel: GuildChannel): string { return `<#${channel.id}> (**#${channel.name}**, \`${channel.id}\`)`; } + +export function messageLink(message: Message): string; +export function messageLink(guildIdOrMessage: string | Message | null, channelId?: string, messageId?: string): string { + let guildId; + if (guildIdOrMessage == null) { + // Full arguments without a guild id -> DM/Group chat + guildId = "@me"; + } else if (guildIdOrMessage instanceof Message) { + // Message object as the only argument + guildId = (guildIdOrMessage.channel as GuildChannel).guild?.id ?? "@me"; + channelId = guildIdOrMessage.channel.id; + messageId = guildIdOrMessage.id; + } else { + // Full arguments with all IDs + guildId = guildIdOrMessage; + } + + return `https://discordapp.com/channels/${guildId}/${channelId}/${messageId}`; +} + +export function isValidEmbed(embed: any): boolean { + const result = decodeAndValidateStrict(tEmbed, embed); + return !(result instanceof StrictValidationError); +} From f3af8360207fac7e2ce47489a48d47839e1c8c66 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:02:37 +0200 Subject: [PATCH 08/67] Update prettier to 1.19.1 for optional chaining/nullish coalescing support --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f240539a..936e5bc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1218,9 +1218,9 @@ } }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "pump": { diff --git a/package.json b/package.json index 7a683076..d63f5b91 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "devDependencies": { "husky": "^3.0.9", "lint-staged": "^9.4.2", - "prettier": "^1.16.4", + "prettier": "^1.19.1", "tslint": "^5.13.1", "tslint-config-prettier": "^1.18.0", "typescript": "^3.7.2" From cbcd2bd67df2c184e6d46a5ccd9d47070eb98d78 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:03:10 +0200 Subject: [PATCH 09/67] Mark StrictValidationError errors property as readonly --- backend/src/validatorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index a200566e..1495d8b5 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -57,7 +57,7 @@ function getContextPath(context) { // tslint:enable export class StrictValidationError extends Error { - private errors; + private readonly errors; constructor(errors: string[]) { errors = Array.from(new Set(errors)); From 279a8fe7aeae9d15e726d23fb59e698f465879a3 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 27 Nov 2019 22:04:00 +0200 Subject: [PATCH 10/67] post: use content as raw embed source in !post_embed with --raw/-r switch --- backend/src/plugins/Post.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Post.ts b/backend/src/plugins/Post.ts index 6717c084..348e6f33 100644 --- a/backend/src/plugins/Post.ts +++ b/backend/src/plugins/Post.ts @@ -13,6 +13,7 @@ import { deactivateMentions, createChunkedMessage, stripObjectToScalars, + isValidEmbed, } from "../utils"; import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { ZeppelinPlugin } from "./ZeppelinPlugin"; @@ -278,6 +279,7 @@ export class PostPlugin extends ZeppelinPlugin { { name: "content", type: "string" }, { name: "color", type: "string" }, { name: "schedule", type: "string" }, + { name: "raw", isSwitch: true, shortcut: "r" }, ], }) @d.permission("can_post") @@ -290,6 +292,7 @@ export class PostPlugin extends ZeppelinPlugin { content?: string; color?: string; schedule?: string; + raw?: boolean; }, ) { if (!(args.channel instanceof TextChannel)) { @@ -315,11 +318,31 @@ export class PostPlugin extends ZeppelinPlugin { color = parseInt(colorMatch[1], 16); } - const embed: EmbedBase = {}; + let embed: EmbedBase = {}; if (args.title) embed.title = args.title; - if (content) embed.description = this.formatContent(content); if (color) embed.color = color; + if (content) { + if (args.raw) { + let parsed; + try { + parsed = JSON.parse(content); + } catch (e) { + this.sendErrorMessage(msg.channel, "Syntax error in embed JSON"); + return; + } + + if (!isValidEmbed(parsed)) { + this.sendErrorMessage(msg.channel, "Embed is not valid"); + return; + } + + embed = Object.assign({}, embed, parsed); + } else { + embed.description = this.formatContent(content); + } + } + if (args.schedule) { // Schedule the post to be posted later const postAt = this.parseScheduleTime(args.schedule); From ba2873a29a9899082014489f9abab502b57f06db Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 28 Nov 2019 02:34:41 +0200 Subject: [PATCH 11/67] Run a loose pre-check before preprocessStaticConfig This loose pre-check checks the config schema by treating every object as partial. This means that if a property exists, it's guaranteed to be the correct type (e.g. object). However, there's no guarantee that all or any properties exist. This allows preprocessStaticConfig implementations to be much less defensive and thus reduce boilerplate. --- backend/src/plugins/ZeppelinPlugin.ts | 10 ++++- backend/src/utils.ts | 63 +++++++++++++++++++++++++++ backend/src/validation.test.ts | 41 +++++++++++++++++ backend/src/validatorUtils.ts | 11 +++++ 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 backend/src/validation.test.ts diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index 3bf8e340..f2cc6481 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -12,6 +12,7 @@ import { resolveMember, resolveUser, resolveUserId, + tDeepPartial, trimEmptyStartEndLines, trimIndents, UnknownUser, @@ -19,7 +20,7 @@ import { import { Invite, Member, User } from "eris"; import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line import { performance } from "perf_hooks"; -import { decodeAndValidateStrict, StrictValidationError } from "../validatorUtils"; +import { decodeAndValidateStrict, StrictValidationError, validate } from "../validatorUtils"; import { SimpleCache } from "../SimpleCache"; const SLOW_RESOLVE_THRESHOLD = 1500; @@ -121,6 +122,13 @@ export class ZeppelinPlugin extends Plug ? options.overrides : (defaultOptions.overrides || []).concat(options.overrides || []); + // Before preprocessing the static config, do a loose check by checking the schema as deeply partial. + // This way the preprocessing function can trust that if a property exists, its value will be the correct (partial) type. + const initialLooseCheck = this.configSchema ? validate(tDeepPartial(this.configSchema), mergedConfig) : null; + if (initialLooseCheck) { + throw initialLooseCheck; + } + mergedConfig = this.preprocessStaticConfig(mergedConfig); const decodedConfig = this.configSchema ? decodeAndValidateStrict(this.configSchema, mergedConfig) : mergedConfig; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index bb7599ab..e5e86863 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -48,6 +48,69 @@ export function tNullable>(type: T) { return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); } +function typeHasProps(type: any): type is t.TypeC { + return type.props != null; +} + +function typeIsArray(type: any): type is t.ArrayC { + return type._tag === "ArrayType"; +} + +export type TDeepPartial = T extends t.InterfaceType + ? TDeepPartialProps + : T extends t.DictionaryType + ? t.DictionaryType> + : T extends t.UnionType + ? t.UnionType>> + : T extends t.IntersectionType + ? t.IntersectionType>> + : T extends t.ArrayType + ? t.ArrayType> + : T; + +// Based on t.PartialC +export interface TDeepPartialProps

+ extends t.PartialType< + P, + { + [K in keyof P]?: TDeepPartial>; + }, + { + [K in keyof P]?: TDeepPartial>; + } + > {} + +export function tDeepPartial(type: T): TDeepPartial { + if (type instanceof t.InterfaceType) { + const newProps = {}; + for (const [key, prop] of Object.entries(type.props)) { + newProps[key] = tDeepPartial(prop); + } + return t.partial(newProps) as TDeepPartial; + } else if (type instanceof t.DictionaryType) { + return t.record(type.domain, tDeepPartial(type.codomain)) as TDeepPartial; + } else if (type instanceof t.UnionType) { + return t.union(type.types.map(unionType => tDeepPartial(unionType))) as TDeepPartial; + } else if (type instanceof t.IntersectionType) { + const types = type.types.map(intersectionType => tDeepPartial(intersectionType)); + return (t.intersection(types as [t.Mixed, t.Mixed]) as unknown) as TDeepPartial; + } else if (type instanceof t.ArrayType) { + return t.array(tDeepPartial(type.type)) as TDeepPartial; + } else { + return type as TDeepPartial; + } +} + +function tDeepPartialProp(prop: any) { + if (typeHasProps(prop)) { + return tDeepPartial(prop); + } else if (typeIsArray(prop)) { + return t.array(tDeepPartialProp(prop.type)); + } else { + return prop; + } +} + /** * Mirrors EmbedOptions from Eris */ diff --git a/backend/src/validation.test.ts b/backend/src/validation.test.ts new file mode 100644 index 00000000..a916fff9 --- /dev/null +++ b/backend/src/validation.test.ts @@ -0,0 +1,41 @@ +import { tDeepPartial } from "./utils"; +import * as t from "io-ts"; +import * as validatorUtils from "./validatorUtils"; +import test from "ava"; +import util from "util"; + +test("tDeepPartial works", ava => { + const originalSchema = t.type({ + listOfThings: t.record( + t.string, + t.type({ + enabled: t.boolean, + someValue: t.number, + }), + ), + }); + + const deepPartialSchema = tDeepPartial(originalSchema); + + const partialValidValue = { + listOfThings: { + myThing: { + someValue: 5, + }, + }, + }; + + const partialErrorValue = { + listOfThings: { + myThing: { + someValue: "test", + }, + }, + }; + + const result1 = validatorUtils.validate(deepPartialSchema, partialValidValue); + ava.is(result1, null); + + const result2 = validatorUtils.validate(deepPartialSchema, partialErrorValue); + ava.not(result2, null); +}); diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index 1495d8b5..b5743030 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -83,6 +83,17 @@ const report = fold((errors: any): StrictValidationError | void => { return new StrictValidationError(errorStrings); }, noop); +export function validate(schema: t.Type, value: any): StrictValidationError | null { + const validationResult = schema.decode(value); + return pipe( + validationResult, + fold( + err => report(validationResult), + result => null, + ), + ); +} + /** * Decodes and validates the given value against the given schema while also disallowing extra properties * See: https://github.com/gcanti/io-ts/issues/322 From 581cf80feb76ce5d2695fe44364ed42956eb12d5 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 28 Nov 2019 02:39:26 +0200 Subject: [PATCH 12/67] starboard: post-merge tweaks; use preprocessStaticConfig; utilize overrides rather than config properties when possible --- backend/src/data/GuildStarboardMessages.ts | 6 +- backend/src/plugins/Starboard.ts | 291 ++++++++++----------- 2 files changed, 145 insertions(+), 152 deletions(-) diff --git a/backend/src/data/GuildStarboardMessages.ts b/backend/src/data/GuildStarboardMessages.ts index d98a4414..c01bb8dc 100644 --- a/backend/src/data/GuildStarboardMessages.ts +++ b/backend/src/data/GuildStarboardMessages.ts @@ -26,12 +26,12 @@ export class GuildStarboardMessages extends BaseGuildRepository { .getMany(); } - async getMessagesForStarboardIdAndSourceMessageId(starboardId: string, sourceMessageId: string) { + async getMatchingStarboardMessages(starboardChannelId: string, sourceMessageId: string) { return this.allStarboardMessages .createQueryBuilder() - .where("guild_id = :gid", { gid: this.guildId }) + .where("guild_id = :guildId", { guildId: this.guildId }) .andWhere("message_id = :msgId", { msgId: sourceMessageId }) - .andWhere("starboard_channel_id = :sbId", { sbId: starboardId }) + .andWhere("starboard_channel_id = :channelId", { channelId: starboardChannelId }) .getMany(); } diff --git a/backend/src/plugins/Starboard.ts b/backend/src/plugins/Starboard.ts index 897cecce..8bcd1be8 100644 --- a/backend/src/plugins/Starboard.ts +++ b/backend/src/plugins/Starboard.ts @@ -1,7 +1,16 @@ import { decorators as d, IPluginOptions } from "knub"; import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; -import { GuildChannel, Message, TextChannel } from "eris"; -import { errorMessage, getUrlsInString, noop, successMessage, tNullable } from "../utils"; +import { Embed, EmbedBase, GuildChannel, Message, TextChannel } from "eris"; +import { + errorMessage, + getUrlsInString, + messageLink, + noop, + successMessage, + TDeepPartialProps, + tNullable, + tDeepPartial, +} from "../utils"; import path from "path"; import moment from "moment-timezone"; import { GuildSavedMessages } from "../data/GuildSavedMessages"; @@ -12,78 +21,85 @@ import { StarboardMessage } from "../data/entities/StarboardMessage"; import { GuildStarboardReactions } from "../data/GuildStarboardReactions"; const StarboardOpts = t.type({ - source_channel_ids: t.array(t.string), - starboard_channel_id: t.string, - positive_emojis: tNullable(t.array(t.string)), - positive_required: tNullable(t.number), + channel_id: t.string, + stars_required: t.number, + star_emoji: tNullable(t.array(t.string)), enabled: tNullable(t.boolean), }); type TStarboardOpts = t.TypeOf; const ConfigSchema = t.type({ - entries: t.record(t.string, StarboardOpts), - + boards: t.record(t.string, StarboardOpts), can_migrate: t.boolean, }); type TConfigSchema = t.TypeOf; +const PartialConfigSchema = tDeepPartial(ConfigSchema); + const defaultStarboardOpts: Partial = { - positive_emojis: ["⭐"], - positive_required: 5, + star_emoji: ["⭐"], enabled: true, }; export class StarboardPlugin extends ZeppelinPlugin { public static pluginName = "starboard"; - public static showInDocs = false; public static configSchema = ConfigSchema; public static pluginInfo = { - prettyName: "Starboards", + prettyName: "Starboard", description: trimPluginDescription(` - This plugin contains all functionality needed to use discord channels as starboards. + This plugin allows you to set up starboards on your server. Starboards are like user voted pins where messages with enough reactions get immortalized on a "starboard" channel. `), configurationGuide: trimPluginDescription(` - You can customize multiple settings for starboards. - Any emoji that you want available needs to be put into the config in its raw form. - To obtain a raw form of an emoji, please write out the emoji and put a backslash in front of it. - Example with default emoji: "\:star:" => "⭐" - Example with custom emoji: "\:mrvnSmile:" => "<:mrvnSmile:543000534102310933>" - Now, past the result into the config, but make sure to exclude all less-than and greater-than signs like in the second example. + ### Note on emojis + To specify emoji in the config, you need to use the emoji's "raw form". + To obtain this, post the emoji with a backslash in front of it. + + - Example with a default emoji: "\:star:" => "⭐" + - Example with a custom emoji: "\:mrvnSmile:" => "<:mrvnSmile:543000534102310933>" - - ### Starboard with one source channel - All messages in the source channel that get enough positive reactions will be posted into the starboard channel. - The only positive reaction counted here is the default emoji "⭐". - Only users with a role matching the allowed_roles role-id will be counted. + ### Basic starboard + Any message on the server that gets 5 star reactions will be posted into the starboard channel (604342689038729226). ~~~yml starboard: config: - entries: - exampleOne: - source_channel_ids: ["604342623569707010"] - starboard_channel_id: "604342689038729226" - positive_emojis: ["⭐"] - positive_required: 5 - allowed_roles: ["556110793058287637"] - enabled: true + boards: + basic: + channel_id: "604342689038729226" + stars_required: 5 ~~~ - ### Starboard with two sources and two emoji - All messages in any of the source channels that get enough positive reactions will be posted into the starboard channel. - Both the default emoji "⭐" and the custom emoji ":mrvnSmile:543000534102310933" are counted. + ### Custom star emoji + This is identical to the basic starboard above, but accepts two emoji: the regular star and a custom :mrvnSmile: emoji ~~~yml starboard: config: - entries: - exampleTwo: - source_channel_ids: ["604342623569707010", "604342649251561487"] - starboard_channel_id: "604342689038729226" - positive_emojis: ["⭐", ":mrvnSmile:543000534102310933"] - positive_required: 10 - enabled: true + boards: + basic: + channel_id: "604342689038729226" + star_emoji: ["⭐", "<:mrvnSmile:543000534102310933>"] + stars_required: 5 + ~~~ + + ### Limit starboard to a specific channel + This is identical to the basic starboard above, but only works from a specific channel (473087035574321152). + + ~~~yml + starboard: + config: + boards: + basic: + enabled: false # The starboard starts disabled and is then enabled in a channel override below + channel_id: "604342689038729226" + stars_required: 5 + overrides: + - channel: "473087035574321152" + config: + boards: + basic: + enabled: true ~~~ `), }; @@ -98,7 +114,7 @@ export class StarboardPlugin extends ZeppelinPlugin { return { config: { can_migrate: false, - entries: {}, + boards: {}, }, overrides: [ @@ -112,27 +128,24 @@ export class StarboardPlugin extends ZeppelinPlugin { }; } - protected getStarboardOptsForSourceChannel(sourceChannel): TStarboardOpts[] { - const config = this.getConfigForChannel(sourceChannel); + protected static preprocessStaticConfig(config: t.TypeOf) { + if (config.boards) { + for (const [name, opts] of Object.entries(config.boards)) { + config.boards[name] = Object.assign({}, defaultStarboardOpts, config.boards[name]); + } + } - const configs = Object.values(config.entries).filter(opts => opts.source_channel_ids.includes(sourceChannel.id)); - configs.forEach(cfg => { - if (cfg.enabled == null) cfg.enabled = defaultStarboardOpts.enabled; - if (cfg.positive_emojis == null) cfg.positive_emojis = defaultStarboardOpts.positive_emojis; - if (cfg.positive_required == null) cfg.positive_required = defaultStarboardOpts.positive_required; - }); - - return configs; + return config; } protected getStarboardOptsForStarboardChannel(starboardChannel): TStarboardOpts[] { const config = this.getConfigForChannel(starboardChannel); - const configs = Object.values(config.entries).filter(opts => opts.starboard_channel_id === starboardChannel.id); + const configs = Object.values(config.boards).filter(opts => opts.channel_id === starboardChannel.id); configs.forEach(cfg => { if (cfg.enabled == null) cfg.enabled = defaultStarboardOpts.enabled; - if (cfg.positive_emojis == null) cfg.positive_emojis = defaultStarboardOpts.positive_emojis; - if (cfg.positive_required == null) cfg.positive_required = defaultStarboardOpts.positive_required; + if (cfg.star_emoji == null) cfg.star_emoji = defaultStarboardOpts.star_emoji; + if (cfg.stars_required == null) cfg.stars_required = defaultStarboardOpts.stars_required; }); return configs; @@ -168,26 +181,24 @@ export class StarboardPlugin extends ZeppelinPlugin { } } - const applicableStarboards = await this.getStarboardOptsForSourceChannel(msg.channel); + const config = this.getConfigForMemberIdAndChannelId(userId, msg.channel.id); + const applicableStarboards = Object.values(config.boards).filter(board => board.enabled); for (const starboard of applicableStarboards) { - // Instantly continue if the starboard is disabled - if (!starboard.enabled) continue; // Can't star messages in the starboard channel itself - if (msg.channel.id === starboard.starboard_channel_id) continue; - // Move reaction into DB at this point + if (msg.channel.id === starboard.channel_id) continue; + + // Save reaction into the database await this.starboardReactions.createStarboardReaction(msg.id, userId).catch(); - // If the message has already been posted to this starboard, we don't need to do anything else here - const starboardMessages = await this.starboardMessages.getMessagesForStarboardIdAndSourceMessageId( - starboard.starboard_channel_id, - msg.id, - ); + + // If the message has already been posted to this starboard, we don't need to do anything else + const starboardMessages = await this.starboardMessages.getMatchingStarboardMessages(starboard.channel_id, msg.id); if (starboardMessages.length > 0) continue; const reactions = await this.starboardReactions.getAllReactionsForMessageId(msg.id); const reactionsCount = reactions.length; - if (reactionsCount >= starboard.positive_required) { - await this.saveMessageToStarboard(msg, starboard.starboard_channel_id); + if (reactionsCount >= starboard.stars_required) { + await this.saveMessageToStarboard(msg, starboard.channel_id); } } } @@ -203,8 +214,8 @@ export class StarboardPlugin extends ZeppelinPlugin { } /** - * Saves/posts a message to the specified starboard. The message is posted as an embed and image attachments are - * included as the embed image. + * Saves/posts a message to the specified starboard. + * The message is posted as an embed and image attachments are included as the embed image. */ async saveMessageToStarboard(msg: Message, starboardChannelId: string) { const channel = this.guild.channels.get(starboardChannelId); @@ -212,13 +223,14 @@ export class StarboardPlugin extends ZeppelinPlugin { const time = moment(msg.timestamp, "x").format("YYYY-MM-DD [at] HH:mm:ss [UTC]"); - const embed: any = { + const embed: EmbedBase = { footer: { text: `#${(msg.channel as GuildChannel).name} - ${time}`, }, author: { name: `${msg.author.username}#${msg.author.discriminator}`, }, + url: messageLink(msg), }; if (msg.author.avatarURL) { @@ -229,6 +241,7 @@ export class StarboardPlugin extends ZeppelinPlugin { embed.description = msg.content; } + // Include attachments if (msg.attachments.length) { const attachment = msg.attachments[0]; const ext = path @@ -238,29 +251,14 @@ export class StarboardPlugin extends ZeppelinPlugin { if (["jpeg", "jpg", "png", "gif", "webp"].includes(ext)) { embed.image = { url: attachment.url }; } - } else if (msg.content) { - const links = getUrlsInString(msg.content); - for (const link of links) { - const parts = link - .toString() - .replace(/\/$/, "") - .split("."); - const ext = parts[parts.length - 1].toLowerCase(); - - if ( - (link.hostname === "i.imgur.com" || link.hostname === "cdn.discordapp.com") && - ["jpeg", "jpg", "png", "gif", "webp"].includes(ext) - ) { - embed.image = { url: link.toString() }; - break; - } - } } - const starboardMessage = await (channel as TextChannel).createMessage({ - content: `https://discordapp.com/channels/${this.guildId}/${msg.channel.id}/${msg.id}`, - embed, - }); + // Include any embed images in the original message + if (msg.embeds.length && msg.embeds[0].image) { + embed.image = msg.embeds[0].image; + } + + const starboardMessage = await (channel as TextChannel).createMessage({ embed }); await this.starboardMessages.createStarboardMessage(channel.id, msg.id, starboardMessage.id); } @@ -271,81 +269,76 @@ export class StarboardPlugin extends ZeppelinPlugin { await this.bot.deleteMessage(msg.starboard_channel_id, msg.starboard_message_id).catch(noop); } - async removeMessageFromStarboardMessages(starboard_message_id: string, starboard_channel_id: string) { - await this.starboardMessages.deleteStarboardMessage(starboard_message_id, starboard_channel_id); + async removeMessageFromStarboardMessages(starboard_message_id: string, channel_id: string) { + await this.starboardMessages.deleteStarboardMessage(starboard_message_id, channel_id); } /** * When a message is deleted, also delete it from any starboards it's been posted in. + * Likewise, if a starboard message (i.e. the bot message in the starboard) is deleted, remove it from the database. * This function is called in response to GuildSavedMessages events. - * TODO: When a message is removed from the starboard itself, i.e. the bot's embed is removed, also remove that message from the starboard_messages database table */ async onMessageDelete(msg: SavedMessage) { - let messages = await this.starboardMessages.getStarboardMessagesForMessageId(msg.id); - if (messages.length > 0) { - for (const starboardMessage of messages) { - if (!starboardMessage.starboard_message_id) continue; - this.removeMessageFromStarboard(starboardMessage).catch(noop); - } - } else { - messages = await this.starboardMessages.getStarboardMessagesForStarboardMessageId(msg.id); - if (messages.length === 0) return; + // Deleted source message + const starboardMessages = await this.starboardMessages.getStarboardMessagesForMessageId(msg.id); + for (const starboardMessage of starboardMessages) { + this.removeMessageFromStarboard(starboardMessage); + } - for (const starboardMessage of messages) { - if (!starboardMessage.starboard_channel_id) continue; - this.removeMessageFromStarboardMessages( - starboardMessage.starboard_message_id, - starboardMessage.starboard_channel_id, - ).catch(noop); - } + // Deleted message from the starboard + const deletedStarboardMessages = await this.starboardMessages.getStarboardMessagesForStarboardMessageId(msg.id); + if (deletedStarboardMessages.length === 0) return; + + for (const starboardMessage of deletedStarboardMessages) { + this.removeMessageFromStarboardMessages( + starboardMessage.starboard_message_id, + starboardMessage.starboard_channel_id, + ); } } - @d.command("starboard migrate_pins", " ", { + @d.command("starboard migrate_pins", " ", { extra: { info: { description: - "Migrates all of a channels pins to starboard messages, posting them in the starboard channel. The old pins are not unpinned.", + "Posts all pins from a channel to the specified starboard. The pins are NOT unpinned automatically.", }, }, }) @d.permission("can_migrate") - async migratePinsCmd(msg: Message, args: { pinChannelId: string; starboardChannelId }) { - try { - const starboards = await this.getStarboardOptsForStarboardChannel(this.bot.getChannel(args.starboardChannelId)); - if (!starboards) { - msg.channel.createMessage(errorMessage("The specified channel doesn't have a starboard!")).catch(noop); - return; - } - - const channel = (await this.guild.channels.get(args.pinChannelId)) as GuildChannel & TextChannel; - if (!channel) { - msg.channel - .createMessage(errorMessage("Could not find the specified channel to migrate pins from!")) - .catch(noop); - return; - } - - msg.channel.createMessage(`Migrating pins from <#${channel.id}> to <#${args.starboardChannelId}>...`).catch(noop); - - const pins = await channel.getPins(); - pins.reverse(); // Migrate pins starting from the oldest message - - for (const pin of pins) { - const existingStarboardMessage = await this.starboardMessages.getMessagesForStarboardIdAndSourceMessageId( - args.starboardChannelId, - pin.id, - ); - if (existingStarboardMessage.length > 0) continue; - await this.saveMessageToStarboard(pin, args.starboardChannelId); - } - - msg.channel.createMessage(successMessage("Pins migrated!")).catch(noop); - } catch (error) { - this.sendErrorMessage( - msg.channel, - "Sorry, but something went wrong!\nSyntax: `starboard migrate_pins `", - ); + async migratePinsCmd(msg: Message, args: { pinChannel: GuildChannel; starboardName: string }) { + const config = await this.getConfig(); + const starboard = config.boards[args.starboardName]; + if (!starboard) { + this.sendErrorMessage(msg.channel, "Unknown starboard specified"); + return; } + + if (!(args.pinChannel instanceof TextChannel)) { + this.sendErrorMessage(msg.channel, "Unknown/invalid pin channel id"); + return; + } + + const starboardChannel = this.guild.channels.get(starboard.channel_id); + if (!starboardChannel || !(starboardChannel instanceof TextChannel)) { + this.sendErrorMessage(msg.channel, "Starboard has an unknown/invalid channel id"); + return; + } + + msg.channel.createMessage(`Migrating pins from <#${args.pinChannel.id}> to <#${starboardChannel.id}>...`); + + const pins = await args.pinChannel.getPins(); + pins.reverse(); // Migrate pins starting from the oldest message + + for (const pin of pins) { + const existingStarboardMessage = await this.starboardMessages.getMatchingStarboardMessages( + starboardChannel.id, + pin.id, + ); + if (existingStarboardMessage.length > 0) continue; + await this.saveMessageToStarboard(pin, starboardChannel.id); + } + + this.sendSuccessMessage(msg.channel, `Pins migrated from <#${args.pinChannel.id}> to <#${starboardChannel.id}>!`); } } From f8444c1a3dbb779c757a3a13a0e269904f562b4f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 28 Nov 2019 02:47:15 +0200 Subject: [PATCH 13/67] utility.about: fix git repo path; don't throw an error if a git repo is not found --- backend/src/plugins/Utility.ts | 38 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index ee5fc185..3fc0c7f2 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -56,6 +56,7 @@ import { getCurrentUptime } from "../uptime"; import LCL from "last-commit-log"; import * as t from "io-ts"; import { ICommandDefinition } from "knub-command-manager"; +import path from "path"; const ConfigSchema = t.type({ can_roles: t.boolean, @@ -344,7 +345,10 @@ export class UtilityPlugin extends ZeppelinPlugin { matchingMembers.sort(sorter(m => BigInt(m.id), realSortDir)); } else { matchingMembers.sort( - multiSorter([[m => m.username.toLowerCase(), realSortDir], [m => m.discriminator, realSortDir]]), + multiSorter([ + [m => m.username.toLowerCase(), realSortDir], + [m => m.discriminator, realSortDir], + ]), ); } @@ -994,7 +998,12 @@ export class UtilityPlugin extends ZeppelinPlugin { ); // Clean up test messages - this.bot.deleteMessages(messages[0].channel.id, messages.map(m => m.id)).catch(noop); + this.bot + .deleteMessages( + messages[0].channel.id, + messages.map(m => m.id), + ) + .catch(noop); } @d.command("source", "", { @@ -1192,8 +1201,25 @@ export class UtilityPlugin extends ZeppelinPlugin { const uptime = getCurrentUptime(); const prettyUptime = humanizeDuration(uptime, { largest: 2, round: true }); - const lcl = new LCL(); - const lastCommit = await lcl.getLastCommit(); + let lastCommit; + + try { + // From project root + // FIXME: Store these paths properly somewhere + const lcl = new LCL(path.resolve(__dirname, "..", "..", "..")); + lastCommit = await lcl.getLastCommit(); + } catch (e) {} // tslint:disable-line:no-empty + + let lastUpdate; + let version; + + if (lastCommit) { + lastUpdate = moment(lastCommit.committer.date, "X").format("LL [at] H:mm [(UTC)]"); + version = lastCommit.shortHash; + } else { + lastUpdate = "?"; + version = "?"; + } const shard = this.bot.shards.get(this.bot.guildShardMap[this.guildId]); @@ -1205,8 +1231,8 @@ export class UtilityPlugin extends ZeppelinPlugin { const basicInfoRows = [ ["Uptime", prettyUptime], ["Last reload", `${lastReload} ago`], - ["Last update", moment(lastCommit.committer.date, "X").format("LL [at] H:mm [(UTC)]")], - ["Version", lastCommit.shortHash], + ["Last update", lastUpdate], + ["Version", version], ["API latency", `${shard.latency}ms`], ]; From b0df86692f7d164482973205f5c1e3a9ef85c246 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 28 Nov 2019 18:34:48 +0200 Subject: [PATCH 14/67] automod: simplify preprocessStaticConfig --- backend/src/plugins/Automod.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index 1ddac0a8..413c7460 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -12,6 +12,7 @@ import { noop, SECONDS, stripObjectToScalars, + tDeepPartial, tNullable, UnknownUser, verboseChannelMention, @@ -297,6 +298,8 @@ const ConfigSchema = t.type({ }); type TConfigSchema = t.TypeOf; +const PartialConfigSchema = tDeepPartial(ConfigSchema); + /** * DEFAULTS */ @@ -499,12 +502,10 @@ export class AutomodPlugin extends ZeppelinPlugin { protected archives: GuildArchives; protected guildLogs: GuildLogs; - protected static preprocessStaticConfig(config) { - if (config.rules && typeof config.rules === "object") { + protected static preprocessStaticConfig(config: t.TypeOf) { + if (config.rules) { // Loop through each rule for (const [name, rule] of Object.entries(config.rules)) { - if (rule == null || typeof rule !== "object") continue; - rule["name"] = name; // If the rule doesn't have an explicitly set "enabled" property, set it to true @@ -513,12 +514,11 @@ export class AutomodPlugin extends ZeppelinPlugin { } // Loop through the rule's triggers - if (rule["triggers"] != null && Array.isArray(rule["triggers"])) { + if (rule["triggers"]) { for (const trigger of rule["triggers"]) { - if (trigger == null || typeof trigger !== "object") continue; // Apply default config to the triggers used in this rule for (const [defaultTriggerName, defaultTrigger] of Object.entries(defaultTriggers)) { - if (trigger[defaultTriggerName] && typeof trigger[defaultTriggerName] === "object") { + if (trigger[defaultTriggerName]) { trigger[defaultTriggerName] = configUtils.mergeConfig({}, defaultTrigger, trigger[defaultTriggerName]); } } @@ -526,7 +526,7 @@ export class AutomodPlugin extends ZeppelinPlugin { } // Enable logging of automod actions by default - if (rule["actions"] && typeof rule["actions"] === "object") { + if (rule["actions"]) { if (rule["actions"]["log"] == null) { rule["actions"]["log"] = true; } From fd8a4598aabb13726581fd1d285c9a15be123e2f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 16:18:29 +0200 Subject: [PATCH 15/67] automod: add add_roles and remove_roles actions --- backend/src/plugins/Automod.ts | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index 413c7460..1fdc0022 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -254,6 +254,14 @@ const ChangeNicknameAction = t.type({ const LogAction = t.boolean; +const AddRolesAction = t.type({ + roles: t.array(t.string), +}); + +const RemoveRolesAction = t.type({ + roles: t.array(t.string), +}); + /** * FULL CONFIG SCHEMA */ @@ -288,6 +296,8 @@ const Rule = t.type({ alert: tNullable(AlertAction), change_nickname: tNullable(ChangeNicknameAction), log: tNullable(LogAction), + add_roles: tNullable(AddRolesAction), + remove_roles: tNullable(RemoveRolesAction), }), cooldown: tNullable(t.string), }); @@ -1255,6 +1265,44 @@ export class AutomodPlugin extends ZeppelinPlugin { actionsTaken.push("nickname"); } + if (rule.actions.add_roles) { + const userIdsToChange = matchResult.type === "raidspam" ? matchResult.userIds : [matchResult.userId]; + for (const userId of userIdsToChange) { + const member = await this.getMember(userId); + if (!member) continue; + + const memberRoles = new Set(member.roles); + for (const roleId of rule.actions.add_roles.roles) { + memberRoles.add(roleId); + } + + const rolesArr = Array.from(memberRoles.values()); + await member.edit({ + roles: rolesArr, + }); + member.roles = rolesArr; // Make sure we know of the new roles internally as well + } + } + + if (rule.actions.remove_roles) { + const userIdsToChange = matchResult.type === "raidspam" ? matchResult.userIds : [matchResult.userId]; + for (const userId of userIdsToChange) { + const member = await this.getMember(userId); + if (!member) continue; + + const memberRoles = new Set(member.roles); + for (const roleId of rule.actions.remove_roles.roles) { + memberRoles.delete(roleId); + } + + const rolesArr = Array.from(memberRoles.values()); + await member.edit({ + roles: rolesArr, + }); + member.roles = rolesArr; // Make sure we know of the new roles internally as well + } + } + // Don't wait for the rest before continuing to other automod items in the queue (async () => { const user = matchResult.type !== "raidspam" ? this.getUser(matchResult.userId) : new UnknownUser(); From 2a646f5a6e62b9d87318455d6d87f9022d94c465 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 17:04:59 +0200 Subject: [PATCH 16/67] Switch from ts-node to tsc-watch for faster watch restarts; move format npm script to project root --- backend/ormconfig.js | 9 +- backend/package-lock.json | 175 +++++++++++++----- backend/package.json | 34 ++-- ...od-paths.js => register-tsconfig-paths.js} | 0 backend/start-dev.js | 18 ++ package.json | 2 +- 6 files changed, 161 insertions(+), 77 deletions(-) rename backend/{register-tsconfig-prod-paths.js => register-tsconfig-paths.js} (100%) create mode 100644 backend/start-dev.js diff --git a/backend/ormconfig.js b/backend/ormconfig.js index c27a72b4..3cf907d9 100644 --- a/backend/ormconfig.js +++ b/backend/ormconfig.js @@ -16,13 +16,8 @@ try { const moment = require('moment-timezone'); moment.tz.setDefault('UTC'); -const entities = process.env.NODE_ENV === 'production' - ? path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/data/entities/*.js')) - : path.relative(process.cwd(), path.resolve(__dirname, 'src/data/entities/*.ts')); - -const migrations = process.env.NODE_ENV === 'production' - ? path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/migrations/*.js')) - : path.relative(process.cwd(), path.resolve(__dirname, 'src/migrations/*.ts')); +const entities = path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/data/entities/*.js')); +const migrations = path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/migrations/*.js')); module.exports = { type: "mysql", diff --git a/backend/package-lock.json b/backend/package-lock.json index a0942190..9ba8a783 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -707,12 +707,6 @@ "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", "integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==" }, - "arg": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.1.tgz", - "integrity": "sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw==", - "dev": true - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2293,12 +2287,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2330,6 +2318,12 @@ "ini": "^1.3.5" } }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -2493,6 +2487,21 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2822,6 +2831,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4294,12 +4309,6 @@ } } }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -4312,6 +4321,12 @@ "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", "dev": true }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -4603,6 +4618,12 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node-cleanup": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", + "integrity": "sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=", + "dev": true + }, "nodemon": { "version": "1.19.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", @@ -5058,6 +5079,15 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "~2.3" + } + }, "picomatch": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.0.tgz", @@ -5211,6 +5241,15 @@ "ipaddr.js": "1.9.0" } }, + "ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "requires": { + "event-stream": "=3.3.4" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -5903,24 +5942,6 @@ "urix": "^0.1.0" } }, - "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -5959,6 +5980,15 @@ "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -6010,6 +6040,21 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, + "string-argv": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.1.2.tgz", + "integrity": "sha512-mBqPGEOMNJKXRo7z0keX0wlAhbBAjilUdPW13nN0PecVryZxdHIeM7TqbsSUA7VYuS00HGC6mojP7DlQzfa9ZA==", + "dev": true + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -6202,6 +6247,12 @@ "thenify": ">= 3.1.0 < 4" } }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "time-zone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", @@ -6329,17 +6380,45 @@ "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-2.0.12.tgz", "integrity": "sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==" }, - "ts-node": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", - "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", + "tsc-watch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-4.0.0.tgz", + "integrity": "sha512-I+1cE7WN9YhDknNRAO5NRI7jzeiIZCxUZ0dFEM/Gf+3KTlHasusDEftwezJ+PSFkECSn3RQmf28RdovjTptkRA==", "dev": true, "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.6", - "yn": "^3.0.0" + "cross-spawn": "^5.1.0", + "node-cleanup": "^2.1.2", + "ps-tree": "^1.2.0", + "string-argv": "^0.1.1", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } } }, "tsconfig-paths": { @@ -6879,12 +6958,6 @@ "camelcase": "^5.0.0", "decamelize": "^1.2.0" } - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true } } } diff --git a/backend/package.json b/backend/package.json index f53089ec..7ba26576 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,18 +4,20 @@ "description": "", "private": true, "scripts": { - "start-bot-dev": "node -r ts-node/register -r tsconfig-paths/register src/index.ts", - "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-prod-paths.js dist/backend/src/index.js", - "watch-bot": "nodemon --config nodemon-bot.json", + "watch": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node start-dev.js\"", "build": "rimraf dist && tsc", - "start-api-dev": "node -r ts-node/register -r tsconfig-paths/register src/api/index.ts", - "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-prod-paths.js dist/backend/src/api/index.js", - "watch-api": "nodemon --config nodemon-api.json", - "format": "prettier --write \"./src/**/*.ts\"", - "typeorm": "node -r ts-node/register -r tsconfig-paths/register ./node_modules/typeorm/cli.js", - "migrate": "npm run typeorm -- migration:run", - "migrate-rollback": "npm run typeorm -- migration:revert", - "test": "ava" + "start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js dist/backend/src/index.js", + "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/index.js", + "start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", + "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", + "typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", + "migrate-prod": "npm run typeorm -- migration:run", + "migrate-dev": "npm run build && npm run typeorm -- migration:run", + "migrate-rollback-prod": "npm run typeorm -- migration:revert", + "migrate-rollback-dev": "npm run build && npm run typeorm -- migration:revert", + "test": "npm run build && npm run run-tests", + "run-tests": "ava", + "test-watch": "tsc-watch --onSuccess \"npx ava\"" }, "dependencies": { "cors": "^2.8.5", @@ -68,20 +70,16 @@ "ava": "^2.4.0", "nodemon": "^1.19.4", "rimraf": "^2.6.2", - "ts-node": "^8.4.1", + "tsc-watch": "^4.0.0", "typescript": "^3.7.2" }, "ava": { "compileEnhancements": false, "files": [ - "src/**/*.test.ts" - ], - "extensions": [ - "ts" + "dist/backend/src/**/*.test.js" ], "require": [ - "ts-node/register", - "tsconfig-paths/register" + "./register-tsconfig-paths.js" ] } } diff --git a/backend/register-tsconfig-prod-paths.js b/backend/register-tsconfig-paths.js similarity index 100% rename from backend/register-tsconfig-prod-paths.js rename to backend/register-tsconfig-paths.js diff --git a/backend/start-dev.js b/backend/start-dev.js new file mode 100644 index 00000000..5b6c3b44 --- /dev/null +++ b/backend/start-dev.js @@ -0,0 +1,18 @@ +/** + * This file starts the bot and api processes in tandem. + * Used with tsc-watch for restarting on watch. + */ + +const childProcess = require("child_process"); + +const cmd = process.platform === "win32" + ? "npm.cmd" + : "npm"; + +childProcess.spawn(cmd, ["run", "start-bot-dev"], { + stdio: [process.stdin, process.stdout, process.stderr], +}); + +childProcess.spawn(cmd, ["run", "start-api-dev"], { + stdio: [process.stdin, process.stdout, process.stderr], +}); diff --git a/package.json b/package.json index d63f5b91..28962d99 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "private": true, "scripts": { - "test": "cd backend && npm run test && cd ../shared && npm run test" + "format": "prettier --write \"./{backend,dashboard}/**/*.ts\"" }, "dependencies": {}, "devDependencies": { From 8b1aa9bce076064c49b1a6ff29f0338c913dbe41 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 17:05:36 +0200 Subject: [PATCH 17/67] Remove obsolete GuildStarboards repository --- backend/src/data/GuildStarboards.ts | 84 ----------------------------- 1 file changed, 84 deletions(-) delete mode 100644 backend/src/data/GuildStarboards.ts diff --git a/backend/src/data/GuildStarboards.ts b/backend/src/data/GuildStarboards.ts deleted file mode 100644 index b5f86e08..00000000 --- a/backend/src/data/GuildStarboards.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { BaseGuildRepository } from "./BaseGuildRepository"; -import { getRepository, Repository } from "typeorm"; -import { Starboard } from "./entities/Starboard"; -import { StarboardMessage } from "./entities/StarboardMessage"; - -export class GuildStarboards extends BaseGuildRepository { - private starboards: Repository; - private starboardMessages: Repository; - - constructor(guildId) { - super(guildId); - this.starboards = getRepository(Starboard); - this.starboardMessages = getRepository(StarboardMessage); - } - - getStarboardByChannelId(channelId): Promise { - return this.starboards.findOne({ - where: { - guild_id: this.guildId, - channel_id: channelId, - }, - }); - } - - getStarboardsByEmoji(emoji): Promise { - return this.starboards.find({ - where: { - guild_id: this.guildId, - emoji, - }, - }); - } - - getStarboardMessageByStarboardIdAndMessageId(starboardId, messageId): Promise { - return this.starboardMessages.findOne({ - relations: this.getRelations(), - where: { - starboard_id: starboardId, - message_id: messageId, - }, - }); - } - - getStarboardMessagesByMessageId(id): Promise { - return this.starboardMessages.find({ - relations: this.getRelations(), - where: { - message_id: id, - }, - }); - } - - async createStarboardMessage(starboardId, messageId, starboardMessageId): Promise { - await this.starboardMessages.insert({ - starboard_id: starboardId, - message_id: messageId, - starboard_message_id: starboardMessageId, - }); - } - - async deleteStarboardMessage(starboardId, messageId): Promise { - await this.starboardMessages.delete({ - starboard_id: starboardId, - message_id: messageId, - }); - } - - async create(channelId: string, channelWhitelist: string[], emoji: string, reactionsRequired: number): Promise { - await this.starboards.insert({ - guild_id: this.guildId, - channel_id: channelId, - channel_whitelist: channelWhitelist ? channelWhitelist.join(",") : null, - emoji, - reactions_required: reactionsRequired, - }); - } - - async delete(channelId: string): Promise { - await this.starboards.delete({ - guild_id: this.guildId, - channel_id: channelId, - }); - } -} From 8b46a070780337f04b8f6d8d4eaca292e37fa788 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 17:08:11 +0200 Subject: [PATCH 18/67] Remove now-unused nodemon --- backend/nodemon-api.json | 7 - backend/nodemon-bot.json | 7 - backend/package-lock.json | 2066 ------------------------------------- backend/package.json | 1 - 4 files changed, 2081 deletions(-) delete mode 100644 backend/nodemon-api.json delete mode 100644 backend/nodemon-bot.json diff --git a/backend/nodemon-api.json b/backend/nodemon-api.json deleted file mode 100644 index 8f3d7153..00000000 --- a/backend/nodemon-api.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "watch": ["src", "../shared/src"], - "ignore": ["src/migrations/*"], - "delay": 300, - "ext": "ts", - "exec": "node -r ts-node/register -r tsconfig-paths/register ./src/api/index.ts" -} diff --git a/backend/nodemon-bot.json b/backend/nodemon-bot.json deleted file mode 100644 index fdfb19e9..00000000 --- a/backend/nodemon-bot.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "watch": ["src", "../shared/src"], - "ignore": ["src/api/*", "src/migrations/*"], - "delay": 300, - "ext": "ts", - "exec": "node -r ts-node/register -r tsconfig-paths/register ./src/index.ts" -} diff --git a/backend/package-lock.json b/backend/package-lock.json index 9ba8a783..711f6350 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -614,12 +614,6 @@ "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", "dev": true }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -629,42 +623,6 @@ "negotiator": "0.6.2" } }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "ansi-escapes": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", @@ -692,16 +650,6 @@ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, "app-root-path": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", @@ -715,24 +663,12 @@ "sprintf-js": "~1.0.2" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -756,47 +692,23 @@ "integrity": "sha512-bdHxtev7FN6+MXI1YFW0Q8mQ8dTJc2S8AMfju+ZR77pbg2yAdVyDlwkaUI7Har0LyOMRFPHrJ9lYdyjZZswdlQ==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "ava": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ava/-/ava-2.4.0.tgz", @@ -1376,61 +1288,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -1446,12 +1303,6 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, "bluebird": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", @@ -1481,54 +1332,6 @@ "type-is": "~1.6.17" } }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1538,35 +1341,6 @@ "concat-map": "0.0.1" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "buffer": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", @@ -1587,23 +1361,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -1678,12 +1435,6 @@ } } }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1694,34 +1445,6 @@ "supports-color": "^5.3.0" } }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - } - } - }, "chunkd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-1.0.0.tgz", @@ -1740,29 +1463,6 @@ "integrity": "sha512-u6dx20FBXm+apMi+5x7UVm6EH7BL1gc4XrcnQewjcB7HWRcor/V5qWc3RG2HwpgDJ26gIi2DSEu3B7sXynAw/g==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1775,12 +1475,6 @@ "integrity": "sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=", "dev": true }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -1871,16 +1565,6 @@ "convert-to-spaces": "^1.0.1" } }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1900,12 +1584,6 @@ "integrity": "sha1-zVL28HEuC6q5fW+XModPIvR3UsA=", "dev": true }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1941,31 +1619,6 @@ } } }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } - } - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -2004,12 +1657,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, "core-js": { "version": "2.6.10", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", @@ -2030,15 +1677,6 @@ "vary": "^1" } }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, "cross-env": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.1.tgz", @@ -2114,12 +1752,6 @@ } } }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", @@ -2177,47 +1809,6 @@ "object-keys": "^1.0.12" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -2296,15 +1887,6 @@ "path-type": "^4.0.0" } }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, "dotenv": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", @@ -2502,41 +2084,6 @@ "through": "~2.3.1" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -2574,92 +2121,6 @@ "vary": "~1.1.2" } }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "fast-diff": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", @@ -2756,29 +2217,6 @@ "escape-string-regexp": "^1.0.5" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -2801,12 +2239,6 @@ "locate-path": "^3.0.0" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -2817,15 +2249,6 @@ "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.1.1.tgz", "integrity": "sha512-YcWhMdDCFCja0MmaDroTgNu+NWWrrnUEn92nvDgrtVy9Z71YFnhNVIghoHPt8gs82ijoMzFGeWKvArbyICiJgw==" }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2842,554 +2265,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3426,12 +2301,6 @@ "pump": "^3.0.0" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, "glob": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz", @@ -3445,27 +2314,6 @@ "path-is-absolute": "^1.0.0" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, "global-dirs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", @@ -3497,33 +2345,6 @@ "slash": "^3.0.0" } }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - }, - "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - } - } - }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -3563,38 +2384,6 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", @@ -3747,26 +2536,6 @@ "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", "dev": true }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", @@ -3779,21 +2548,6 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", @@ -3808,62 +2562,17 @@ "ci-info": "^2.0.0" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, "is-error": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz", "integrity": "sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==", "dev": true }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3894,38 +2603,6 @@ "is-path-inside": "^1.0.0" } }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, "is-observable": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.0.0.tgz", @@ -3973,27 +2650,12 @@ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -4002,12 +2664,6 @@ "has": "^1.0.1" } }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true - }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -4040,12 +2696,6 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", @@ -4062,12 +2712,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -4125,12 +2769,6 @@ "json-buffer": "3.0.0" } }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, "knub": { "version": "26.1.0", "resolved": "https://registry.npmjs.org/knub/-/knub-26.1.0.tgz", @@ -4173,15 +2811,6 @@ "dotgitconfig": "^1.0.1" } }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4309,12 +2938,6 @@ } } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -4327,15 +2950,6 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, "matcher": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-2.0.0.tgz", @@ -4433,27 +3047,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4507,27 +3100,6 @@ "is-plain-obj": "^1.1.0" } }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -4582,32 +3154,6 @@ "thenify-all": "^1.0.0" } }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -4624,50 +3170,6 @@ "integrity": "sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=", "dev": true }, - "nodemon": { - "version": "1.19.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", - "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", - "dev": true, - "requires": { - "chokidar": "^2.1.8", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -4680,15 +3182,6 @@ "validate-npm-package-license": "^3.0.1" } }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, "normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", @@ -4714,37 +3207,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "object-inspect": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", @@ -4761,15 +3223,6 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", @@ -4791,15 +3244,6 @@ "es-abstract": "^1.5.1" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "observable-to-promise": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/observable-to-promise/-/observable-to-promise-1.0.0.tgz", @@ -4947,18 +3391,6 @@ "release-zalgo": "^1.0.0" } }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, "parent-require": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", @@ -4990,12 +3422,6 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, "passport": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", @@ -5030,12 +3456,6 @@ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -5206,18 +3626,6 @@ "irregular-plurals": "^2.0.0" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, "pretty-ms": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-5.0.0.tgz", @@ -5256,12 +3664,6 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, - "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", - "dev": true - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -5402,17 +3804,6 @@ "util-deprecate": "~1.0.1" } }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, "redent": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", @@ -5451,27 +3842,6 @@ "regenerate": "^1.4.0" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - } - } - }, "regexp-tree": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.14.tgz", @@ -5500,25 +3870,6 @@ "unicode-match-property-value-ecmascript": "^1.1.0" } }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, "regjsgen": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", @@ -5551,24 +3902,6 @@ "es6-error": "^4.0.1" } }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5609,12 +3942,6 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -5634,12 +3961,6 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5752,29 +4073,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -5816,138 +4114,12 @@ "is-fullwidth-code-point": "^2.0.0" } }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -5989,15 +4161,6 @@ "through": "2" } }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6014,27 +4177,6 @@ "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -6259,12 +4401,6 @@ "integrity": "sha1-mcW/VZWJZq9tBtg73zgA3IL67F0=", "dev": true }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, "tlds": { "version": "1.203.1", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.203.1.tgz", @@ -6284,79 +4420,17 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", "dev": true }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, "trim-newlines": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", @@ -6529,15 +4603,6 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -6566,18 +4631,6 @@ "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", "dev": true }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, "unique-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", @@ -6603,114 +4656,6 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - } - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6829,17 +4774,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "write-file-atomic": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", - "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, "ws": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 7ba26576..0e7f252e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -68,7 +68,6 @@ "@types/passport-strategy": "^0.2.35", "@types/tmp": "0.0.33", "ava": "^2.4.0", - "nodemon": "^1.19.4", "rimraf": "^2.6.2", "tsc-watch": "^4.0.0", "typescript": "^3.7.2" From fb43ec159a340668eb23426692c0cd13cbd178dd Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 17:11:20 +0200 Subject: [PATCH 19/67] Update README instructions --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bfaadfaf..cf65b7c6 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,10 @@ These instructions are intended for bot development only. 2. `npm ci` 3. Make a copy of `bot.env.example` called `bot.env`, fill in the values 4. Run the desired start script: - * `npm run start-bot-dev` to run the bot with `ts-node` - * `npm run build` followed by `npm run start-bot-prod` to run the bot compiled - * `npm run watch-bot` to run the bot with `ts-node` and restart on changes + * `npm run build` followed by `npm run start-bot-dev` to run the bot in a **development** environment + * `npm run build` followed by `npm run start-bot-prod` to run the bot in a **production** environment + * `npm run watch` to watch files and run the **bot and api both** in a **development** environment + with automatic restart on file changes 5. When testing, make sure you have your test server in the `allowed_guilds` table or the guild's config won't be loaded at all ## Running the API server @@ -18,9 +19,10 @@ These instructions are intended for bot development only. 2. `npm ci` 3. Make a copy of `api.env.example` called `api.env`, fill in the values 4. Run the desired start script: - * `npm run start-api-dev` to run the API server with `ts-node` - * `npm run build` followed by `npm run start-api-prod` to run the API server compiled - * `npm run watch-api` to run the API server with `ts-node` and restart on changes + * `npm run build` followed by `npm run start-api-dev` to run the api in a **development** environment + * `npm run build` followed by `npm run start-api-prod` to run the api in a **production** environment + * `npm run watch` to watch files and run the **bot and api both** in a **development** environment + with automatic restart on file changes ## Running the dashboard 1. `cd dashboard` @@ -34,8 +36,8 @@ These instructions are intended for bot development only. * Since we now use shared paths in `tsconfig.json`, the compiled files in `backend/dist/` have longer paths, e.g. `backend/dist/backend/src/index.js` instead of `backend/dist/index.js`. This is because the compiled shared files are placed in `backend/dist/shared`. -* The `backend/register-tsconfig-prod-paths.js` module takes care of registering shared paths from `tsconfig.json` for - `ts-node`, `ava`, and compiled `.js` files +* The `backend/register-tsconfig-paths.js` module takes care of registering shared paths from `tsconfig.json` for + `ava` and compiled `.js` files * To run the tests for the files in the `shared/` directory, you also need to run `npm ci` there ## Config format example From 7df1bb91d26c2c58019d9c93bb158f893606fe2b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 18:07:25 +0200 Subject: [PATCH 20/67] automod: show matched text in summaries; don't use show post date in summaries; add logMessage variable to alert action Post date will always be more or less the time the log message is posted. The logMessage variable in the alert action contains the full, formatted log message that would be posted in a log channel as the AUTOMOD_ACTION log type. --- backend/src/plugins/Automod.ts | 287 +++++++++++++++++++-------------- backend/src/utils.ts | 15 +- 2 files changed, 175 insertions(+), 127 deletions(-) diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index 1fdc0022..7964883f 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -2,6 +2,9 @@ import { PluginInfo, trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlu import * as t from "io-ts"; import { convertDelayStringToMS, + disableCodeBlocks, + disableInlineCode, + disableLinkPreviews, getEmojiInString, getInviteCodesInString, getRoleMentions, @@ -55,12 +58,14 @@ interface MessageTextTriggerMatchResult extends TriggerMatchResult { str: string; userId: string; messageInfo: MessageInfo; + matchedContent?: string; } interface OtherTextTriggerMatchResult extends TriggerMatchResult { type: "username" | "nickname" | "visiblename" | "customstatus"; str: string; userId: string; + matchedContent?: string; } type TextTriggerMatchResult = MessageTextTriggerMatchResult | OtherTextTriggerMatchResult; @@ -94,7 +99,7 @@ type AnyTriggerMatchResult = | OtherSpamTriggerMatchResult; /** - * TRIGGERS + * CONFIG SCHEMA FOR TRIGGERS */ const MatchWordsTrigger = t.type({ @@ -221,7 +226,7 @@ const VoiceMoveSpamTrigger = BaseSpamTrigger; type TVoiceMoveSpamTrigger = t.TypeOf; /** - * ACTIONS + * CONFIG SCHEMA FOR ACTIONS */ const CleanAction = t.boolean; @@ -254,13 +259,8 @@ const ChangeNicknameAction = t.type({ const LogAction = t.boolean; -const AddRolesAction = t.type({ - roles: t.array(t.string), -}); - -const RemoveRolesAction = t.type({ - roles: t.array(t.string), -}); +const AddRolesAction = t.array(t.string); +const RemoveRolesAction = t.array(t.string); /** * FULL CONFIG SCHEMA @@ -374,6 +374,13 @@ const RECENT_NICKNAME_CHANGE_EXPIRY_TIME = 5 * MINUTES; const inviteCache = new SimpleCache(10 * MINUTES); +/** + * General plugin flow: + * - When a message is posted: + * 1. Run logRecentActionsForMessage() -- used for detecting spam + * 2. Run matchRuleToMessage() for each automod rule. This checks if any triggers in the rule match the message. + * 3. If a rule matched, run applyActionsOnMatch() for that rule/match + */ export class AutomodPlugin extends ZeppelinPlugin { public static pluginName = "automod"; public static configSchema = ConfigSchema; @@ -593,62 +600,72 @@ export class AutomodPlugin extends ZeppelinPlugin { clearInterval(this.recentNicknameChangesClearInterval); } - protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): boolean { + /** + * @return Matched word + */ + protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): null | string { for (const word of trigger.words) { const pattern = trigger.only_full_words ? `\\b${escapeStringRegexp(word)}\\b` : escapeStringRegexp(word); const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i"); const test = regex.test(str); - if (test) return true; + if (test) return word; } - return false; + return null; } - protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): boolean { + /** + * @return Matched regex pattern + */ + protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): null | string { // TODO: Time limit regexes for (const pattern of trigger.patterns) { const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i"); const test = regex.test(str); - if (test) return true; + if (test) return regex.source; } - return false; + return null; } - protected async evaluateMatchInvitesTrigger(trigger: TMatchInvitesTrigger, str: string): Promise { + /** + * @return Matched invite code + */ + protected async evaluateMatchInvitesTrigger(trigger: TMatchInvitesTrigger, str: string): Promise { const inviteCodes = getInviteCodesInString(str); - if (inviteCodes.length === 0) return false; + if (inviteCodes.length === 0) return null; const uniqueInviteCodes = Array.from(new Set(inviteCodes)); for (const code of uniqueInviteCodes) { if (trigger.include_invite_codes && trigger.include_invite_codes.includes(code)) { - return true; + return code; } if (trigger.exclude_invite_codes && !trigger.exclude_invite_codes.includes(code)) { - return true; + return code; } } - const invites: Array = await Promise.all(uniqueInviteCodes.map(code => this.resolveInvite(code))); - - for (const invite of invites) { - // Always match on unknown invites - if (!invite) return true; + for (const inviteCode of uniqueInviteCodes) { + const invite = await this.resolveInvite(inviteCode); + if (!invite) return inviteCode; if (trigger.include_guilds && trigger.include_guilds.includes(invite.guild.id)) { - return true; + return inviteCode; } if (trigger.exclude_guilds && !trigger.exclude_guilds.includes(invite.guild.id)) { - return true; + return inviteCode; } } - return false; + return null; } - protected evaluateMatchLinksTrigger(trigger: TMatchLinksTrigger, str: string): boolean { + /** + * @return Matched link + */ + protected evaluateMatchLinksTrigger(trigger: TMatchLinksTrigger, str: string): null | string { const links = getUrlsInString(str, true); for (const link of links) { @@ -658,10 +675,10 @@ export class AutomodPlugin extends ZeppelinPlugin { for (const domain of trigger.include_domains) { const normalizedDomain = domain.toLowerCase(); if (normalizedDomain === normalizedHostname) { - return true; + return domain; } if (trigger.include_subdomains && normalizedHostname.endsWith(`.${domain}`)) { - return true; + return domain; } } } @@ -670,18 +687,18 @@ export class AutomodPlugin extends ZeppelinPlugin { for (const domain of trigger.exclude_domains) { const normalizedDomain = domain.toLowerCase(); if (normalizedDomain === normalizedHostname) { - return false; + return null; } if (trigger.include_subdomains && normalizedHostname.endsWith(`.${domain}`)) { - return false; + return null; } } - return true; + return link.toString(); } } - return false; + return null; } protected matchTextSpamTrigger( @@ -721,38 +738,38 @@ export class AutomodPlugin extends ZeppelinPlugin { if (trigger.match_messages) { const str = msg.data.content; const match = await cb(str); - if (match) return { type: "message", str, userId: msg.user_id, messageInfo }; + if (match) return { type: "message", str, userId: msg.user_id, messageInfo, matchedContent: match }; } if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) { const str = JSON.stringify(msg.data.embeds[0]); const match = await cb(str); - if (match) return { type: "embed", str, userId: msg.user_id, messageInfo }; + if (match) return { type: "embed", str, userId: msg.user_id, messageInfo, matchedContent: match }; } if (trigger.match_visible_names) { const str = member.nick || msg.data.author.username; const match = await cb(str); - if (match) return { type: "visiblename", str, userId: msg.user_id }; + if (match) return { type: "visiblename", str, userId: msg.user_id, matchedContent: match }; } if (trigger.match_usernames) { const str = `${msg.data.author.username}#${msg.data.author.discriminator}`; const match = await cb(str); - if (match) return { type: "username", str, userId: msg.user_id }; + if (match) return { type: "username", str, userId: msg.user_id, matchedContent: match }; } if (trigger.match_nicknames && member.nick) { const str = member.nick; const match = await cb(str); - if (match) return { type: "nickname", str, userId: msg.user_id }; + if (match) return { type: "nickname", str, userId: msg.user_id, matchedContent: match }; } // type 4 = custom status if (trigger.match_custom_status && member.game && member.game.type === 4) { const str = member.game.state; const match = await cb(str); - if (match) return { type: "customstatus", str, userId: msg.user_id }; + if (match) return { type: "customstatus", str, userId: msg.user_id, matchedContent: match }; } return null; @@ -1059,89 +1076,19 @@ export class AutomodPlugin extends ZeppelinPlugin { } protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) { - const actionsTaken = []; - - let matchSummary = null; - let caseExtraNote = null; - - if (rule.cooldown) { - let cooldownKey = rule.name + "-"; - - if (matchResult.type === "textspam") { - cooldownKey += matchResult.channelId ? `${matchResult.channelId}-${matchResult.userId}` : matchResult.userId; - } else if (matchResult.type === "message" || matchResult.type === "embed") { - cooldownKey += matchResult.userId; - } else if ( - matchResult.type === "username" || - matchResult.type === "nickname" || - matchResult.type === "visiblename" || - matchResult.type === "customstatus" - ) { - cooldownKey += matchResult.userId; - } else if (matchResult.type === "otherspam") { - cooldownKey += matchResult.userId; - } else { - cooldownKey = null; - } - - if (cooldownKey) { - if (this.cooldownManager.isOnCooldown(cooldownKey)) { - return; - } - - const cooldownTime = convertDelayStringToMS(rule.cooldown, "s"); - if (cooldownTime) { - this.cooldownManager.setCooldown(cooldownKey, cooldownTime); - } - } + if (rule.cooldown && this.checkAndUpdateCooldown(rule, matchResult)) { + return; } - if (matchResult.type === "textspam") { - this.activateGracePeriod(matchResult); - this.clearSpecificRecentActions( - matchResult.actionType, - matchResult.channelId ? `${matchResult.channelId}-${matchResult.userId}` : matchResult.userId, - ); - } + const matchSummary = this.getMatchSummary(matchResult); - // Match summary - let matchedMessageIds = []; - if (matchResult.type === "message" || matchResult.type === "embed") { - matchedMessageIds = [matchResult.messageInfo.messageId]; - } else if (matchResult.type === "textspam" || matchResult.type === "raidspam") { - matchedMessageIds = matchResult.messageInfos.map(m => m.messageId); - } - - if (matchedMessageIds.length > 1) { - const savedMessages = await this.savedMessages.getMultiple(matchedMessageIds); - const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild); - const baseUrl = this.knub.getGlobalConfig().url; - const archiveUrl = this.archives.getUrl(baseUrl, archiveId); - matchSummary = `Matched messages: <${archiveUrl}>`; - } else if (matchedMessageIds.length === 1) { - const message = await this.savedMessages.find(matchedMessageIds[0]); - const channel = this.guild.channels.get(message.channel_id); - const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; - matchSummary = `Matched message in ${channelMention} (originally posted at **${ - message.posted_at - }**):\n${messageSummary(message)}`; - } - - if (matchResult.type === "username") { - matchSummary = `Matched username: ${matchResult.str}`; - } else if (matchResult.type === "nickname") { - matchSummary = `Matched nickname: ${matchResult.str}`; - } else if (matchResult.type === "visiblename") { - matchSummary = `Matched visible name: ${matchResult.str}`; - } else if (matchResult.type === "customstatus") { - matchSummary = `Matched custom status: ${matchResult.str}`; - } - - caseExtraNote = `Matched automod rule "${rule.name}"`; + let caseExtraNote = `Matched automod rule "${rule.name}"`; if (matchSummary) { caseExtraNote += `\n${matchSummary}`; } + const actionsTaken = []; + // Actions if (rule.actions.clean) { const messagesToDelete: Array<{ channelId: string; messageId: string }> = []; @@ -1272,16 +1219,23 @@ export class AutomodPlugin extends ZeppelinPlugin { if (!member) continue; const memberRoles = new Set(member.roles); - for (const roleId of rule.actions.add_roles.roles) { + for (const roleId of rule.actions.add_roles) { memberRoles.add(roleId); } + if (memberRoles.size === member.roles.length) { + // No role changes + continue; + } + const rolesArr = Array.from(memberRoles.values()); await member.edit({ roles: rolesArr, }); member.roles = rolesArr; // Make sure we know of the new roles internally as well } + + actionsTaken.push("add roles"); } if (rule.actions.remove_roles) { @@ -1291,16 +1245,23 @@ export class AutomodPlugin extends ZeppelinPlugin { if (!member) continue; const memberRoles = new Set(member.roles); - for (const roleId of rule.actions.remove_roles.roles) { + for (const roleId of rule.actions.remove_roles) { memberRoles.delete(roleId); } + if (memberRoles.size === member.roles.length) { + // No role changes + continue; + } + const rolesArr = Array.from(memberRoles.values()); await member.edit({ roles: rolesArr, }); member.roles = rolesArr; // Make sure we know of the new roles internally as well } + + actionsTaken.push("remove roles"); } // Don't wait for the rest before continuing to other automod items in the queue @@ -1310,6 +1271,15 @@ export class AutomodPlugin extends ZeppelinPlugin { const safeUser = stripObjectToScalars(user); const safeUsers = users.map(u => stripObjectToScalars(u)); + const logData = { + rule: rule.name, + user: safeUser, + users: safeUsers, + actionsTaken: actionsTaken.length ? actionsTaken.join(", ") : "", + matchSummary, + }; + const logMessage = this.getLogs().getLogMessage(LogType.AUTOMOD_ACTION, logData); + if (rule.actions.alert) { const channel = this.guild.channels.get(rule.actions.alert.channel); if (channel && channel instanceof TextChannel) { @@ -1320,6 +1290,7 @@ export class AutomodPlugin extends ZeppelinPlugin { users: safeUsers, text, matchSummary, + logMessage, }); channel.createMessage(rendered); actionsTaken.push("alert"); @@ -1331,17 +1302,83 @@ export class AutomodPlugin extends ZeppelinPlugin { } if (rule.actions.log) { - this.getLogs().log(LogType.AUTOMOD_ACTION, { - rule: rule.name, - user: safeUser, - users: safeUsers, - actionsTaken: actionsTaken.length ? actionsTaken.join(", ") : "", - matchSummary, - }); + this.getLogs().log(LogType.AUTOMOD_ACTION, logData); } })(); } + /** + * @return Whether the rule's on cooldown + */ + protected checkAndUpdateCooldown(rule: TRule, matchResult: AnyTriggerMatchResult): boolean { + let cooldownKey = rule.name + "-"; + + if (matchResult.type === "textspam") { + cooldownKey += matchResult.channelId ? `${matchResult.channelId}-${matchResult.userId}` : matchResult.userId; + } else if (matchResult.type === "message" || matchResult.type === "embed") { + cooldownKey += matchResult.userId; + } else if ( + matchResult.type === "username" || + matchResult.type === "nickname" || + matchResult.type === "visiblename" || + matchResult.type === "customstatus" + ) { + cooldownKey += matchResult.userId; + } else if (matchResult.type === "otherspam") { + cooldownKey += matchResult.userId; + } else { + cooldownKey = null; + } + + if (cooldownKey) { + if (this.cooldownManager.isOnCooldown(cooldownKey)) { + return true; + } + + const cooldownTime = convertDelayStringToMS(rule.cooldown, "s"); + if (cooldownTime) { + this.cooldownManager.setCooldown(cooldownKey, cooldownTime); + } + } + + return false; + } + + protected async getMatchSummary(matchResult: AnyTriggerMatchResult): Promise { + if (matchResult.type === "message" || matchResult.type === "embed") { + const message = await this.savedMessages.find(matchResult.messageInfo.messageId); + const channel = this.guild.channels.get(matchResult.messageInfo.channelId); + const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; + const matchedContent = disableInlineCode(matchResult.matchedContent); + + return trimPluginDescription(` + Matched \`${matchedContent}\` in message in ${channelMention}: + ${messageSummary(message)} + `); + } else if (matchResult.type === "textspam" || matchResult.type === "raidspam") { + const savedMessages = await this.savedMessages.getMultiple(matchResult.messageInfos.map(i => i.messageId)); + const archiveId = await this.archives.createFromSavedMessages(savedMessages, this.guild); + const baseUrl = this.knub.getGlobalConfig().url; + const archiveUrl = this.archives.getUrl(baseUrl, archiveId); + + return trimPluginDescription(` + Matched spam: ${disableLinkPreviews(archiveUrl)} + `); + } else if (matchResult.type === "username") { + const matchedContent = disableInlineCode(matchResult.matchedContent); + return `Matched \`${matchedContent}\` in username: ${matchResult.str}`; + } else if (matchResult.type === "nickname") { + const matchedContent = disableInlineCode(matchResult.matchedContent); + return `Matched \`${matchedContent}\` in nickname: ${matchResult.str}`; + } else if (matchResult.type === "visiblename") { + const matchedContent = disableInlineCode(matchResult.matchedContent); + return `Matched \`${matchedContent}\` in visible name: ${matchResult.str}`; + } else if (matchResult.type === "customstatus") { + const matchedContent = disableInlineCode(matchResult.matchedContent); + return `Matched \`${matchedContent}\` in custom status: ${matchResult.str}`; + } + } + protected onMessageCreate(msg: SavedMessage) { if (msg.is_bot) return; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index e5e86863..4b1d6740 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -209,7 +209,7 @@ export function convertDelayStringToMS(str, defaultUnit = "m"): number { } export function successMessage(str) { - return `👌 ${str}`; + return `<:zep_check:650361014180904971> ${str}`; } export function errorMessage(str) { @@ -462,7 +462,7 @@ export function getRoleMentions(str: string) { } /** - * Disables link previews in the given string by wrapping links in < > + * Disable link previews in the given string by wrapping links in < > */ export function disableLinkPreviews(str: string): string { return str.replace(/(?"); @@ -472,6 +472,17 @@ export function deactivateMentions(content: string): string { return content.replace(/@/g, "@\u200b"); } +/** + * Disable inline code in the given string by replacing backticks/grave accents with acute accents + * FIXME: Find a better way that keeps the grave accents? Can't use the code block approach here since it's just 1 character. + */ +export function disableInlineCode(content: string): string { + return content.replace(/`/g, "\u00b4"); +} + +/** + * Disable code blocks in the given string by adding invisible unicode characters between backticks + */ export function disableCodeBlocks(content: string): string { return content.replace(/`/g, "`\u200b"); } From 64e1fbc10cab546c7f51e8403c2d279a8973cea2 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 19:54:44 +0200 Subject: [PATCH 21/67] Add !context utility command --- backend/src/plugins/Utility.ts | 28 ++++++++++++++++++++++++++++ backend/src/utils.ts | 1 + 2 files changed, 29 insertions(+) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 3fc0c7f2..645d94b5 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -29,6 +29,7 @@ import { get, getInviteCodesInString, isSnowflake, + messageLink, MINUTES, multiSorter, noop, @@ -72,6 +73,7 @@ const ConfigSchema = t.type({ can_vcmove: t.boolean, can_help: t.boolean, can_about: t.boolean, + can_context: t.boolean, }); type TConfigSchema = t.TypeOf; @@ -125,6 +127,7 @@ export class UtilityPlugin extends ZeppelinPlugin { can_vcmove: false, can_help: false, can_about: false, + can_context: false, }, overrides: [ { @@ -139,6 +142,7 @@ export class UtilityPlugin extends ZeppelinPlugin { can_nickname: true, can_vcmove: true, can_help: true, + can_context: true, }, }, { @@ -1030,6 +1034,30 @@ export class UtilityPlugin extends ZeppelinPlugin { msg.channel.createMessage(`Message source: ${url}`); } + @d.command("context", " ", { + extra: { + info: { + description: "Get a link to the context of the specified message", + basicUsage: "!context 94882524378968064 650391267720822785", + }, + }, + }) + @d.permission("can_context") + async contextCmd(msg: Message, args: { channel: Channel; messageId: string }) { + if (!(args.channel instanceof TextChannel)) { + this.sendErrorMessage(msg.channel, "Channel must be a text channel"); + return; + } + + const previousMessage = (await this.bot.getMessages(args.channel.id, 1, args.messageId))[0]; + if (!previousMessage) { + this.sendErrorMessage(msg.channel, "Message context not found"); + return; + } + + msg.channel.createMessage(messageLink(this.guildId, previousMessage.channel.id, previousMessage.id)); + } + @d.command("vcmove", " ", { extra: { info: { diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 4b1d6740..2005f862 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -947,6 +947,7 @@ export function verboseChannelMention(channel: GuildChannel): string { } export function messageLink(message: Message): string; +export function messageLink(guildId: string, channelId: string, messageId: string): string; export function messageLink(guildIdOrMessage: string | Message | null, channelId?: string, messageId?: string): string { let guildId; if (guildIdOrMessage == null) { From e586bfbda3fb4a9a4c66fdff33ef35828bbc58f1 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:04:28 +0200 Subject: [PATCH 22/67] automod: add normalize and loose_matching trigger options --- backend/package-lock.json | 37 ++++++++++++++++++++++++++++++++++ backend/package.json | 1 + backend/src/plugins/Automod.ts | 29 +++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 711f6350..2f4e35e9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4431,6 +4431,43 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "transliteration": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.1.7.tgz", + "integrity": "sha512-o3678GPmKKGqOBB+trAKzhBUjHddU18He2V8AKB1XuegaGJekO0xmfkkvbc9LCBat62nb7IH8z5/OJY+mNugkg==", + "requires": { + "yargs": "^14.0.0" + }, + "dependencies": { + "yargs": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz", + "integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.0" + } + }, + "yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "trim-newlines": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 0e7f252e..08e2a049 100644 --- a/backend/package.json +++ b/backend/package.json @@ -51,6 +51,7 @@ "seedrandom": "^3.0.1", "tlds": "^1.203.1", "tmp": "0.0.33", + "transliteration": "^2.1.7", "tsconfig-paths": "^3.9.0", "typeorm": "^0.2.14", "uuid": "^3.3.2" diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index 7964883f..55306c14 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -36,6 +36,7 @@ import { GuildLogs } from "../data/GuildLogs"; import { SavedMessage } from "../data/entities/SavedMessage"; import moment from "moment-timezone"; import { renderTemplate } from "../templateFormatter"; +import { transliterate } from "transliteration"; import Timeout = NodeJS.Timeout; type MessageInfo = { channelId: string; messageId: string }; @@ -106,6 +107,9 @@ const MatchWordsTrigger = t.type({ words: t.array(t.string), case_sensitive: t.boolean, only_full_words: t.boolean, + normalize: t.boolean, + loose_matching: t.boolean, + loose_matching_threshold: t.number, match_messages: t.boolean, match_embeds: t.boolean, match_visible_names: t.boolean, @@ -118,6 +122,9 @@ const defaultMatchWordsTrigger: TMatchWordsTrigger = { words: [], case_sensitive: false, only_full_words: true, + normalize: false, + loose_matching: false, + loose_matching_threshold: 4, match_messages: true, match_embeds: true, match_visible_names: false, @@ -129,6 +136,7 @@ const defaultMatchWordsTrigger: TMatchWordsTrigger = { const MatchRegexTrigger = t.type({ patterns: t.array(TSafeRegex), case_sensitive: t.boolean, + normalize: t.boolean, match_messages: t.boolean, match_embeds: t.boolean, match_visible_names: t.boolean, @@ -139,6 +147,7 @@ const MatchRegexTrigger = t.type({ type TMatchRegexTrigger = t.TypeOf; const defaultMatchRegexTrigger: Partial = { case_sensitive: false, + normalize: false, match_messages: true, match_embeds: true, match_visible_names: false, @@ -604,8 +613,22 @@ export class AutomodPlugin extends ZeppelinPlugin { * @return Matched word */ protected evaluateMatchWordsTrigger(trigger: TMatchWordsTrigger, str: string): null | string { + if (trigger.normalize) { + str = transliterate(str); + } + + const looseMatchingThreshold = Math.min(Math.max(trigger.loose_matching_threshold, 1), 64); + for (const word of trigger.words) { - const pattern = trigger.only_full_words ? `\\b${escapeStringRegexp(word)}\\b` : escapeStringRegexp(word); + // When performing loose matching, allow any amount of whitespace or up to looseMatchingThreshold number of other + // characters between the matched characters. E.g. if we're matching banana, a loose match could also match b a n a n a + let pattern = trigger.loose_matching + ? [...word].map(c => escapeStringRegexp(c)).join(`(?:\\s*|.{0,${looseMatchingThreshold})`) + : escapeStringRegexp(word); + + if (trigger.only_full_words) { + pattern = `\\b${pattern}\\b`; + } const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i"); const test = regex.test(str); @@ -619,6 +642,10 @@ export class AutomodPlugin extends ZeppelinPlugin { * @return Matched regex pattern */ protected evaluateMatchRegexTrigger(trigger: TMatchRegexTrigger, str: string): null | string { + if (trigger.normalize) { + str = transliterate(str); + } + // TODO: Time limit regexes for (const pattern of trigger.patterns) { const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i"); From 48adb1df90dea92b0f1f1c6290b82e77ec525274 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:05:48 +0200 Subject: [PATCH 23/67] chore: clean up getInviteCodesInString --- backend/src/utils.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 2005f862..139f9d18 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -345,15 +345,7 @@ export function getUrlsInString(str: string, unique = false): url.URL[] { export function getInviteCodesInString(str: string): string[] { const inviteCodeRegex = /(?:discord.gg|discordapp.com\/invite)\/([a-z0-9]+)/gi; - const inviteCodes = []; - let match; - - // tslint:disable-next-line - while ((match = inviteCodeRegex.exec(str)) !== null) { - inviteCodes.push(match[1]); - } - - return inviteCodes; + return Array.from(str.matchAll(inviteCodeRegex)).map(m => m[1]); } export const unicodeEmojiRegex = emojiRegex(); From 42df230e7144a66bcf93b83de3662adfc523cace Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:06:26 +0200 Subject: [PATCH 24/67] automod: better formatting for matched content in log messages --- backend/src/plugins/Automod.ts | 143 +++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 53 deletions(-) diff --git a/backend/src/plugins/Automod.ts b/backend/src/plugins/Automod.ts index 55306c14..7fe773e2 100644 --- a/backend/src/plugins/Automod.ts +++ b/backend/src/plugins/Automod.ts @@ -2,7 +2,6 @@ import { PluginInfo, trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlu import * as t from "io-ts"; import { convertDelayStringToMS, - disableCodeBlocks, disableInlineCode, disableLinkPreviews, getEmojiInString, @@ -21,7 +20,7 @@ import { verboseChannelMention, } from "../utils"; import { configUtils, CooldownManager } from "knub"; -import { Invite, Member, TextChannel } from "eris"; +import { Member, TextChannel } from "eris"; import escapeStringRegexp from "escape-string-regexp"; import { SimpleCache } from "../SimpleCache"; import { Queue } from "../Queue"; @@ -51,25 +50,26 @@ type TextTriggerWithMultipleMatchTypes = { }; interface TriggerMatchResult { + trigger: string; type: string; } -interface MessageTextTriggerMatchResult extends TriggerMatchResult { +interface MessageTextTriggerMatchResult extends TriggerMatchResult { type: "message" | "embed"; str: string; userId: string; messageInfo: MessageInfo; - matchedContent?: string; + matchedValue: T; } -interface OtherTextTriggerMatchResult extends TriggerMatchResult { +interface OtherTextTriggerMatchResult extends TriggerMatchResult { type: "username" | "nickname" | "visiblename" | "customstatus"; str: string; userId: string; - matchedContent?: string; + matchedValue: T; } -type TextTriggerMatchResult = MessageTextTriggerMatchResult | OtherTextTriggerMatchResult; +type TextTriggerMatchResult = MessageTextTriggerMatchResult | OtherTextTriggerMatchResult; interface TextSpamTriggerMatchResult extends TriggerMatchResult { type: "textspam"; @@ -118,8 +118,7 @@ const MatchWordsTrigger = t.type({ match_custom_status: t.boolean, }); type TMatchWordsTrigger = t.TypeOf; -const defaultMatchWordsTrigger: TMatchWordsTrigger = { - words: [], +const defaultMatchWordsTrigger: Partial = { case_sensitive: false, only_full_words: true, normalize: false, @@ -732,7 +731,7 @@ export class AutomodPlugin extends ZeppelinPlugin { recentActionType: RecentActionType, trigger: TBaseTextSpamTrigger, msg: SavedMessage, - ): TextSpamTriggerMatchResult { + ): Partial { const since = moment.utc(msg.posted_at).valueOf() - convertDelayStringToMS(trigger.within); const recentActions = trigger.per_channel ? this.getMatchingRecentActions(recentActionType, `${msg.channel_id}-${msg.user_id}`, since) @@ -754,69 +753,85 @@ export class AutomodPlugin extends ZeppelinPlugin { return null; } - protected async matchMultipleTextTypesOnMessage( + protected async matchMultipleTextTypesOnMessage( trigger: TextTriggerWithMultipleMatchTypes, msg: SavedMessage, - cb, - ): Promise { + matchFn: (str: string) => T | Promise | null, + ): Promise>> { const messageInfo: MessageInfo = { channelId: msg.channel_id, messageId: msg.id }; const member = this.guild.members.get(msg.user_id); if (trigger.match_messages) { const str = msg.data.content; - const match = await cb(str); - if (match) return { type: "message", str, userId: msg.user_id, messageInfo, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "message", str, userId: msg.user_id, messageInfo, matchedValue: matchResult }; + } } if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) { const str = JSON.stringify(msg.data.embeds[0]); - const match = await cb(str); - if (match) return { type: "embed", str, userId: msg.user_id, messageInfo, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "embed", str, userId: msg.user_id, messageInfo, matchedValue: matchResult }; + } } if (trigger.match_visible_names) { const str = member.nick || msg.data.author.username; - const match = await cb(str); - if (match) return { type: "visiblename", str, userId: msg.user_id, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "visiblename", str, userId: msg.user_id, matchedValue: matchResult }; + } } if (trigger.match_usernames) { const str = `${msg.data.author.username}#${msg.data.author.discriminator}`; - const match = await cb(str); - if (match) return { type: "username", str, userId: msg.user_id, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "username", str, userId: msg.user_id, matchedValue: matchResult }; + } } if (trigger.match_nicknames && member.nick) { const str = member.nick; - const match = await cb(str); - if (match) return { type: "nickname", str, userId: msg.user_id, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "nickname", str, userId: msg.user_id, matchedValue: matchResult }; + } } // type 4 = custom status if (trigger.match_custom_status && member.game && member.game.type === 4) { const str = member.game.state; - const match = await cb(str); - if (match) return { type: "customstatus", str, userId: msg.user_id, matchedContent: match }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "customstatus", str, userId: msg.user_id, matchedValue: matchResult }; + } } return null; } - protected async matchMultipleTextTypesOnMember( + protected async matchMultipleTextTypesOnMember( trigger: TextTriggerWithMultipleMatchTypes, member: Member, - cb, - ): Promise { + matchFn: (str: string) => T | Promise | null, + ): Promise>> { if (trigger.match_usernames) { const str = `${member.user.username}#${member.user.discriminator}`; - const match = await cb(str); - if (match) return { type: "username", str, userId: member.id }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "username", str, userId: member.id, matchedValue: matchResult }; + } } if (trigger.match_nicknames && member.nick) { const str = member.nick; - const match = await cb(str); - if (match) return { type: "nickname", str, userId: member.id }; + const matchResult = await matchFn(str); + if (matchResult) { + return { type: "nickname", str, userId: member.id, matchedValue: matchResult }; + } } return null; @@ -836,63 +851,63 @@ export class AutomodPlugin extends ZeppelinPlugin { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_words, msg, str => { return this.evaluateMatchWordsTrigger(trigger.match_words, str); }); - if (match) return match; + if (match) return { ...match, trigger: "match_words" } as TextTriggerMatchResult; } if (trigger.match_regex) { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_regex, msg, str => { return this.evaluateMatchRegexTrigger(trigger.match_regex, str); }); - if (match) return match; + if (match) return { ...match, trigger: "match_regex" } as TextTriggerMatchResult; } if (trigger.match_invites) { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_invites, msg, str => { return this.evaluateMatchInvitesTrigger(trigger.match_invites, str); }); - if (match) return match; + if (match) return { ...match, trigger: "match_invites" } as TextTriggerMatchResult; } if (trigger.match_links) { const match = await this.matchMultipleTextTypesOnMessage(trigger.match_links, msg, str => { return this.evaluateMatchLinksTrigger(trigger.match_links, str); }); - if (match) return match; + if (match) return { ...match, trigger: "match_links" } as TextTriggerMatchResult; } if (trigger.message_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Message, trigger.message_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "message_spam" } as TextSpamTriggerMatchResult; } if (trigger.mention_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Mention, trigger.mention_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "mention_spam" } as TextSpamTriggerMatchResult; } if (trigger.link_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Link, trigger.link_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "link_spam" } as TextSpamTriggerMatchResult; } if (trigger.attachment_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Attachment, trigger.attachment_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "attachment_spam" } as TextSpamTriggerMatchResult; } if (trigger.emoji_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Emoji, trigger.emoji_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "emoji_spam" } as TextSpamTriggerMatchResult; } if (trigger.line_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Line, trigger.line_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "line_spam" } as TextSpamTriggerMatchResult; } if (trigger.character_spam) { const match = this.matchTextSpamTrigger(RecentActionType.Character, trigger.character_spam, msg); - if (match) return match; + if (match) return { ...match, trigger: "character_spam" } as TextSpamTriggerMatchResult; } } @@ -1102,6 +1117,9 @@ export class AutomodPlugin extends ZeppelinPlugin { }); } + /** + * Apply the actions of the specified rule on the matched message/member + */ protected async applyActionsOnMatch(rule: TRule, matchResult: AnyTriggerMatchResult) { if (rule.cooldown && this.checkAndUpdateCooldown(rule, matchResult)) { return; @@ -1335,6 +1353,7 @@ export class AutomodPlugin extends ZeppelinPlugin { } /** + * Check if the rule's on cooldown and bump its usage count towards the cooldown up * @return Whether the rule's on cooldown */ protected checkAndUpdateCooldown(rule: TRule, matchResult: AnyTriggerMatchResult): boolean { @@ -1371,15 +1390,17 @@ export class AutomodPlugin extends ZeppelinPlugin { return false; } + /** + * Returns a text summary for the match result for use in logs/alerts + */ protected async getMatchSummary(matchResult: AnyTriggerMatchResult): Promise { if (matchResult.type === "message" || matchResult.type === "embed") { const message = await this.savedMessages.find(matchResult.messageInfo.messageId); const channel = this.guild.channels.get(matchResult.messageInfo.channelId); const channelMention = channel ? verboseChannelMention(channel) : `\`#${message.channel_id}\``; - const matchedContent = disableInlineCode(matchResult.matchedContent); return trimPluginDescription(` - Matched \`${matchedContent}\` in message in ${channelMention}: + Matched ${this.getMatchedValueText(matchResult)} in message in ${channelMention}: ${messageSummary(message)} `); } else if (matchResult.type === "textspam" || matchResult.type === "raidspam") { @@ -1392,20 +1413,36 @@ export class AutomodPlugin extends ZeppelinPlugin { Matched spam: ${disableLinkPreviews(archiveUrl)} `); } else if (matchResult.type === "username") { - const matchedContent = disableInlineCode(matchResult.matchedContent); - return `Matched \`${matchedContent}\` in username: ${matchResult.str}`; + return `Matched ${this.getMatchedValueText(matchResult)} in username: ${matchResult.str}`; } else if (matchResult.type === "nickname") { - const matchedContent = disableInlineCode(matchResult.matchedContent); - return `Matched \`${matchedContent}\` in nickname: ${matchResult.str}`; + return `Matched ${this.getMatchedValueText(matchResult)} in nickname: ${matchResult.str}`; } else if (matchResult.type === "visiblename") { - const matchedContent = disableInlineCode(matchResult.matchedContent); - return `Matched \`${matchedContent}\` in visible name: ${matchResult.str}`; + return `Matched ${this.getMatchedValueText(matchResult)} in visible name: ${matchResult.str}`; } else if (matchResult.type === "customstatus") { - const matchedContent = disableInlineCode(matchResult.matchedContent); - return `Matched \`${matchedContent}\` in custom status: ${matchResult.str}`; + return `Matched ${this.getMatchedValueText(matchResult)} in custom status: ${matchResult.str}`; } } + /** + * Returns a formatted version of the matched value (word, regex pattern, link, etc.) for use in the match summary + */ + protected getMatchedValueText(matchResult: TextTriggerMatchResult): string | null { + if (matchResult.trigger === "match_words") { + return `word \`${disableInlineCode(matchResult.matchedValue)}\``; + } else if (matchResult.trigger === "match_regex") { + return `regex \`${disableInlineCode(matchResult.matchedValue)}\``; + } else if (matchResult.trigger === "match_invites") { + return `invite code \`${disableInlineCode(matchResult.matchedValue)}\``; + } else if (matchResult.trigger === "match_links") { + return `link \`${disableInlineCode(matchResult.matchedValue)}\``; + } + + return typeof matchResult.matchedValue === "string" ? `\`${disableInlineCode(matchResult.matchedValue)}\`` : null; + } + + /** + * Run automod actions on new messages + */ protected onMessageCreate(msg: SavedMessage) { if (msg.is_bot) return; From 51bfb376cf4f436f8390f3c17226a77813e39d9a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:06:48 +0200 Subject: [PATCH 25/67] tags: allow using --delete/-d with !tag to delete tags --- backend/src/plugins/Tags.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Tags.ts b/backend/src/plugins/Tags.ts index abcdbde9..812162dc 100644 --- a/backend/src/plugins/Tags.ts +++ b/backend/src/plugins/Tags.ts @@ -183,8 +183,20 @@ export class TagsPlugin extends ZeppelinPlugin { msg.channel.createMessage(successMessage(`Tag set! Use it with: \`${prefix}${args.tag}\``)); } - @d.command("tag", "") - async tagSourceCmd(msg: Message, args: { tag: string }) { + @d.command("tag", "", { + options: [ + { + name: "delete", + shortcut: "d", + isSwitch: true, + }, + ], + }) + async tagSourceCmd(msg: Message, args: { tag: string; delete?: boolean }) { + if (args.delete) { + return this.deleteTagCmd(msg, { tag: args.tag }); + } + const tag = await this.tags.find(args.tag); if (!tag) { msg.channel.createMessage(errorMessage("No tag with that name")); From 17f34ffeb7d415c229a23987e6bcb754b28e3948 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:13:48 +0200 Subject: [PATCH 26/67] utility: fix !search --export/-e only showing the first 15 results --- backend/src/plugins/Utility.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 645d94b5..34bd00df 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -84,6 +84,7 @@ const MAX_CLEAN_COUNT = 150; const MAX_CLEAN_TIME = 1 * DAYS; const CLEAN_COMMAND_DELETE_DELAY = 5000; const MEMBER_REFRESH_FREQUENCY = 10 * 60 * 1000; // How often to do a full member refresh when using !search or !roles --counts +const SEARCH_EXPORT_LIMIT = 1_000_000; const activeReloads: Map = new Map(); @@ -356,11 +357,11 @@ export class UtilityPlugin extends ZeppelinPlugin { ); } - const lastPage = Math.ceil(matchingMembers.length / SEARCH_RESULTS_PER_PAGE); + const lastPage = Math.max(1, Math.ceil(matchingMembers.length / perPage)); page = Math.min(lastPage, Math.max(1, page)); - const from = (page - 1) * SEARCH_RESULTS_PER_PAGE; - const to = Math.min(from + SEARCH_RESULTS_PER_PAGE, matchingMembers.length); + const from = (page - 1) * perPage; + const to = Math.min(from + perPage, matchingMembers.length); const pageMembers = matchingMembers.slice(from, to); @@ -447,7 +448,7 @@ export class UtilityPlugin extends ZeppelinPlugin { // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. // Just get the results and dump them in an archive. if (args.export) { - const results = await this.performMemberSearch(args, 1, Infinity); + const results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); if (results.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); } From 53e7c2f17d4e49ee71c29d23f1384006da4c1069 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:16:57 +0200 Subject: [PATCH 27/67] utility: add --bot/-bot to !search to search for bot members --- backend/src/plugins/Utility.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 34bd00df..0af2ef5b 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -92,6 +92,7 @@ type MemberSearchParams = { query?: string; role?: string; voice?: boolean; + bot?: boolean; sort?: string; "case-sensitive"?: boolean; }; @@ -326,6 +327,10 @@ export class UtilityPlugin extends ZeppelinPlugin { matchingMembers = matchingMembers.filter(m => m.voiceState.channelID != null); } + if (args.bot) { + matchingMembers = matchingMembers.filter(m => m.bot); + } + if (args.query) { const query = args["case-sensitive"] ? args.query.trimStart() : args.query.toLowerCase().trimStart(); @@ -380,15 +385,23 @@ export class UtilityPlugin extends ZeppelinPlugin { options: [ { name: "page", + shortcut: "p", type: "number", }, { name: "role", + shortcut: "r", type: "string", }, { name: "voice", - type: "bool", + shortcut: "v", + isSwitch: true, + }, + { + name: "bot", + shortcut: "b", + isSwitch: true, }, { name: "sort", @@ -426,9 +439,10 @@ export class UtilityPlugin extends ZeppelinPlugin { msg: Message, args: { query?: string; - role?: string; page?: number; + role?: string; voice?: boolean; + bot?: boolean; sort?: string; "case-sensitive"?: boolean; export?: boolean; From 23a9a5e800e92db7d802c558c675955df1889e3b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:22:25 +0200 Subject: [PATCH 28/67] utility: reply with archive url when !cleaning other channels --- backend/src/plugins/Utility.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 0af2ef5b..0e438c8f 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -594,6 +594,8 @@ export class UtilityPlugin extends ZeppelinPlugin { count: savedMessages.length, archiveUrl, }); + + return { archiveUrl }; } @d.command("clean", "", { @@ -706,10 +708,12 @@ export class UtilityPlugin extends ZeppelinPlugin { let responseMsg: Message; if (messagesToClean.length > 0) { - await this.cleanMessages(targetChannel, messagesToClean, msg.author); + const cleanResult = await this.cleanMessages(targetChannel, messagesToClean, msg.author); let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`; - if (targetChannel.id !== msg.channel.id) responseText += ` in <#${targetChannel.id}>`; + if (targetChannel.id !== msg.channel.id) { + responseText += ` in <#${targetChannel.id}>\n${cleanResult.archiveUrl}`; + } responseMsg = await msg.channel.createMessage(successMessage(responseText)); } else { From b47872bf874a1a0ec5717e742466046305dd76d7 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 22:23:02 +0200 Subject: [PATCH 29/67] utility: fix !clean response delete behaviour The !clean response is intended to be deleted after a delay when cleaning the current channel i.e. not specifying a different channel to clean. This behaviour was reversed, so the response got deleted when cleaning a different channel and stayed when cleaning the current channel. --- backend/src/plugins/Utility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 0e438c8f..b4153df0 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -720,7 +720,7 @@ export class UtilityPlugin extends ZeppelinPlugin { responseMsg = await msg.channel.createMessage(errorMessage(`Found no messages to clean!`)); } - if (targetChannel.id !== msg.channel.id) { + if (targetChannel.id === msg.channel.id) { // Delete the !clean command and the bot response if a different channel wasn't specified // (so as not to spam the cleaned channel with the command itself) setTimeout(() => { From 29d0bc3a181ca20a69b2a47804780354b6112a3b Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 23:38:11 +0200 Subject: [PATCH 30/67] typeorm: set migrationsDir to the dev folder The actual migrations are run based on the "migrations" array, so this only affects the migration creation command (which is always in dev). --- backend/ormconfig.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/ormconfig.js b/backend/ormconfig.js index 3cf907d9..b7ffe433 100644 --- a/backend/ormconfig.js +++ b/backend/ormconfig.js @@ -18,6 +18,7 @@ moment.tz.setDefault('UTC'); const entities = path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/data/entities/*.js')); const migrations = path.relative(process.cwd(), path.resolve(__dirname, 'dist/backend/src/migrations/*.js')); +const migrationsDir = path.relative(process.cwd(), path.resolve(__dirname, 'src/migrations')); module.exports = { type: "mysql", @@ -50,6 +51,6 @@ module.exports = { // Migrations migrations: [migrations], cli: { - migrationsDir: path.dirname(migrations) + migrationsDir, }, }; From 546835d421577a5eb17dd999c7eef4aad2eb7fa9 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 23:39:12 +0200 Subject: [PATCH 31/67] starboard: fix starboards accepting any emoji instead of just the specified one --- backend/src/plugins/Starboard.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/src/plugins/Starboard.ts b/backend/src/plugins/Starboard.ts index 8bcd1be8..d0056626 100644 --- a/backend/src/plugins/Starboard.ts +++ b/backend/src/plugins/Starboard.ts @@ -10,6 +10,7 @@ import { TDeepPartialProps, tNullable, tDeepPartial, + UnknownUser, } from "../utils"; import path from "path"; import moment from "moment-timezone"; @@ -181,13 +182,34 @@ export class StarboardPlugin extends ZeppelinPlugin { } } + const user = await this.resolveUser(userId); + if (user instanceof UnknownUser) return; + if (user.bot) return; + const config = this.getConfigForMemberIdAndChannelId(userId, msg.channel.id); - const applicableStarboards = Object.values(config.boards).filter(board => board.enabled); + const applicableStarboards = Object.values(config.boards) + .filter(board => board.enabled) + // Can't star messages in the starboard channel itself + .filter(board => board.channel_id !== msg.channel.id) + // Matching emoji + .filter(board => { + return board.star_emoji.some((boardEmoji: string) => { + if (emoji.id) { + // Custom emoji + const customEmojiMatch = boardEmoji.match(/^?$/); + if (customEmojiMatch) { + return customEmojiMatch[1] === emoji.id; + } + + return boardEmoji === emoji.id; + } else { + // Unicode emoji + return emoji.name === boardEmoji; + } + }); + }); for (const starboard of applicableStarboards) { - // Can't star messages in the starboard channel itself - if (msg.channel.id === starboard.channel_id) continue; - // Save reaction into the database await this.starboardReactions.createStarboardReaction(msg.id, userId).catch(); From d2a6cb1684cfb070cf40e165ad4c0d863099aacb Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sat, 30 Nov 2019 23:39:29 +0200 Subject: [PATCH 32/67] Add --exclusive/-e to !reaction_roles When reaction roles are set as exclusive, a user can only have 1 reaction role from that message. Others are removed automatically when picking a role if needed. --- backend/src/data/GuildReactionRoles.ts | 3 ++- backend/src/data/entities/ReactionRole.ts | 2 ++ ...145703039-AddIsExclusiveToReactionRoles.ts | 19 ++++++++++++++++ backend/src/plugins/ReactionRoles.ts | 22 ++++++++++++++++--- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 backend/src/migrations/1575145703039-AddIsExclusiveToReactionRoles.ts diff --git a/backend/src/data/GuildReactionRoles.ts b/backend/src/data/GuildReactionRoles.ts index 0f7196c2..c197bc1d 100644 --- a/backend/src/data/GuildReactionRoles.ts +++ b/backend/src/data/GuildReactionRoles.ts @@ -50,13 +50,14 @@ export class GuildReactionRoles extends BaseGuildRepository { await this.reactionRoles.delete(criteria); } - async add(channelId: string, messageId: string, emoji: string, roleId: string) { + async add(channelId: string, messageId: string, emoji: string, roleId: string, exclusive?: boolean) { await this.reactionRoles.insert({ guild_id: this.guildId, channel_id: channelId, message_id: messageId, emoji, role_id: roleId, + is_exclusive: Boolean(exclusive), }); } } diff --git a/backend/src/data/entities/ReactionRole.ts b/backend/src/data/entities/ReactionRole.ts index ebefd1a0..38cb256d 100644 --- a/backend/src/data/entities/ReactionRole.ts +++ b/backend/src/data/entities/ReactionRole.ts @@ -19,4 +19,6 @@ export class ReactionRole { emoji: string; @Column() role_id: string; + + @Column() is_exclusive: boolean; } diff --git a/backend/src/migrations/1575145703039-AddIsExclusiveToReactionRoles.ts b/backend/src/migrations/1575145703039-AddIsExclusiveToReactionRoles.ts new file mode 100644 index 00000000..141aeef3 --- /dev/null +++ b/backend/src/migrations/1575145703039-AddIsExclusiveToReactionRoles.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class AddIsExclusiveToReactionRoles1575145703039 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + "reaction_roles", + new TableColumn({ + name: "is_exclusive", + type: "tinyint", + unsigned: true, + default: 0, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("reaction_roles", "is_exclusive"); + } +} diff --git a/backend/src/plugins/ReactionRoles.ts b/backend/src/plugins/ReactionRoles.ts index 5bdc4c51..b07609d9 100644 --- a/backend/src/plugins/ReactionRoles.ts +++ b/backend/src/plugins/ReactionRoles.ts @@ -268,9 +268,17 @@ export class ReactionRolesPlugin extends ZeppelinPlugin { * :zep_twitch: = 473086848831455234 * :zep_ps4: = 543184300250759188 */ - @d.command("reaction_roles", " ") + @d.command("reaction_roles", " ", { + options: [ + { + name: "exclusive", + shortcut: "e", + isSwitch: true, + }, + ], + }) @d.permission("can_manage") - async reactionRolesCmd(msg: Message, args: { messageId: string; reactionRolePairs: string }) { + async reactionRolesCmd(msg: Message, args: { messageId: string; reactionRolePairs: string; exclusive?: boolean }) { const savedMessage = await this.savedMessages.find(args.messageId); if (!savedMessage) { msg.channel.createMessage(errorMessage("Unknown message")); @@ -331,7 +339,7 @@ export class ReactionRolesPlugin extends ZeppelinPlugin { // Save the new reaction roles to the database for (const pair of emojiRolePairs) { - await this.reactionRoles.add(channel.id, targetMessage.id, pair[0], pair[1]); + await this.reactionRoles.add(channel.id, targetMessage.id, pair[0], pair[1], args.exclusive); } // Apply the reactions themselves @@ -370,6 +378,14 @@ export class ReactionRolesPlugin extends ZeppelinPlugin { const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(msg.id, emoji.id || emoji.name); if (!matchingReactionRole) return; + // If the reaction role is exclusive, remove any other roles in the message first + if (matchingReactionRole.is_exclusive) { + const messageReactionRoles = await this.reactionRoles.getForMessage(msg.id); + for (const reactionRole of messageReactionRoles) { + this.addMemberPendingRoleChange(userId, "-", reactionRole.role_id); + } + } + this.addMemberPendingRoleChange(userId, "+", matchingReactionRole.role_id); } From c1cb5a4ed7f59550d821a6f9133d668704847417 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 00:37:46 +0200 Subject: [PATCH 33/67] search: add -ids switch to list result ids; fix reactions from other messages affecting search results --- backend/src/plugins/Utility.ts | 37 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index b4153df0..2b1f2e4c 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -80,6 +80,8 @@ type TConfigSchema = t.TypeOf; const { performance } = require("perf_hooks"); const SEARCH_RESULTS_PER_PAGE = 15; +const SEARCH_ID_RESULTS_PER_PAGE = 50; + const MAX_CLEAN_COUNT = 150; const MAX_CLEAN_TIME = 1 * DAYS; const CLEAN_COMMAND_DELETE_DELAY = 5000; @@ -417,6 +419,10 @@ export class UtilityPlugin extends ZeppelinPlugin { shortcut: "e", isSwitch: true, }, + { + name: "ids", + isSwitch: true, + }, ], extra: { info: { @@ -446,9 +452,10 @@ export class UtilityPlugin extends ZeppelinPlugin { sort?: string; "case-sensitive"?: boolean; export?: boolean; + ids?: boolean; }, ) { - const formatSearchResultLines = (members: Member[]) => { + const formatSearchResultList = (members: Member[]): string => { const longestId = members.reduce((longest, member) => Math.max(longest, member.id.length), 0); const lines = members.map(member => { const paddedId = member.id.padEnd(longestId, " "); @@ -456,7 +463,11 @@ export class UtilityPlugin extends ZeppelinPlugin { if (member.nick) line += ` (${member.nick})`; return line; }); - return lines; + return lines.join("\n"); + }; + + const formatSearchResultIdList = (members: Member[]): string => { + return members.map(m => m.id).join(" "); }; // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. @@ -467,12 +478,13 @@ export class UtilityPlugin extends ZeppelinPlugin { return this.sendErrorMessage(msg.channel, "No results found"); } - const resultLines = formatSearchResultLines(results.results); + const resultList = args.ids ? formatSearchResultIdList(results.results) : formatSearchResultList(results.results); + const archiveId = await this.archives.create( trimLines(` Search results (total ${results.totalResults}): - ${resultLines.join("\n")} + ${resultList} `), moment().add(1, "hour"), ); @@ -491,6 +503,8 @@ export class UtilityPlugin extends ZeppelinPlugin { let clearReactionsFn = null; let clearReactionsTimeout = null; + const perPage = args.ids ? SEARCH_ID_RESULTS_PER_PAGE : SEARCH_RESULTS_PER_PAGE; + const loadSearchPage = async page => { if (searching) return; searching = true; @@ -505,23 +519,27 @@ export class UtilityPlugin extends ZeppelinPlugin { searchMsgPromise.then(m => (originalSearchMsg = m)); } - const searchResult = await this.performMemberSearch(args, page, SEARCH_RESULTS_PER_PAGE); + const searchResult = await this.performMemberSearch(args, page, perPage); if (searchResult.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); } const resultWord = searchResult.totalResults === 1 ? "matching member" : "matching members"; const headerText = - searchResult.totalResults > SEARCH_RESULTS_PER_PAGE + searchResult.totalResults > perPage ? trimLines(` **Page ${searchResult.page}** (${searchResult.from}-${searchResult.to}) (total ${searchResult.totalResults}) `) : `Found ${searchResult.totalResults} ${resultWord}`; - const lines = formatSearchResultLines(searchResult.results); + + const resultList = args.ids + ? formatSearchResultIdList(searchResult.results) + : formatSearchResultList(searchResult.results); + const result = trimLines(` ${headerText} \`\`\`js - ${lines.join("\n")} + ${resultList} \`\`\` `); @@ -529,7 +547,7 @@ export class UtilityPlugin extends ZeppelinPlugin { searchMsg.edit(result); // Set up pagination reactions if needed. The reactions are cleared after a timeout. - if (searchResult.totalResults > SEARCH_RESULTS_PER_PAGE) { + if (searchResult.totalResults > perPage) { if (!hasReactions) { hasReactions = true; searchMsg.addReaction("⬅"); @@ -537,6 +555,7 @@ export class UtilityPlugin extends ZeppelinPlugin { searchMsg.addReaction("🔄"); const removeListenerFn = this.on("messageReactionAdd", (rMsg: Message, emoji, userId) => { + if (rMsg.id !== searchMsg.id) return; if (userId !== msg.author.id) return; if (!["⬅", "➡", "🔄"].includes(emoji.name)) return; From a0edd962f3af3292ecd004b265cb8a8cb0e13b1f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 01:01:20 +0200 Subject: [PATCH 34/67] Add support for regex in !search via -regex/-re --- backend/package-lock.json | 6 ++++ backend/package.json | 1 + backend/src/plugins/Utility.ts | 56 ++++++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 2f4e35e9..b041a984 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -598,6 +598,12 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, + "@types/safe-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/safe-regex/-/safe-regex-1.1.2.tgz", + "integrity": "sha512-wuS9LVpgIiTYaGKd+s6Dj0kRXBkttaXjVxzaXmviCACi8RO+INPayND+VNjAcall/l1Jkyhh9lyPfKW/aP/Yug==", + "dev": true + }, "@types/serve-static": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", diff --git a/backend/package.json b/backend/package.json index 08e2a049..6342e440 100644 --- a/backend/package.json +++ b/backend/package.json @@ -67,6 +67,7 @@ "@types/passport": "^1.0.0", "@types/passport-oauth2": "^1.4.8", "@types/passport-strategy": "^0.2.35", + "@types/safe-regex": "^1.1.2", "@types/tmp": "0.0.33", "ava": "^2.4.0", "rimraf": "^2.6.2", diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 2b1f2e4c..5d88c29e 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -58,6 +58,8 @@ import LCL from "last-commit-log"; import * as t from "io-ts"; import { ICommandDefinition } from "knub-command-manager"; import path from "path"; +import escapeStringRegexp from "escape-string-regexp"; +import safeRegex from "safe-regex"; const ConfigSchema = t.type({ can_roles: t.boolean, @@ -97,8 +99,11 @@ type MemberSearchParams = { bot?: boolean; sort?: string; "case-sensitive"?: boolean; + regex?: boolean; }; +class SearchError extends Error {} + export class UtilityPlugin extends ZeppelinPlugin { public static pluginName = "utility"; public static configSchema = ConfigSchema; @@ -334,17 +339,22 @@ export class UtilityPlugin extends ZeppelinPlugin { } if (args.query) { - const query = args["case-sensitive"] ? args.query.trimStart() : args.query.toLowerCase().trimStart(); + let queryRegex: RegExp; + if (args.regex) { + queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "i" : ""); + } else { + queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "i" : ""); + } + + if (!safeRegex(queryRegex)) { + throw new SearchError("Unsafe/too complex regex (star depth is limited to 1)"); + } matchingMembers = matchingMembers.filter(member => { - const nick = args["case-sensitive"] ? member.nick : member.nick && member.nick.toLowerCase(); + if (member.nick && member.nick.match(queryRegex)) return true; - const fullUsername = args["case-sensitive"] - ? `${member.user.username}#${member.user.discriminator}` - : `${member.user.username}#${member.user.discriminator}`.toLowerCase(); - - if (nick && nick.indexOf(query) !== -1) return true; - if (fullUsername.indexOf(query) !== -1) return true; + const fullUsername = `${member.user.username}#${member.user.discriminator}`; + if (fullUsername.match(queryRegex)) return true; return false; }); @@ -423,6 +433,11 @@ export class UtilityPlugin extends ZeppelinPlugin { name: "ids", isSwitch: true, }, + { + name: "regex", + shortcut: "re", + isSwitch: true, + }, ], extra: { info: { @@ -453,6 +468,7 @@ export class UtilityPlugin extends ZeppelinPlugin { "case-sensitive"?: boolean; export?: boolean; ids?: boolean; + regex?: boolean; }, ) { const formatSearchResultList = (members: Member[]): string => { @@ -473,7 +489,17 @@ export class UtilityPlugin extends ZeppelinPlugin { // If we're exporting the results, we don't need all the fancy schmancy pagination stuff. // Just get the results and dump them in an archive. if (args.export) { - const results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); + let results; + try { + results = await this.performMemberSearch(args, 1, SEARCH_EXPORT_LIMIT); + } catch (e) { + if (e instanceof SearchError) { + return this.sendErrorMessage(msg.channel, e.message); + } + + throw e; + } + if (results.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); } @@ -519,7 +545,17 @@ export class UtilityPlugin extends ZeppelinPlugin { searchMsgPromise.then(m => (originalSearchMsg = m)); } - const searchResult = await this.performMemberSearch(args, page, perPage); + let searchResult; + try { + searchResult = await this.performMemberSearch(args, page, perPage); + } catch (e) { + if (e instanceof SearchError) { + return this.sendErrorMessage(msg.channel, e.message); + } + + throw e; + } + if (searchResult.totalResults === 0) { return this.sendErrorMessage(msg.channel, "No results found"); } From 83f49f38059c40960a364588774e9f12b8439fef Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 02:05:33 +0200 Subject: [PATCH 35/67] Fix --- .../1573158035867-AddTypeAndPermissionsToApiPermissions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts index d6de7ffd..0e057203 100644 --- a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts +++ b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts @@ -2,8 +2,11 @@ import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeor export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.dropPrimaryKey("api_permissions"); - await queryRunner.dropIndex("api_permissions", "IDX_5e371749d4cb4a5191f35e26f6"); + try { + await queryRunner.dropPrimaryKey("api_permissions"); + } catch (e) {} // tslint:disable-line + + await queryRunner.dropIndex("api_permissions", new TableIndex({ columnNames: ["user_id"] })); await queryRunner.addColumn( "api_permissions", From 5489840bb4cd98b6ad3ef268f3e94677517d8463 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 02:07:22 +0200 Subject: [PATCH 36/67] Fix 2 --- .../1573158035867-AddTypeAndPermissionsToApiPermissions.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts index 0e057203..1087bf76 100644 --- a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts +++ b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts @@ -2,10 +2,6 @@ import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeor export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - try { - await queryRunner.dropPrimaryKey("api_permissions"); - } catch (e) {} // tslint:disable-line - await queryRunner.dropIndex("api_permissions", new TableIndex({ columnNames: ["user_id"] })); await queryRunner.addColumn( From 0687e67bc588f0c1b478159d95ea355373a34d16 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 02:09:48 +0200 Subject: [PATCH 37/67] Fix 3 --- ...573158035867-AddTypeAndPermissionsToApiPermissions.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts index 1087bf76..c921a65a 100644 --- a/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts +++ b/backend/src/migrations/1573158035867-AddTypeAndPermissionsToApiPermissions.ts @@ -2,7 +2,14 @@ import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from "typeor export class AddTypeAndPermissionsToApiPermissions1573158035867 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.dropIndex("api_permissions", new TableIndex({ columnNames: ["user_id"] })); + try { + await queryRunner.dropPrimaryKey("api_permissions"); + } catch (e) {} // tslint:disable-line + + const table = await queryRunner.getTable("api_permissions"); + if (table.indices.length) { + await queryRunner.dropIndex("api_permissions", table.indices[0]); + } await queryRunner.addColumn( "api_permissions", From 698174a584ad1823c1f536871d91e8ee051cd811 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 02:40:50 +0200 Subject: [PATCH 38/67] vcalert: use overloads instead of double optional parameter Knub-command-manager doesn't support more than one optional parameter at the moment. --- backend/src/plugins/LocateUser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/plugins/LocateUser.ts b/backend/src/plugins/LocateUser.ts index 2ac5f938..1bd28d04 100644 --- a/backend/src/plugins/LocateUser.ts +++ b/backend/src/plugins/LocateUser.ts @@ -90,7 +90,8 @@ export class LocatePlugin extends ZeppelinPlugin { sendWhere(this.guild, member, msg.channel, `${msg.member.mention} |`); } - @d.command("vcalert", " [duration:delay] [reminder:string$]", { + @d.command("vcalert", " ", { + overloads: [" ", ""], aliases: ["vca"], extra: { info: { From 26c460e67a2632b034f0214847c178aaa48d551f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:57:17 +0200 Subject: [PATCH 39/67] trimPluginDescription: use first line's indentation instead --- backend/src/plugins/ZeppelinPlugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index f2cc6481..ce593a0b 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -53,8 +53,8 @@ export interface CommandInfo { export function trimPluginDescription(str) { const emptyLinesTrimmed = trimEmptyStartEndLines(str); const lines = emptyLinesTrimmed.split("\n"); - const lastLineIndentation = (lines[lines.length - 1].match(/^ +/g) || [""])[0].length; - return trimIndents(emptyLinesTrimmed, lastLineIndentation); + const firstLineIndentation = (lines[0].match(/^ +/g) || [""])[0].length; + return trimIndents(emptyLinesTrimmed, firstLineIndentation); } const inviteCache = new SimpleCache>(10 * MINUTES, 200); From 56fb432c7c3bf12b29b783aa259691b219f08459 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:57:35 +0200 Subject: [PATCH 40/67] Initial work on stats --- backend/src/data/GuildStats.ts | 30 ++++ backend/src/data/entities/StatValue.ts | 20 +++ .../1575199835233-CreateStatsTable.ts | 59 +++++++ backend/src/plugins/Stats.ts | 155 ++++++++++++++++++ backend/src/utils.test.ts | 34 +++- backend/src/utils.ts | 53 ++++++ 6 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 backend/src/data/GuildStats.ts create mode 100644 backend/src/data/entities/StatValue.ts create mode 100644 backend/src/migrations/1575199835233-CreateStatsTable.ts create mode 100644 backend/src/plugins/Stats.ts diff --git a/backend/src/data/GuildStats.ts b/backend/src/data/GuildStats.ts new file mode 100644 index 00000000..46c7497b --- /dev/null +++ b/backend/src/data/GuildStats.ts @@ -0,0 +1,30 @@ +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { connection } from "./db"; +import { getRepository, Repository } from "typeorm"; +import { StatValue } from "./entities/StatValue"; + +export class GuildStats extends BaseGuildRepository { + private stats: Repository; + + constructor(guildId) { + super(guildId); + this.stats = getRepository(StatValue); + } + + async saveValue(source: string, key: string, value: number): Promise { + await this.stats.insert({ + guild_id: this.guildId, + source, + key, + value, + }); + } + + async deleteOldValues(source: string, cutoff: string): Promise { + await this.stats + .createQueryBuilder() + .where("source = :source", { source }) + .andWhere("created_at < :cutoff", { cutoff }) + .delete(); + } +} diff --git a/backend/src/data/entities/StatValue.ts b/backend/src/data/entities/StatValue.ts new file mode 100644 index 00000000..78978a28 --- /dev/null +++ b/backend/src/data/entities/StatValue.ts @@ -0,0 +1,20 @@ +import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm"; + +@Entity("stats") +export class StatValue { + @Column() + @PrimaryColumn() + id: string; + + @Column() + guild_id: string; + + @Column() + source: string; + + @Column() key: string; + + @Column() value: number; + + @Column() created_at: string; +} diff --git a/backend/src/migrations/1575199835233-CreateStatsTable.ts b/backend/src/migrations/1575199835233-CreateStatsTable.ts new file mode 100644 index 00000000..d6c6a8d7 --- /dev/null +++ b/backend/src/migrations/1575199835233-CreateStatsTable.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateStatsTable1575199835233 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "stats", + columns: [ + { + name: "id", + type: "bigint", + unsigned: true, + isPrimary: true, + generationStrategy: "increment", + }, + { + name: "guild_id", + type: "bigint", + unsigned: true, + }, + { + name: "source", + type: "varchar", + length: "64", + collation: "ascii_bin", + }, + { + name: "key", + type: "varchar", + length: "64", + collation: "ascii_bin", + }, + { + name: "value", + type: "integer", + unsigned: true, + }, + { + name: "created_at", + type: "datetime", + default: "NOW()", + }, + ], + indices: [ + { + columnNames: ["guild_id", "source", "key"], + }, + { + columnNames: ["created_at"], + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("stats"); + } +} diff --git a/backend/src/plugins/Stats.ts b/backend/src/plugins/Stats.ts new file mode 100644 index 00000000..63c12856 --- /dev/null +++ b/backend/src/plugins/Stats.ts @@ -0,0 +1,155 @@ +import { ZeppelinPlugin } from "./ZeppelinPlugin"; +import * as t from "io-ts"; +import { convertDelayStringToMS, DAYS, HOURS, tAlphanumeric, tDateTime, tDeepPartial, tDelayString } from "../utils"; +import { IPluginOptions } from "knub"; +import moment from "moment-timezone"; +import { GuildStats } from "../data/GuildStats"; +import { Message } from "eris"; +import escapeStringRegexp from "escape-string-regexp"; +import { SavedMessage } from "../data/entities/SavedMessage"; +import { GuildSavedMessages } from "../data/GuildSavedMessages"; + +const tBaseSource = t.type({ + name: tAlphanumeric, + track: t.boolean, + retention_period: tDelayString, +}); + +const tMemberMessagesSource = t.intersection([ + tBaseSource, + t.type({ + type: t.literal("member_messages"), + }), +]); +type TMemberMessagesSource = t.TypeOf; + +const tChannelMessagesSource = t.intersection([ + tBaseSource, + t.type({ + type: t.literal("channel_messages"), + }), +]); +type TChannelMessagesSource = t.TypeOf; + +const tKeywordsSource = t.intersection([ + tBaseSource, + t.type({ + type: t.literal("keywords"), + keywords: t.array(t.string), + }), +]); +type TKeywordsSource = t.TypeOf; + +const tSource = t.union([tMemberMessagesSource, tChannelMessagesSource, tKeywordsSource]); +type TSource = t.TypeOf; + +const tConfigSchema = t.type({ + sources: t.record(tAlphanumeric, tSource), +}); + +type TConfigSchema = t.TypeOf; +const tPartialConfigSchema = tDeepPartial(tConfigSchema); + +const DEFAULT_RETENTION_PERIOD = "4w"; + +export class StatsPlugin extends ZeppelinPlugin { + public static pluginName = "stats"; + public static configSchema = tConfigSchema; + public static showInDocs = false; + + protected stats: GuildStats; + protected savedMessages: GuildSavedMessages; + + private onMessageCreateFn; + private cleanStatsInterval; + + public static getStaticDefaultOptions(): IPluginOptions { + return { + config: { + sources: {}, + }, + }; + } + + protected static preprocessStaticConfig(config: t.TypeOf) { + // TODO: Limit min period, min period start date + + if (config.sources) { + for (const [key, source] of Object.entries(config.sources)) { + source.name = key; + + if (source.track == null) { + source.track = true; + } + + if (source.retention_period == null) { + source.retention_period = DEFAULT_RETENTION_PERIOD; + } + } + } + + return config; + } + + protected onLoad() { + this.stats = GuildStats.getGuildInstance(this.guildId); + this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId); + + this.onMessageCreateFn = this.savedMessages.events.on("create", msg => this.onMessageCreate(msg)); + + this.cleanOldStats(); + this.cleanStatsInterval = setInterval(() => this.cleanOldStats(), 1 * DAYS); + } + + protected onUnload() { + this.savedMessages.events.off("create", this.onMessageCreateFn); + clearInterval(this.cleanStatsInterval); + } + + protected async cleanOldStats() { + const config = this.getConfig(); + for (const source of Object.values(config.sources)) { + const cutoffMS = convertDelayStringToMS(source.retention_period); + const cutoff = moment() + .subtract(cutoffMS, "ms") + .format("YYYY-MM-DD HH:mm:ss"); + await this.stats.deleteOldValues(source.name, cutoff); + } + } + + protected saveMemberMessagesStats(source: TMemberMessagesSource, msg: SavedMessage) { + this.stats.saveValue(source.name, msg.user_id, 1); + } + + protected saveChannelMessagesStats(source: TChannelMessagesSource, msg: SavedMessage) { + this.stats.saveValue(source.name, msg.channel_id, 1); + } + + protected saveKeywordsStats(source: TKeywordsSource, msg: SavedMessage) { + const content = msg.data.content; + if (!content) return; + + for (const keyword of source.keywords) { + const regex = new RegExp(`\\b${escapeStringRegexp(keyword)}\\b`, "i"); + if (content.match(regex)) { + this.stats.saveValue(source.name, "keyword", 1); + break; + } + } + } + + onMessageCreate(msg: SavedMessage) { + const config = this.getConfigForMemberIdAndChannelId(msg.user_id, msg.channel_id); + for (const source of Object.values(config.sources)) { + if (!source.track) continue; + + if (source.type === "member_messages") { + this.saveMemberMessagesStats(source, msg); + } else if (source.type === "channel_messages") { + this.saveChannelMessagesStats(source, msg); + } else if (source.type === "keywords") { + this.saveKeywordsStats(source, msg); + } + } + } +} diff --git a/backend/src/utils.test.ts b/backend/src/utils.test.ts index cc0eca24..345c32a3 100644 --- a/backend/src/utils.test.ts +++ b/backend/src/utils.test.ts @@ -1,21 +1,47 @@ -import { getUrlsInString } from "./utils"; +import { convertDelayStringToMS, convertMSToDelayString, getUrlsInString } from "./utils"; import test from "ava"; -test("Detects full links", t => { +test("getUrlsInString(): detects full links", t => { const urls = getUrlsInString("foo https://google.com/ bar"); t.is(urls.length, 1); t.is(urls[0].hostname, "google.com"); }); -test("Detects partial links", t => { +test("getUrlsInString(): detects partial links", t => { const urls = getUrlsInString("foo google.com bar"); t.is(urls.length, 1); t.is(urls[0].hostname, "google.com"); }); -test("Detects subdomains", t => { +test("getUrlsInString(): detects subdomains", t => { const urls = getUrlsInString("foo photos.google.com bar"); t.is(urls.length, 1); t.is(urls[0].hostname, "photos.google.com"); }); + +test("delay strings: basic support", t => { + const delayString = "2w4d7h32m17s"; + const expected = 1_582_337_000; + t.is(convertDelayStringToMS(delayString), expected); +}); + +test("delay strings: default unit (minutes)", t => { + t.is(convertDelayStringToMS("10"), 10 * 60 * 1000); +}); + +test("delay strings: custom default unit", t => { + t.is(convertDelayStringToMS("10", "s"), 10 * 1000); +}); + +test("delay strings: reverse conversion", t => { + const ms = 1_582_337_020; + const expected = "2w4d7h32m17s20x"; + t.is(convertMSToDelayString(ms), expected); +}); + +test("delay strings: reverse conversion (conservative)", t => { + const ms = 1_209_600_000; + const expected = "2w"; + t.is(convertMSToDelayString(ms), expected); +}); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 139f9d18..5fa40b93 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -29,6 +29,9 @@ import tmp from "tmp"; import { logger, waitForReaction } from "knub"; import { SavedMessage } from "./data/entities/SavedMessage"; import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; +import { either } from "fp-ts/lib/Either"; +import safeRegex from "safe-regex"; +import moment from "moment-timezone"; const delayStringMultipliers = { w: 1000 * 60 * 60 * 24 * 7, @@ -36,6 +39,7 @@ const delayStringMultipliers = { h: 1000 * 60 * 60, m: 1000 * 60, s: 1000, + x: 1, }; export const MS = 1; @@ -184,6 +188,40 @@ export function dropPropertiesByName(obj, propName) { } } +export const tAlphanumeric = new t.Type( + "tAlphanumeric", + (s): s is string => typeof s === "string", + (from, to) => + either.chain(t.string.validate(from, to), s => { + return s.match(/\W/) ? t.failure(from, to, "String must be alphanumeric") : t.success(s); + }), + s => s, +); + +export const tDateTime = new t.Type( + "tDateTime", + (s): s is string => typeof s === "string", + (from, to) => + either.chain(t.string.validate(from, to), s => { + const parsed = + s.length === 10 ? moment(s, "YYYY-MM-DD") : s.length === 19 ? moment(s, "YYYY-MM-DD HH:mm:ss") : null; + + return parsed && parsed.isValid() ? t.success(s) : t.failure(from, to, "Invalid datetime"); + }), + s => s, +); + +export const tDelayString = new t.Type( + "tDelayString", + (s): s is string => typeof s === "string", + (from, to) => + either.chain(t.string.validate(from, to), s => { + const ms = convertDelayStringToMS(s); + return ms === null ? t.failure(from, to, "Invalid delay string") : t.success(s); + }), + s => s, +); + /** * Turns a "delay string" such as "1h30m" to milliseconds */ @@ -208,6 +246,21 @@ export function convertDelayStringToMS(str, defaultUnit = "m"): number { return ms; } +export function convertMSToDelayString(ms: number): string { + let result = ""; + let remaining = ms; + for (const [abbr, multiplier] of Object.entries(delayStringMultipliers)) { + if (multiplier <= remaining) { + const amount = Math.floor(remaining / multiplier); + result += `${amount}${abbr}`; + remaining -= amount * multiplier; + } + + if (remaining === 0) break; + } + return result; +} + export function successMessage(str) { return `<:zep_check:650361014180904971> ${str}`; } From 646156344aec399c15dfc38280c66f019c91662a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:59:57 +0200 Subject: [PATCH 41/67] automod: move actions taken after summary in log message --- backend/src/data/DefaultLogMessages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index 3bd715be..b9ac5a53 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -58,5 +58,5 @@ "POSTED_SCHEDULED_MESSAGE": "\uD83D\uDCE8 Posted scheduled message (`{messageId}`) to {channelMention(channel)} as scheduled by {userMention(author)}", "BOT_ALERT": "⚠ {tmplEval(body)}", - "AUTOMOD_ACTION": "\uD83E\uDD16 Automod rule **{rule}** triggered by {userMention(user)}. Actions taken: **{actionsTaken}**\n{matchSummary}" + "AUTOMOD_ACTION": "\uD83E\uDD16 Automod rule **{rule}** triggered by {userMention(user)}\n{matchSummary}\nActions taken: **{actionsTaken}**" } From 2ff65e89fd37d68031ead15d50f5937cad9538c0 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 1 Dec 2019 23:23:34 +0200 Subject: [PATCH 42/67] Add repeat options for scheduled posts --- backend/src/data/DefaultLogMessages.json | 2 + backend/src/data/GuildScheduledPosts.ts | 4 + backend/src/data/LogType.ts | 3 + backend/src/data/entities/ScheduledPost.ts | 9 + ...079526-AddRepeatColumnsToScheduledPosts.ts | 31 ++ backend/src/plugins/Post.ts | 383 ++++++++++++------ backend/src/utils.ts | 1 + 7 files changed, 320 insertions(+), 113 deletions(-) create mode 100644 backend/src/migrations/1575230079526-AddRepeatColumnsToScheduledPosts.ts diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index b9ac5a53..b5eded54 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -55,6 +55,8 @@ "MEMBER_MUTE_REJOIN": "⚠ Reapplied active mute for {userMention(member)} on rejoin", "SCHEDULED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {date} at {time} (UTC)", + "SCHEDULED_REPEATED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} on {date} at {time} (UTC), repeated {repeatDetails}", + "REPEATED_MESSAGE": "⏰ {userMention(author)} scheduled a message to be posted to {channelMention(channel)} {repeatDetails}", "POSTED_SCHEDULED_MESSAGE": "\uD83D\uDCE8 Posted scheduled message (`{messageId}`) to {channelMention(channel)} as scheduled by {userMention(author)}", "BOT_ALERT": "⚠ {tmplEval(body)}", diff --git a/backend/src/data/GuildScheduledPosts.ts b/backend/src/data/GuildScheduledPosts.ts index 0e37426e..af621434 100644 --- a/backend/src/data/GuildScheduledPosts.ts +++ b/backend/src/data/GuildScheduledPosts.ts @@ -38,4 +38,8 @@ export class GuildScheduledPosts extends BaseGuildRepository { guild_id: this.guildId, }); } + + async update(id: number, data: Partial) { + await this.scheduledPosts.update(id, data); + } } diff --git a/backend/src/data/LogType.ts b/backend/src/data/LogType.ts index 1be6580b..31798b78 100644 --- a/backend/src/data/LogType.ts +++ b/backend/src/data/LogType.ts @@ -59,4 +59,7 @@ export enum LogType { BOT_ALERT, AUTOMOD_ACTION, + + SCHEDULED_REPEATED_MESSAGE, + REPEATED_MESSAGE, } diff --git a/backend/src/data/entities/ScheduledPost.ts b/backend/src/data/entities/ScheduledPost.ts index e5592f51..3295c6b1 100644 --- a/backend/src/data/entities/ScheduledPost.ts +++ b/backend/src/data/entities/ScheduledPost.ts @@ -22,5 +22,14 @@ export class ScheduledPost { @Column() post_at: string; + /** + * How often to post the message, in milliseconds + */ + @Column() repeat_interval: number; + + @Column() repeat_until: string; + + @Column() repeat_times: number; + @Column() enable_mentions: boolean; } diff --git a/backend/src/migrations/1575230079526-AddRepeatColumnsToScheduledPosts.ts b/backend/src/migrations/1575230079526-AddRepeatColumnsToScheduledPosts.ts new file mode 100644 index 00000000..4a1b735b --- /dev/null +++ b/backend/src/migrations/1575230079526-AddRepeatColumnsToScheduledPosts.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class AddRepeatColumnsToScheduledPosts1575230079526 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns("scheduled_posts", [ + new TableColumn({ + name: "repeat_interval", + type: "integer", + unsigned: true, + isNullable: true, + }), + new TableColumn({ + name: "repeat_until", + type: "datetime", + isNullable: true, + }), + new TableColumn({ + name: "repeat_times", + type: "integer", + unsigned: true, + isNullable: true, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("scheduled_posts", "repeat_interval"); + await queryRunner.dropColumn("scheduled_posts", "repeat_until"); + await queryRunner.dropColumn("scheduled_posts", "repeat_times"); + } +} diff --git a/backend/src/plugins/Post.ts b/backend/src/plugins/Post.ts index 348e6f33..9633e5b4 100644 --- a/backend/src/plugins/Post.ts +++ b/backend/src/plugins/Post.ts @@ -14,6 +14,9 @@ import { createChunkedMessage, stripObjectToScalars, isValidEmbed, + MINUTES, + StrictMessageContent, + DAYS, } from "../utils"; import { GuildSavedMessages } from "../data/GuildSavedMessages"; import { ZeppelinPlugin } from "./ZeppelinPlugin"; @@ -24,6 +27,7 @@ import moment, { Moment } from "moment-timezone"; import { GuildLogs } from "../data/GuildLogs"; import { LogType } from "../data/LogType"; import * as t from "io-ts"; +import humanizeDuration from "humanize-duration"; const ConfigSchema = t.type({ can_post: t.boolean, @@ -34,9 +38,13 @@ const fsp = fs.promises; const COLOR_MATCH_REGEX = /^#?([0-9a-f]{6})$/; -const SCHEDULED_POST_CHECK_INTERVAL = 15 * SECONDS; +const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS; const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50; +const MIN_REPEAT_TIME = 5 * MINUTES; +const MAX_REPEAT_TIME = 100 * 365 * DAYS; +const MAX_REPEAT_UNTIL = moment().add(100, "years"); + export class PostPlugin extends ZeppelinPlugin { public static pluginName = "post"; public static configSchema = ConfigSchema; @@ -143,17 +151,25 @@ export class PostPlugin extends ZeppelinPlugin { } protected parseScheduleTime(str): Moment { - const dtMatch = str.match(/^\d{4}-\d{2}-\d{2} \d{1,2}:\d{1,2}(:\d{1,2})?$/); - if (dtMatch) { - const dt = moment(str, dtMatch[1] ? "YYYY-MM-DD H:m:s" : "YYYY-MM-DD H:m"); - return dt; + const dt1 = moment(str, "YYYY-MM-DD HH:mm:ss"); + if (dt1 && dt1.isValid()) return dt1; + + const dt2 = moment(str, "YYYY-MM-DD HH:mm"); + if (dt2 && dt2.isValid()) return dt2; + + const date = moment(str, "YYYY-MM-DD"); + if (date && date.isValid()) return date; + + const t1 = moment(str, "HH:mm:ss"); + if (t1 && t1.isValid()) { + if (t1.isBefore(moment())) t1.add(1, "day"); + return t1; } - const tMatch = str.match(/^\d{1,2}:\d{1,2}(:\d{1,2})?$/); - if (tMatch) { - const dt = moment(str, tMatch[1] ? "H:m:s" : "H:m"); - if (dt.isBefore(moment())) dt.add(1, "day"); - return dt; + const t2 = moment(str, "HH:mm"); + if (t2 && t2.isValid()) { + if (t2.isBefore(moment())) t2.add(1, "day"); + return t2; } const delayStringMS = convertDelayStringToMS(str, "m"); @@ -195,12 +211,208 @@ export class PostPlugin extends ZeppelinPlugin { } } - await this.scheduledPosts.delete(post.id); + let shouldClear = true; + + if (post.repeat_interval) { + const nextPostAt = moment().add(post.repeat_interval, "ms"); + + if (post.repeat_until) { + const repeatUntil = moment(post.repeat_until, DBDateFormat); + if (nextPostAt.isSameOrBefore(repeatUntil)) { + await this.scheduledPosts.update(post.id, { + post_at: nextPostAt.format(DBDateFormat), + }); + shouldClear = false; + } + } else if (post.repeat_times) { + if (post.repeat_times > 1) { + await this.scheduledPosts.update(post.id, { + post_at: nextPostAt.format(DBDateFormat), + repeat_times: post.repeat_times - 1, + }); + shouldClear = false; + } + } + } + + if (shouldClear) { + await this.scheduledPosts.delete(post.id); + } } this.scheduledPostLoopTimeout = setTimeout(() => this.scheduledPostLoop(), SCHEDULED_POST_CHECK_INTERVAL); } + /** + * Since !post and !post_embed have a lot of overlap for post scheduling, repeating, etc., that functionality is abstracted out to here + */ + async actualPostCmd( + msg: Message, + targetChannel: Channel, + content: StrictMessageContent, + 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")); + return; + } + + if (content == null && msg.attachments.length === 0) { + msg.channel.createMessage(errorMessage("Message content or attachment required")); + return; + } + + if (opts.repeat) { + if (opts.repeat < MIN_REPEAT_TIME) { + return this.sendErrorMessage(msg.channel, `Minimum time for -repeat is ${humanizeDuration(MIN_REPEAT_TIME)}`); + } + if (opts.repeat > MAX_REPEAT_TIME) { + return this.sendErrorMessage( + msg.channel, + "I'm sure you don't need the repetition interval to be over 100 years long 👀", + ); + } + } + + // If this is a scheduled or repeated post, figure out the next post date + let postAt; + if (opts.schedule) { + // Schedule the post to be posted later + postAt = this.parseScheduleTime(opts.schedule); + if (!postAt) { + return this.sendErrorMessage(msg.channel, "Invalid schedule time"); + } + } else if (opts.repeat) { + postAt = moment().add(opts.repeat, "ms"); + } + + // 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; + + if (opts["repeat-until"]) { + repeatUntil = this.parseScheduleTime(opts["repeat-until"]); + + // Invalid time + if (!repeatUntil) { + return this.sendErrorMessage(msg.channel, "Invalid time specified for -repeat-until"); + } + if (repeatUntil.isBefore(moment())) { + return this.sendErrorMessage(msg.channel, "You can't set -repeat-until in the past"); + } + if (repeatUntil.isAfter(MAX_REPEAT_UNTIL)) { + return this.sendErrorMessage( + msg.channel, + "Unfortunately, -repeat-until can only be at most 100 years into the future. Maybe 99 years would be enough?", + ); + } + } else if (opts["repeat-times"]) { + repeatTimes = opts["repeat-times"]; + if (repeatTimes <= 0) { + return this.sendErrorMessage(msg.channel, "-repeat-times must be 1 or more"); + } + } + + if (repeatUntil && repeatTimes) { + return this.sendErrorMessage(msg.channel, "You can only use one of -repeat-until or -repeat-times at once"); + } + + if (opts.repeat && !repeatUntil && !repeatTimes) { + return this.sendErrorMessage( + msg.channel, + "You must specify -repeat-until or -repeat-times for repeated messages", + ); + } + + if (opts.repeat) { + repeatDetailsStr = repeatUntil + ? `every ${humanizeDuration(opts.repeat)} until ${repeatUntil.format(DBDateFormat)}` + : `every ${humanizeDuration(opts.repeat)}, ${repeatTimes} times in total`; + } + + // Save schedule/repeat information in DB + if (postAt) { + if (postAt < moment()) { + return this.sendErrorMessage(msg.channel, "Post can't be scheduled to be posted in the past"); + } + + await this.scheduledPosts.create({ + author_id: msg.author.id, + author_name: `${msg.author.username}#${msg.author.discriminator}`, + channel_id: targetChannel.id, + content, + attachments: msg.attachments, + post_at: postAt.format(DBDateFormat), + enable_mentions: opts["enable-mentions"], + repeat_interval: opts.repeat, + repeat_until: repeatUntil ? repeatUntil.format(DBDateFormat) : null, + repeat_times: repeatTimes ?? null, + }); + + if (opts.repeat) { + this.logs.log(LogType.SCHEDULED_REPEATED_MESSAGE, { + author: stripObjectToScalars(msg.author), + channel: stripObjectToScalars(targetChannel), + date: postAt.format("YYYY-MM-DD"), + time: postAt.format("HH:mm:ss"), + repeatInterval: humanizeDuration(opts.repeat), + repeatDetails: repeatDetailsStr, + }); + } else { + this.logs.log(LogType.SCHEDULED_MESSAGE, { + author: stripObjectToScalars(msg.author), + channel: stripObjectToScalars(targetChannel), + date: postAt.format("YYYY-MM-DD"), + time: postAt.format("HH:mm:ss"), + }); + } + } + + // When the message isn't scheduled for later, post it immediately + if (!opts.schedule) { + await this.postMessage(targetChannel, content, msg.attachments, opts["enable-mentions"]); + } + + if (opts.repeat) { + this.logs.log(LogType.REPEATED_MESSAGE, { + author: stripObjectToScalars(msg.author), + channel: stripObjectToScalars(targetChannel), + date: postAt.format("YYYY-MM-DD"), + time: postAt.format("HH:mm:ss"), + repeatInterval: humanizeDuration(opts.repeat), + repeatDetails: repeatDetailsStr, + }); + } + + // Bot reply schenanigans + let successMessage = opts.schedule + ? `Message scheduled to be posted in <#${targetChannel.id}> on ${postAt.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)` + : `Message posted in <#${targetChannel.id}>`; + + if (opts.repeat) { + successMessage += `. Message will be automatically reposted every ${humanizeDuration(opts.repeat)}`; + + if (repeatUntil) { + successMessage += ` until ${repeatUntil.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)`; + } else if (repeatTimes) { + successMessage += `, ${repeatTimes} times in total`; + } + + successMessage += "."; + } + + if (targetChannel.id !== msg.channel.id || opts.schedule || opts.repeat) { + this.sendSuccessMessage(msg.channel, successMessage); + } + } + /** * COMMAND: Post a regular text message as the bot to the specified channel */ @@ -214,60 +426,34 @@ export class PostPlugin extends ZeppelinPlugin { name: "schedule", type: "string", }, + { + name: "repeat", + type: "delay", + }, + { + name: "repeat-until", + type: "string", + }, + { + name: "repeat-times", + type: "number", + }, ], }) @d.permission("can_post") async postCmd( msg: Message, - args: { channel: Channel; content?: string; "enable-mentions": boolean; schedule?: string }, + args: { + channel: Channel; + content?: string; + "enable-mentions": boolean; + schedule?: string; + repeat?: number; + "repeat-until"?: string; + "repeat-times"?: number; + }, ) { - if (!(args.channel instanceof TextChannel)) { - msg.channel.createMessage(errorMessage("Channel is not a text channel")); - return; - } - - if (args.content == null && msg.attachments.length === 0) { - msg.channel.createMessage(errorMessage("Text content or attachment required")); - return; - } - - if (args.schedule) { - // Schedule the post to be posted later - const postAt = this.parseScheduleTime(args.schedule); - if (!postAt) { - return this.sendErrorMessage(msg.channel, "Invalid schedule time"); - } - - if (postAt < moment()) { - return this.sendErrorMessage(msg.channel, "Post can't be scheduled to be posted in the past"); - } - - await this.scheduledPosts.create({ - author_id: msg.author.id, - author_name: `${msg.author.username}#${msg.author.discriminator}`, - channel_id: args.channel.id, - content: { content: args.content }, - attachments: msg.attachments, - post_at: postAt.format(DBDateFormat), - enable_mentions: args["enable-mentions"], - }); - this.sendSuccessMessage( - msg.channel, - `Message scheduled to be posted in <#${args.channel.id}> on ${postAt.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)`, - ); - this.logs.log(LogType.SCHEDULED_MESSAGE, { - author: stripObjectToScalars(msg.author), - channel: stripObjectToScalars(args.channel), - date: postAt.format("YYYY-MM-DD"), - time: postAt.format("HH:mm:ss"), - }); - } else { - // Post the message immediately - await this.postMessage(args.channel, args.content, msg.attachments, args["enable-mentions"]); - if (args.channel.id !== msg.channel.id) { - this.sendSuccessMessage(msg.channel, `Message posted in <#${args.channel.id}>`); - } - } + this.actualPostCmd(msg, args.channel, { content: args.content }, args); } /** @@ -280,6 +466,18 @@ export class PostPlugin extends ZeppelinPlugin { { name: "color", type: "string" }, { name: "schedule", type: "string" }, { name: "raw", isSwitch: true, shortcut: "r" }, + { + name: "repeat", + type: "delay", + }, + { + name: "repeat-until", + type: "string", + }, + { + name: "repeat-times", + type: "number", + }, ], }) @d.permission("can_post") @@ -293,13 +491,11 @@ export class PostPlugin extends ZeppelinPlugin { color?: string; schedule?: string; raw?: boolean; + repeat?: number; + "repeat-until"?: string; + "repeat-times"?: number; }, ) { - if (!(args.channel instanceof TextChannel)) { - msg.channel.createMessage(errorMessage("Channel is not a text channel")); - return; - } - const content = args.content || args.maincontent; if (!args.title && !content) { @@ -343,54 +539,7 @@ export class PostPlugin extends ZeppelinPlugin { } } - if (args.schedule) { - // Schedule the post to be posted later - const postAt = this.parseScheduleTime(args.schedule); - if (!postAt) { - return this.sendErrorMessage(msg.channel, "Invalid schedule time"); - } - - if (postAt < moment()) { - return this.sendErrorMessage(msg.channel, "Post can't be scheduled to be posted in the past"); - } - - await this.scheduledPosts.create({ - author_id: msg.author.id, - author_name: `${msg.author.username}#${msg.author.discriminator}`, - channel_id: args.channel.id, - content: { embed }, - attachments: msg.attachments, - post_at: postAt.format(DBDateFormat), - }); - await this.sendSuccessMessage( - msg.channel, - `Embed scheduled to be posted in <#${args.channel.id}> on ${postAt.format("YYYY-MM-DD [at] HH:mm:ss")} (UTC)`, - ); - this.logs.log(LogType.SCHEDULED_MESSAGE, { - author: stripObjectToScalars(msg.author), - channel: stripObjectToScalars(args.channel), - date: postAt.format("YYYY-MM-DD"), - time: postAt.format("HH:mm:ss"), - }); - } else { - const createdMsg = await args.channel.createMessage({ embed }); - this.savedMessages.setPermanent(createdMsg.id); - - if (msg.channel.id !== args.channel.id) { - await this.sendSuccessMessage(msg.channel, `Embed posted in <#${args.channel.id}>`); - } - } - - if (args.content) { - const prefix = this.guildConfig.prefix || "!"; - msg.channel.createMessage( - trimLines(` - <@!${msg.author.id}> You can now specify an embed's content directly at the end of the command: - \`${prefix}post_embed -title "Some title" content goes here\` - The \`-content\` option will soon be removed in favor of this. - `), - ); - } + this.actualPostCmd(msg, args.channel, { embed }, args); } /** @@ -495,6 +644,14 @@ export class PostPlugin extends ZeppelinPlugin { const parts = [`\`#${i++}\` \`[${p.post_at}]\` ${previewText}${isTruncated ? "..." : ""}`]; if (p.attachments.length) parts.push("*(with attachment)*"); if (p.content.embed) parts.push("*(embed)*"); + if (p.repeat_until) + parts.push(`*(repeated every ${humanizeDuration(p.repeat_interval)} until ${p.repeat_until})*`); + if (p.repeat_times) + parts.push( + `*(repeated every ${humanizeDuration(p.repeat_interval)}, ${p.repeat_times} more ${ + p.repeat_times === 1 ? "time" : "times" + })*`, + ); parts.push(`*(${p.author_name})*`); return parts.join(" "); @@ -503,7 +660,7 @@ export class PostPlugin extends ZeppelinPlugin { const finalMessage = trimLines(` ${postLines.join("\n")} - Use \`scheduled_posts show \` to view a scheduled post in full + Use \`scheduled_posts \` to view a scheduled post in full Use \`scheduled_posts delete \` to delete a scheduled post `); createChunkedMessage(msg.channel, finalMessage); diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 5fa40b93..009fcdcc 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -47,6 +47,7 @@ export const SECONDS = 1000 * MS; export const MINUTES = 60 * SECONDS; export const HOURS = 60 * MINUTES; export const DAYS = 24 * HOURS; +export const WEEKS = 7 * 24 * HOURS; export function tNullable>(type: T) { return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); From c7103ac432a28e97d5924e8d642c0b5c64fb7657 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Dec 2019 00:09:04 +0200 Subject: [PATCH 43/67] Set !post -repeat max time to 2^32 milliseconds --- backend/src/plugins/Post.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/backend/src/plugins/Post.ts b/backend/src/plugins/Post.ts index 9633e5b4..02749bc7 100644 --- a/backend/src/plugins/Post.ts +++ b/backend/src/plugins/Post.ts @@ -42,7 +42,7 @@ const SCHEDULED_POST_CHECK_INTERVAL = 5 * SECONDS; const SCHEDULED_POST_PREVIEW_TEXT_LENGTH = 50; const MIN_REPEAT_TIME = 5 * MINUTES; -const MAX_REPEAT_TIME = 100 * 365 * DAYS; +const MAX_REPEAT_TIME = Math.pow(2, 32); const MAX_REPEAT_UNTIL = moment().add(100, "years"); export class PostPlugin extends ZeppelinPlugin { @@ -273,10 +273,7 @@ export class PostPlugin extends ZeppelinPlugin { return this.sendErrorMessage(msg.channel, `Minimum time for -repeat is ${humanizeDuration(MIN_REPEAT_TIME)}`); } if (opts.repeat > MAX_REPEAT_TIME) { - return this.sendErrorMessage( - msg.channel, - "I'm sure you don't need the repetition interval to be over 100 years long 👀", - ); + return this.sendErrorMessage(msg.channel, `Max time for -repeat is ${humanizeDuration(MAX_REPEAT_TIME)}`); } } @@ -644,14 +641,16 @@ export class PostPlugin extends ZeppelinPlugin { const parts = [`\`#${i++}\` \`[${p.post_at}]\` ${previewText}${isTruncated ? "..." : ""}`]; if (p.attachments.length) parts.push("*(with attachment)*"); if (p.content.embed) parts.push("*(embed)*"); - if (p.repeat_until) + if (p.repeat_until) { parts.push(`*(repeated every ${humanizeDuration(p.repeat_interval)} until ${p.repeat_until})*`); - if (p.repeat_times) + } + if (p.repeat_times) { parts.push( `*(repeated every ${humanizeDuration(p.repeat_interval)}, ${p.repeat_times} more ${ p.repeat_times === 1 ? "time" : "times" })*`, ); + } parts.push(`*(${p.author_name})*`); return parts.join(" "); From 5ab6f5959363cef464bcca5cfebf46358a94ad48 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Dec 2019 01:11:40 +0200 Subject: [PATCH 44/67] starboard: localized timestamps; add link to original message again --- backend/src/plugins/Starboard.ts | 7 +++++-- backend/src/utils.ts | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Starboard.ts b/backend/src/plugins/Starboard.ts index d0056626..e51e637b 100644 --- a/backend/src/plugins/Starboard.ts +++ b/backend/src/plugins/Starboard.ts @@ -11,6 +11,7 @@ import { tNullable, tDeepPartial, UnknownUser, + EMPTY_CHAR, } from "../utils"; import path from "path"; import moment from "moment-timezone"; @@ -247,12 +248,12 @@ export class StarboardPlugin extends ZeppelinPlugin { const embed: EmbedBase = { footer: { - text: `#${(msg.channel as GuildChannel).name} - ${time}`, + text: `#${(msg.channel as GuildChannel).name}`, }, author: { name: `${msg.author.username}#${msg.author.discriminator}`, }, - url: messageLink(msg), + timestamp: new Date(msg.timestamp).toISOString(), }; if (msg.author.avatarURL) { @@ -280,6 +281,8 @@ export class StarboardPlugin extends ZeppelinPlugin { embed.image = msg.embeds[0].image; } + embed.fields = [{ name: EMPTY_CHAR, value: `[Jump to message](${messageLink(msg)})` }]; + const starboardMessage = await (channel as TextChannel).createMessage({ embed }); await this.starboardMessages.createStarboardMessage(channel.id, msg.id, starboardMessage.id); } diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 009fcdcc..9542c48b 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -49,6 +49,8 @@ export const HOURS = 60 * MINUTES; export const DAYS = 24 * HOURS; export const WEEKS = 7 * 24 * HOURS; +export const EMPTY_CHAR = "\u200b"; + export function tNullable>(type: T) { return t.union([type, t.undefined, t.null], `Nullable<${type.name}>`); } From d403292ef674943d7fc3953e9cdbda85402d7c1c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Dec 2019 01:13:53 +0200 Subject: [PATCH 45/67] starboard: prevent self-votes --- backend/src/plugins/Starboard.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/plugins/Starboard.ts b/backend/src/plugins/Starboard.ts index e51e637b..46868206 100644 --- a/backend/src/plugins/Starboard.ts +++ b/backend/src/plugins/Starboard.ts @@ -183,6 +183,9 @@ export class StarboardPlugin extends ZeppelinPlugin { } } + // No self-votes! + if (msg.author.id === userId) return; + const user = await this.resolveUser(userId); if (user instanceof UnknownUser) return; if (user.bot) return; @@ -212,7 +215,7 @@ export class StarboardPlugin extends ZeppelinPlugin { for (const starboard of applicableStarboards) { // Save reaction into the database - await this.starboardReactions.createStarboardReaction(msg.id, userId).catch(); + await this.starboardReactions.createStarboardReaction(msg.id, userId).catch(noop); // If the message has already been posted to this starboard, we don't need to do anything else const starboardMessages = await this.starboardMessages.getMatchingStarboardMessages(starboard.channel_id, msg.id); From 30e86fcc7354b4c9f966ebd05bd8232c1ad91fa9 Mon Sep 17 00:00:00 2001 From: Miikka <2606411+Dragory@users.noreply.github.com> Date: Mon, 2 Dec 2019 10:46:11 +0200 Subject: [PATCH 46/67] Update README.md --- README.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cf65b7c6..e004b434 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,29 @@ -# Development +# Zeppelin +Zeppelin is a moderation bot for Discord, designed with large servers and reliability in mind. + +**Main features include:** +- Extensive automoderator features (automod) + - Word filters, spam detection, etc. +- Detailed moderator action tracking and notes (cases) +- Customizable server logs +- Tags/custom commands +- Reaction roles +- Tons of utility commands, including a granular member search +- Full configuration via a web dashboard + - Override specific settings and permissions on e.g. a per-user, per-channel, or per-permission-level basis +- Bot-managed slowmodes + - Automatically switches between native slowmodes (for 6h or less) and bot-enforced (for longer slowmodes) +- Starboard +- And more! + +See https://zeppelin.gg/ for more details. + +## Development These instructions are intended for bot development only. 👉 **No support is offered for self-hosting the bot!** 👈 -## Running the bot +### Running the bot 1. `cd backend` 2. `npm ci` 3. Make a copy of `bot.env.example` called `bot.env`, fill in the values @@ -14,7 +34,7 @@ These instructions are intended for bot development only. with automatic restart on file changes 5. When testing, make sure you have your test server in the `allowed_guilds` table or the guild's config won't be loaded at all -## Running the API server +### Running the API server 1. `cd backend` 2. `npm ci` 3. Make a copy of `api.env.example` called `api.env`, fill in the values @@ -24,7 +44,7 @@ These instructions are intended for bot development only. * `npm run watch` to watch files and run the **bot and api both** in a **development** environment with automatic restart on file changes -## Running the dashboard +### Running the dashboard 1. `cd dashboard` 2. `npm ci` 3. Make a copy of `.env.example` called `.env`, fill in the values @@ -32,7 +52,7 @@ These instructions are intended for bot development only. * `npm run build` compiles the dashboard's static files to `dist/` which can then be served with any web server * `npm run watch` runs webpack's dev server that automatically reloads on changes -## Notes +### Notes * Since we now use shared paths in `tsconfig.json`, the compiled files in `backend/dist/` have longer paths, e.g. `backend/dist/backend/src/index.js` instead of `backend/dist/index.js`. This is because the compiled shared files are placed in `backend/dist/shared`. @@ -40,7 +60,7 @@ These instructions are intended for bot development only. `ava` and compiled `.js` files * To run the tests for the files in the `shared/` directory, you also need to run `npm ci` there -## Config format example +### Config format example Configuration is stored in the database in the `configs` table ```yml From 73780e503f2d13060ae786b6c1400b709875923f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 8 Dec 2019 02:05:35 +0200 Subject: [PATCH 47/67] Add source map support to backend dev builds --- backend/package-lock.json | 18 ++++++++++++++++++ backend/package.json | 5 +++-- backend/tsconfig.json | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index b041a984..b7e832f9 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -4126,6 +4126,24 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 6342e440..f27f4b0f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,9 +6,9 @@ "scripts": { "watch": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node start-dev.js\"", "build": "rimraf dist && tsc", - "start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js dist/backend/src/index.js", + "start-bot-dev": "cross-env NODE_ENV=development node -r source-map-support/register -r ./register-tsconfig-paths.js dist/backend/src/index.js", "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/index.js", - "start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", + "start-api-dev": "cross-env NODE_ENV=development node -r source-map-support/register -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", "typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", "migrate-prod": "npm run typeorm -- migration:run", @@ -71,6 +71,7 @@ "@types/tmp": "0.0.33", "ava": "^2.4.0", "rimraf": "^2.6.2", + "source-map-support": "^0.5.16", "tsc-watch": "^4.0.0", "typescript": "^3.7.2" }, diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 1bc9478e..84c78116 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -18,7 +18,8 @@ "@shared/*": [ "../shared/src/*" ] - } + }, + "sourceMap": true }, "include": [ "src/**/*.ts" From b249ab71426ac79d8b26b107690568bc879c32de Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sun, 29 Dec 2019 00:51:04 +1100 Subject: [PATCH 48/67] fixed case issue --- backend/src/plugins/Utility.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 5d88c29e..0a81aaa1 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -341,9 +341,9 @@ export class UtilityPlugin extends ZeppelinPlugin { if (args.query) { let queryRegex: RegExp; if (args.regex) { - queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "i" : ""); + queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "" : "i"); } else { - queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "i" : ""); + queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); } if (!safeRegex(queryRegex)) { From 3740a59d20ae27296df2283ca4869066ed414e46 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 29 Dec 2019 22:34:05 -0500 Subject: [PATCH 49/67] Update Knub to 26.1.1 --- backend/package-lock.json | 6 +++--- backend/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index b7e832f9..7fcae5a1 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2776,9 +2776,9 @@ } }, "knub": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/knub/-/knub-26.1.0.tgz", - "integrity": "sha512-QeFb2R3l2jBEF/jfJn2ahJT6ueNF6KruNMhusntP7HKPzTM0YL0k22VTq6wMGMn3obEOkdQ/r3ENg26OWz5CkA==", + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/knub/-/knub-26.1.1.tgz", + "integrity": "sha512-y9tLo7JQYH39YUKmkOmxanF/w/mWOjppAMN6BrT8HmKAiXvMoxu6c4cvZycm8TH9edJLCi2Z4c18FXmeDOYd5w==", "requires": { "escape-string-regexp": "^2.0.0", "knub-command-manager": "^6.1.0", diff --git a/backend/package.json b/backend/package.json index f27f4b0f..d6af8fa6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,7 +32,7 @@ "humanize-duration": "^3.15.0", "io-ts": "^2.0.0", "js-yaml": "^3.13.1", - "knub": "^26.0.2", + "knub": "^26.1.1", "knub-command-manager": "^6.1.0", "last-commit-log": "^2.1.0", "lodash.chunk": "^4.2.0", From 25c88ae630128b681042cad1c95195b3e0317d30 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sat, 4 Jan 2020 16:00:21 +1100 Subject: [PATCH 50/67] Converted all remaining boolean parameters to flags --- backend/src/plugins/Post.ts | 2 +- backend/src/plugins/Slowmode.ts | 2 +- backend/src/plugins/Utility.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/Post.ts b/backend/src/plugins/Post.ts index 02749bc7..5263d450 100644 --- a/backend/src/plugins/Post.ts +++ b/backend/src/plugins/Post.ts @@ -417,7 +417,7 @@ export class PostPlugin extends ZeppelinPlugin { options: [ { name: "enable-mentions", - type: "bool", + isSwitch: true, }, { name: "schedule", diff --git a/backend/src/plugins/Slowmode.ts b/backend/src/plugins/Slowmode.ts index 23f52c9f..3535797a 100644 --- a/backend/src/plugins/Slowmode.ts +++ b/backend/src/plugins/Slowmode.ts @@ -212,7 +212,7 @@ export class SlowmodePlugin extends ZeppelinPlugin { options: [ { name: "force", - type: "bool", + isSwitch: true, }, ], }) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 0a81aaa1..ff38eef4 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -191,7 +191,7 @@ export class UtilityPlugin extends ZeppelinPlugin { options: [ { name: "counts", - type: "bool", + isSwitch: true, }, { name: "sort", From 17decd09d5b5700b9a49329485c95f5db1123d82 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Wed, 8 Jan 2020 14:29:49 +1100 Subject: [PATCH 51/67] Added created_at field to reminders table. Added time remaining timestamp to reminders command. Added creation date timestamp to reminder activation message --- backend/src/data/GuildReminders.ts | 3 ++- backend/src/data/entities/Reminder.ts | 2 ++ ...8445483917-CreateReminderCreatedAtField.ts | 18 +++++++++++++ backend/src/plugins/Reminders.ts | 27 +++++++++++++++---- 4 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts diff --git a/backend/src/data/GuildReminders.ts b/backend/src/data/GuildReminders.ts index 13f2f5b2..6425ff61 100644 --- a/backend/src/data/GuildReminders.ts +++ b/backend/src/data/GuildReminders.ts @@ -34,13 +34,14 @@ export class GuildReminders extends BaseGuildRepository { }); } - async add(userId: string, channelId: string, remindAt: string, body: string) { + async add(userId: string, channelId: string, remindAt: string, body: string, created_at: string) { await this.reminders.insert({ guild_id: this.guildId, user_id: userId, channel_id: channelId, remind_at: remindAt, body, + created_at }); } } diff --git a/backend/src/data/entities/Reminder.ts b/backend/src/data/entities/Reminder.ts index a069ddcf..b232c796 100644 --- a/backend/src/data/entities/Reminder.ts +++ b/backend/src/data/entities/Reminder.ts @@ -15,4 +15,6 @@ export class Reminder { @Column() remind_at: string; @Column() body: string; + + @Column() created_at: string } diff --git a/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts b/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts new file mode 100644 index 00000000..b078d18f --- /dev/null +++ b/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; + +export class CreateReminderCreatedAtField1578445483917 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn("reminders", + new TableColumn({ + name: "created_at", + type: "datetime", + isNullable: false + })); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("reminders", "created_at"); + } + +} diff --git a/backend/src/plugins/Reminders.ts b/backend/src/plugins/Reminders.ts index b97dc72d..22678f96 100644 --- a/backend/src/plugins/Reminders.ts +++ b/backend/src/plugins/Reminders.ts @@ -11,8 +11,10 @@ import { errorMessage, sorter, successMessage, + tDateTime, } from "../utils"; import * as t from "io-ts"; +import { EventListenerTypes } from "typeorm/metadata/types/EventListenerTypes"; const ConfigSchema = t.type({ can_use: t.boolean, @@ -68,11 +70,23 @@ export class RemindersPlugin extends ZeppelinPlugin { const pendingReminders = await this.reminders.getDueReminders(); for (const reminder of pendingReminders) { const channel = this.guild.channels.get(reminder.channel_id); + if (channel && channel instanceof TextChannel) { try { - await channel.createMessage( - disableLinkPreviews(`<@!${reminder.user_id}> You asked me to remind you: ${reminder.body}`), - ); + //Only show created at date if one exists + if(moment(reminder.created_at).isValid()){ + const target = moment(); + const diff = target.diff(moment(reminder.created_at , "YYYY-MM-DD HH:mm:ss")); + const result = humanizeDuration(diff, { largest: 2, round: true }); + await channel.createMessage( + disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body} \n\`Set at ${reminder.created_at} (${result} ago)\``), + ); + } + else{ + await channel.createMessage( + disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body}`), + ); + } } catch (e) { // Probably random Discord internal server error or missing permissions or somesuch // Try again next round unless we've already tried to post this a bunch of times @@ -127,7 +141,7 @@ export class RemindersPlugin extends ZeppelinPlugin { } const reminderBody = args.reminder || `https://discordapp.com/channels/${this.guildId}/${msg.channel.id}/${msg.id}`; - await this.reminders.add(msg.author.id, msg.channel.id, reminderTime.format("YYYY-MM-DD HH:mm:ss"), reminderBody); + await this.reminders.add(msg.author.id, msg.channel.id, reminderTime.format("YYYY-MM-DD HH:mm:ss"), reminderBody, moment().format("YYYY-MM-DD HH:mm:ss")); const msUntilReminder = reminderTime.diff(now); const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); @@ -152,7 +166,10 @@ export class RemindersPlugin extends ZeppelinPlugin { const lines = Array.from(reminders.entries()).map(([i, reminder]) => { const num = i + 1; const paddedNum = num.toString().padStart(longestNum, " "); - return `\`${paddedNum}.\` \`${reminder.remind_at}\` ${reminder.body}`; + const target = moment(reminder.remind_at , "YYYY-MM-DD HH:mm:ss"); + const diff = target.diff(moment()); + const result = humanizeDuration(diff, { largest: 2, round: true }); + return `\`${paddedNum}.\` \`${reminder.remind_at} (${result})\` ${reminder.body}`; }); createChunkedMessage(msg.channel, lines.join("\n")); From 34aed027f47e60c2855390ba77db45707f11bda1 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Wed, 8 Jan 2020 14:33:12 +1100 Subject: [PATCH 52/67] removed unnecessary code lines --- backend/src/plugins/Reminders.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/plugins/Reminders.ts b/backend/src/plugins/Reminders.ts index 22678f96..5f59e9bf 100644 --- a/backend/src/plugins/Reminders.ts +++ b/backend/src/plugins/Reminders.ts @@ -11,7 +11,6 @@ import { errorMessage, sorter, successMessage, - tDateTime, } from "../utils"; import * as t from "io-ts"; import { EventListenerTypes } from "typeorm/metadata/types/EventListenerTypes"; From a1cb358d16062ff3a191e3df880f7f09508577ec Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Wed, 8 Jan 2020 14:34:36 +1100 Subject: [PATCH 53/67] removed unnecessary code lines 2: electric boogaloo --- backend/src/plugins/Reminders.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/plugins/Reminders.ts b/backend/src/plugins/Reminders.ts index 5f59e9bf..2d1a6a82 100644 --- a/backend/src/plugins/Reminders.ts +++ b/backend/src/plugins/Reminders.ts @@ -13,7 +13,6 @@ import { successMessage, } from "../utils"; import * as t from "io-ts"; -import { EventListenerTypes } from "typeorm/metadata/types/EventListenerTypes"; const ConfigSchema = t.type({ can_use: t.boolean, From 329de665f519bc96066dc73de5a1cd016b415bbd Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Wed, 8 Jan 2020 14:35:44 +1100 Subject: [PATCH 54/67] removed unnecessary code lines 2: electric boogaloo 2 --- backend/src/plugins/Reminders.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/plugins/Reminders.ts b/backend/src/plugins/Reminders.ts index 2d1a6a82..3f15161a 100644 --- a/backend/src/plugins/Reminders.ts +++ b/backend/src/plugins/Reminders.ts @@ -68,7 +68,6 @@ export class RemindersPlugin extends ZeppelinPlugin { const pendingReminders = await this.reminders.getDueReminders(); for (const reminder of pendingReminders) { const channel = this.guild.channels.get(reminder.channel_id); - if (channel && channel instanceof TextChannel) { try { //Only show created at date if one exists From 0b1381e7b331ab63d826aec9b42e9d7336217b58 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Thu, 9 Jan 2020 00:23:41 +1100 Subject: [PATCH 55/67] created new plugin --- .gitignore | 1 + backend/src/plugins/Roles.ts | 92 +++++++++++++++++++++++++ backend/src/plugins/availablePlugins.ts | 2 + 3 files changed, 95 insertions(+) create mode 100644 backend/src/plugins/Roles.ts diff --git a/.gitignore b/.gitignore index d38433b6..efa6d897 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ desktop.ini .cache npm-ls.txt npm-audit.txt +.vscode/launch.json diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts new file mode 100644 index 00000000..fd49e1ee --- /dev/null +++ b/backend/src/plugins/Roles.ts @@ -0,0 +1,92 @@ +import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; +import * as t from "io-ts"; +import { tNullable } from "src/utils"; +import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub"; +import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; +import { GuildLogs } from "src/data/GuildLogs"; + +const ConfigSchema = t.type({ + can_assign: t.boolean, + assignable_roles: tNullable(t.array(t.string)) + }); +type TConfigSchema = t.TypeOf; + +enum RoleActions{ + Add, + Remove +}; + +export class RolesPlugin extends ZeppelinPlugin { + public static pluginName = "roles"; + public static configSchema = ConfigSchema; + + public static pluginInfo = { + prettyName: "Roles", + description: trimPluginDescription(` + Enables authorised users to add and remove whitelisted roles with a command. + `), + }; + protected logs: GuildLogs; + + onLoad(){ + this.logs = new GuildLogs(this.guildId); + } + + public static getStaticDefaultOptions(): IPluginOptions { + return { + config: { + can_assign: false, + assignable_roles: null + }, + overrides: [ + { + level: ">=50", + config: { + can_assign: true, + }, + }, + ], + }; + } + + + @d.command("role", " ",{ + extra: { + info: { + description: "Assign a permitted role to a user", + }, + }, + }) + @d.permission("can_assign") + async assignRole(msg, args: {action: string; user: string; role: string}){ + const user = await this.resolveUser(args.user); + console.log(user); + if (!user) { + return this.sendErrorMessage(msg.channel, `User not found`); + } + + //if the role doesnt exist, we can exit + let roleIds = (msg.channel as TextChannel).guild.roles.map(x => x.id) + if(!(roleIds.includes(args.role))){ + return this.sendErrorMessage(msg.channel, `Role not found`); + } + + // If the user exists as a guild member, make sure we can act on them first + const member = await this.getMember(user.id); + if (member && !this.canActOn(msg.member, member)) { + this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: insufficient permissions"); + return; + } + + const action: string = args.action[0].toUpperCase() + args.action.slice(1).toLowerCase(); + if(!RoleActions[action]){ + this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: invalid action"); + } + + //check if the role is allowed to be applied + + + console.log("exited at the end"); + } + +} \ No newline at end of file diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index f202ea76..8e6d3374 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -27,6 +27,7 @@ import { LocatePlugin } from "./LocateUser"; import { GuildConfigReloader } from "./GuildConfigReloader"; import { ChannelArchiverPlugin } from "./ChannelArchiver"; import { AutomodPlugin } from "./Automod"; +import { RolesPlugin} from "./Roles"; /** * Plugins available to be loaded for individual guilds @@ -58,6 +59,7 @@ export const availablePlugins = [ CompanionChannelPlugin, LocatePlugin, ChannelArchiverPlugin, + RolesPlugin, ]; /** From fa1e8b78f54df03d01d64cc514794611d3253e7f Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Thu, 9 Jan 2020 01:32:12 +1100 Subject: [PATCH 56/67] POC done --- backend/src/plugins/Roles.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts index fd49e1ee..08734dc0 100644 --- a/backend/src/plugins/Roles.ts +++ b/backend/src/plugins/Roles.ts @@ -1,9 +1,9 @@ import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; import * as t from "io-ts"; -import { tNullable } from "src/utils"; +import { tNullable } from "../utils"; import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub"; import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; -import { GuildLogs } from "src/data/GuildLogs"; +import { GuildLogs } from "../data/GuildLogs"; const ConfigSchema = t.type({ can_assign: t.boolean, @@ -12,7 +12,7 @@ const ConfigSchema = t.type({ type TConfigSchema = t.TypeOf; enum RoleActions{ - Add, + Add = 1, Remove }; @@ -58,10 +58,10 @@ export class RolesPlugin extends ZeppelinPlugin { }, }) @d.permission("can_assign") - async assignRole(msg, args: {action: string; user: string; role: string}){ + async assignRole(msg: Message, args: {action: string; user: string; role: string}){ const user = await this.resolveUser(args.user); console.log(user); - if (!user) { + if (user.discriminator == "0000") { return this.sendErrorMessage(msg.channel, `User not found`); } @@ -81,10 +81,17 @@ export class RolesPlugin extends ZeppelinPlugin { const action: string = args.action[0].toUpperCase() + args.action.slice(1).toLowerCase(); if(!RoleActions[action]){ this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: invalid action"); + return; } //check if the role is allowed to be applied - + let config = this.getConfigForMsg(msg) + if(!config.assignable_roles || !config.assignable_roles.includes(args.role)){ + this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); + return; + } + //at this point, everything has been verified, so apply the role + await this.bot.addGuildMemberRole(this.guildId, user.id, args.role); console.log("exited at the end"); } From 7de4be0b444eebf9d3b02e258364f09fdf1ba223 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Thu, 9 Jan 2020 06:05:17 +0200 Subject: [PATCH 57/67] Update Knub to v27.0.0; update knub-command-manager to v7.0.0 --- backend/package-lock.json | 14 +++++++------- backend/package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 7fcae5a1..ff495015 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2776,12 +2776,12 @@ } }, "knub": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/knub/-/knub-26.1.1.tgz", - "integrity": "sha512-y9tLo7JQYH39YUKmkOmxanF/w/mWOjppAMN6BrT8HmKAiXvMoxu6c4cvZycm8TH9edJLCi2Z4c18FXmeDOYd5w==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/knub/-/knub-27.0.0.tgz", + "integrity": "sha512-iA332KoD2LN4R6c24HFGxQc8QH2GwG5Az97Rie5eqT5KFiWV/f5UJ8EfWhIJ/bOmMHubtJ4j+tEdQIBidC+WxA==", "requires": { "escape-string-regexp": "^2.0.0", - "knub-command-manager": "^6.1.0", + "knub-command-manager": "^7.0.0", "lodash.clonedeep": "^4.5.0", "reflect-metadata": "^0.1.13", "ts-essentials": "^2.0.12" @@ -2795,9 +2795,9 @@ } }, "knub-command-manager": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/knub-command-manager/-/knub-command-manager-6.1.0.tgz", - "integrity": "sha512-Bn//fk3ZKUNoJ+p0fNdUfbcyzTUdWHGaP12irSy8U1lfxy3pBrOZZsc0tpIkBFLpwLWw/VxHInX1x2b6MFhn0Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/knub-command-manager/-/knub-command-manager-7.0.0.tgz", + "integrity": "sha512-6Vhw3jtIyr5AqVqLcdK6/TbVcZ45hcTw+50w+5Z3YYEqBV1eiPO3WXZpEgaiTUmn0Q60OPOSzt73Bu6DyTBR6A==", "requires": { "escape-string-regexp": "^2.0.0" }, diff --git a/backend/package.json b/backend/package.json index d6af8fa6..d8be4984 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,8 +32,8 @@ "humanize-duration": "^3.15.0", "io-ts": "^2.0.0", "js-yaml": "^3.13.1", - "knub": "^26.1.1", - "knub-command-manager": "^6.1.0", + "knub": "^27.0.0", + "knub-command-manager": "^7.0.0", "last-commit-log": "^2.1.0", "lodash.chunk": "^4.2.0", "lodash.clonedeep": "^4.5.0", From d16a67bca3563da0bfd443952f03678621d2cf76 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Fri, 10 Jan 2020 01:04:58 +1100 Subject: [PATCH 58/67] added match by role name functionality to roles plugin --- backend/src/plugins/Roles.ts | 37 +++++++++++++++++++-------- backend/src/plugins/ZeppelinPlugin.ts | 11 ++++++++ backend/src/utils.ts | 26 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts index 08734dc0..1355333e 100644 --- a/backend/src/plugins/Roles.ts +++ b/backend/src/plugins/Roles.ts @@ -50,7 +50,7 @@ export class RolesPlugin extends ZeppelinPlugin { } - @d.command("role", " ",{ + @d.command("role", " [role:string$]",{ extra: { info: { description: "Assign a permitted role to a user", @@ -60,20 +60,20 @@ export class RolesPlugin extends ZeppelinPlugin { @d.permission("can_assign") async assignRole(msg: Message, args: {action: string; user: string; role: string}){ const user = await this.resolveUser(args.user); - console.log(user); + const roleId = await this.resolveRoleId(args.role); if (user.discriminator == "0000") { return this.sendErrorMessage(msg.channel, `User not found`); } //if the role doesnt exist, we can exit let roleIds = (msg.channel as TextChannel).guild.roles.map(x => x.id) - if(!(roleIds.includes(args.role))){ + if(!(roleIds.includes(roleId))){ return this.sendErrorMessage(msg.channel, `Role not found`); } // If the user exists as a guild member, make sure we can act on them first - const member = await this.getMember(user.id); - if (member && !this.canActOn(msg.member, member)) { + const targetMember = await this.getMember(user.id); + if (targetMember && !this.canActOn(msg.member, targetMember)) { this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: insufficient permissions"); return; } @@ -86,14 +86,31 @@ export class RolesPlugin extends ZeppelinPlugin { //check if the role is allowed to be applied let config = this.getConfigForMsg(msg) - if(!config.assignable_roles || !config.assignable_roles.includes(args.role)){ + if(!config.assignable_roles || !config.assignable_roles.includes(roleId)){ this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); return; } - //at this point, everything has been verified, so apply the role - await this.bot.addGuildMemberRole(this.guildId, user.id, args.role); - - console.log("exited at the end"); + //at this point, everything has been verified, so it's ACTION TIME + switch(RoleActions[action]){ + case RoleActions.Add: + if(targetMember.roles.includes(roleId)){ + this.sendErrorMessage(msg.channel, "Role already applied to user"); + return; + } + await this.bot.addGuildMemberRole(this.guildId, user.id, roleId); + this.sendSuccessMessage(msg.channel, `Role added to user!`); + break; + case RoleActions.Remove: + if(!targetMember.roles.includes(roleId)){ + this.sendErrorMessage(msg.channel, "User does not have role"); + return; + } + await this.bot.removeGuildMemberRole(this.guildId, user.id, roleId); + this.sendSuccessMessage(msg.channel, `Role removed from user!`); + break; + default: + break; + } } } \ No newline at end of file diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index ce593a0b..65eecbaa 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -16,6 +16,7 @@ import { trimEmptyStartEndLines, trimIndents, UnknownUser, + resolveRoleId, } from "../utils"; import { Invite, Member, User } from "eris"; import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line @@ -237,6 +238,16 @@ export class ZeppelinPlugin extends Plug return user; } + /** + * Resolves a role from the passed string. The passed string can be a role ID, a role mention or a role name. + * In the event of duplicate role names, this function will return the first one it comes across. + * @param roleResolvable + */ + async resolveRoleId(roleResolvable: string): Promise { + const roleId = await resolveRoleId(this.bot, this.guildId, roleResolvable); + return roleId; + } + /** * Resolves a member from the passed string. The passed string can be a user id, a user mention, a full username (with discrim), etc. * If the member is not found in the cache, it's fetched from the API. diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 9542c48b..f12a04e1 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -956,6 +956,32 @@ export async function resolveMember(bot: Client, guild: Guild, value: string): P return null; } +export async function resolveRoleId(bot: Client, guildId: string, value: string){ + if(value == null){ + return null; + } + + //role mention + const mentionMatch = value.match(/^<@&?(\d+)>$/); + if(mentionMatch){ + return mentionMatch[1]; + } + + //role name + let roleList = await bot.getRESTGuildRoles(guildId); + let role = roleList.filter(x => x.name.toLocaleLowerCase() == value.toLocaleLowerCase()); + if(role[0]){ + return role[0].id; + } + + //role ID + const idMatch = value.match(/^\d+$/); + if (idMatch) { + return value; + } + return null; +} + export type StrictMessageContent = { content?: string; tts?: boolean; disableEveryone?: boolean; embed?: EmbedOptions }; export async function confirm(bot: Client, channel: TextableChannel, userId: string, content: MessageContent) { From 12d8b19561415bf5a05a80cccc371d813e4fb9c3 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sat, 11 Jan 2020 01:39:02 +1100 Subject: [PATCH 59/67] Added compact switch to !info --- backend/src/plugins/Utility.ts | 113 ++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 0a81aaa1..51c85815 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -792,9 +792,16 @@ export class UtilityPlugin extends ZeppelinPlugin { basicUsage: "!info 106391128718245888", }, }, + options: [ + { + name: "compact", + shortcut: "c", + isSwitch: true, + } + ] }) @d.permission("can_info") - async infoCmd(msg: Message, args: { user?: User | UnknownUser }) { + async infoCmd(msg: Message, args: { user?: User | UnknownUser, compact?: boolean }) { const user = args.user || msg.author; let member; @@ -816,15 +823,27 @@ export class UtilityPlugin extends ZeppelinPlugin { embed.title = `${user.username}#${user.discriminator}`; embed.thumbnail = { url: user.avatarURL }; - embed.fields.push({ - name: "User information", - value: - trimLines(` - ID: **${user.id}** - Profile: <@!${user.id}> - Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** - `) + embedPadding, - }); + if(args.compact){ + embed.fields.push({ + name: "User information", + value: + trimLines(` + Profile: <@!${user.id}> + Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + `), + }); + } + else{ + embed.fields.push({ + name: "User information", + value: + trimLines(` + ID: **${user.id}** + Profile: <@!${user.id}> + Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + `) + embedPadding, + }); + } } else { embed.title = `Unknown user`; } @@ -837,56 +856,62 @@ export class UtilityPlugin extends ZeppelinPlugin { }); const roles = member.roles.map(id => this.guild.roles.get(id)).filter(r => !!r); - embed.fields.push({ - name: "Member information", - value: - trimLines(` - Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})** - ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} - `) + embedPadding, - }); - - const voiceChannel = member.voiceState.channelID ? this.guild.channels.get(member.voiceState.channelID) : null; - if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { + if(args.compact){ + embed.fields[0].value += `\n` + trimLines(`Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**`); + } + else{ embed.fields.push({ - name: "Voice information", + name: "Member information", value: trimLines(` - ${voiceChannel ? `Current voice channel: **${voiceChannel ? voiceChannel.name : "None"}**` : ""} - ${member.voiceState.mute ? "Server voice muted: **Yes**" : ""} - ${member.voiceState.deaf ? "Server voice deafened: **Yes**" : ""} - `) + embedPadding, + Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} + `) + embedPadding, }); } + if(!args.compact){ + const voiceChannel = member.voiceState.channelID ? this.guild.channels.get(member.voiceState.channelID) : null; + if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { + embed.fields.push({ + name: "Voice information", + value: + trimLines(` + ${voiceChannel ? `Current voice channel: **${voiceChannel ? voiceChannel.name : "None"}**` : ""} + ${member.voiceState.mute ? "Server voice muted: **Yes**" : ""} + ${member.voiceState.deaf ? "Server voice deafened: **Yes**" : ""} + `) + embedPadding, + }); + } + } } else { embed.fields.push({ name: "!! USER IS NOT ON THE SERVER !!", value: embedPadding, }); } + if(!args.compact){ + const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); - const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); + if (cases.length > 0) { + cases.sort((a, b) => { + return a.created_at < b.created_at ? 1 : -1; + }); - if (cases.length > 0) { - cases.sort((a, b) => { - return a.created_at < b.created_at ? 1 : -1; - }); + const caseSummary = cases.slice(0, 3).map(c => { + return `${CaseTypes[c.type]} (#${c.case_number})`; + }); - const caseSummary = cases.slice(0, 3).map(c => { - return `${CaseTypes[c.type]} (#${c.case_number})`; - }); + const summaryText = cases.length > 3 ? "Last 3 cases" : "Summary"; - const summaryText = cases.length > 3 ? "Last 3 cases" : "Summary"; - - embed.fields.push({ - name: "Cases", - value: trimLines(` - Total cases: **${cases.length}** - ${summaryText}: ${caseSummary.join(", ")} - `), - }); + embed.fields.push({ + name: "Cases", + value: trimLines(` + Total cases: **${cases.length}** + ${summaryText}: ${caseSummary.join(", ")} + `), + }); + } } - msg.channel.createMessage({ embed }); } From 4623475fb5d03d7f187b2d315ccbb1d731b93a09 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sat, 11 Jan 2020 02:05:54 +1100 Subject: [PATCH 60/67] Cleaned up code --- backend/src/plugins/Utility.ts | 86 ++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 51c85815..ce89b98e 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -832,6 +832,21 @@ export class UtilityPlugin extends ZeppelinPlugin { Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** `), }); + if (member) { + const joinedAt = moment(member.joinedAt); + const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + largest: 2, + round: true, + }); + embed.fields[0].value += `\nJoined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**` + } else { + embed.fields.push({ + name: "!! USER IS NOT ON THE SERVER !!", + value: embedPadding, + }); + } + msg.channel.createMessage({ embed }); + return; } else{ embed.fields.push({ @@ -856,61 +871,52 @@ export class UtilityPlugin extends ZeppelinPlugin { }); const roles = member.roles.map(id => this.guild.roles.get(id)).filter(r => !!r); - if(args.compact){ - embed.fields[0].value += `\n` + trimLines(`Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**`); - } - else{ + embed.fields.push({ + name: "Member information", + value: + trimLines(` + Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} + `) + embedPadding, + }); + const voiceChannel = member.voiceState.channelID ? this.guild.channels.get(member.voiceState.channelID) : null; + if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { embed.fields.push({ - name: "Member information", + name: "Voice information", value: trimLines(` - Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})** - ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} - `) + embedPadding, + ${voiceChannel ? `Current voice channel: **${voiceChannel ? voiceChannel.name : "None"}**` : ""} + ${member.voiceState.mute ? "Server voice muted: **Yes**" : ""} + ${member.voiceState.deaf ? "Server voice deafened: **Yes**" : ""} + `) + embedPadding, }); } - if(!args.compact){ - const voiceChannel = member.voiceState.channelID ? this.guild.channels.get(member.voiceState.channelID) : null; - if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { - embed.fields.push({ - name: "Voice information", - value: - trimLines(` - ${voiceChannel ? `Current voice channel: **${voiceChannel ? voiceChannel.name : "None"}**` : ""} - ${member.voiceState.mute ? "Server voice muted: **Yes**" : ""} - ${member.voiceState.deaf ? "Server voice deafened: **Yes**" : ""} - `) + embedPadding, - }); - } - } } else { embed.fields.push({ name: "!! USER IS NOT ON THE SERVER !!", value: embedPadding, }); } - if(!args.compact){ - const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); + const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); - if (cases.length > 0) { - cases.sort((a, b) => { - return a.created_at < b.created_at ? 1 : -1; - }); + if (cases.length > 0) { + cases.sort((a, b) => { + return a.created_at < b.created_at ? 1 : -1; + }); - const caseSummary = cases.slice(0, 3).map(c => { - return `${CaseTypes[c.type]} (#${c.case_number})`; - }); + const caseSummary = cases.slice(0, 3).map(c => { + return `${CaseTypes[c.type]} (#${c.case_number})`; + }); - const summaryText = cases.length > 3 ? "Last 3 cases" : "Summary"; + const summaryText = cases.length > 3 ? "Last 3 cases" : "Summary"; - embed.fields.push({ - name: "Cases", - value: trimLines(` - Total cases: **${cases.length}** - ${summaryText}: ${caseSummary.join(", ")} - `), - }); - } + embed.fields.push({ + name: "Cases", + value: trimLines(` + Total cases: **${cases.length}** + ${summaryText}: ${caseSummary.join(", ")} + `), + }); } msg.channel.createMessage({ embed }); } From 1d1de40e35390863c448fdd1de87b9f1e582fd9d Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Sat, 11 Jan 2020 10:26:47 +1100 Subject: [PATCH 61/67] fixed whitespace --- backend/src/plugins/Utility.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index ce89b98e..c083b988 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -879,6 +879,7 @@ export class UtilityPlugin extends ZeppelinPlugin { ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} `) + embedPadding, }); + const voiceChannel = member.voiceState.channelID ? this.guild.channels.get(member.voiceState.channelID) : null; if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { embed.fields.push({ @@ -896,6 +897,7 @@ export class UtilityPlugin extends ZeppelinPlugin { name: "!! USER IS NOT ON THE SERVER !!", value: embedPadding, }); + } const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); @@ -918,6 +920,7 @@ export class UtilityPlugin extends ZeppelinPlugin { `), }); } + msg.channel.createMessage({ embed }); } From 59a927ba93a457c45416b9b0d88c07e65cd29f3c Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 10:28:38 +0200 Subject: [PATCH 62/67] Update to Knub 28, improve debugger-friendliness Development npm scripts now also listen for debuggers: - Port 9229 for the bot - Port 9239 for the api Via Knub 28, PluginErrors are no longer used in development, which helps with call stacks in debuggers (see Knub changelog). Unhandled promise rejections are now treated as exceptions via nodejs flag --unhandled-rejections=strict, which allows catching them with a debugger. The internal "error-tolerant" error handler is now only used in production; in development, all unhandled errors cause the bot to crash and are easily catchable by debuggers. --- backend/package-lock.json | 6 +++--- backend/package.json | 10 ++++----- backend/src/index.ts | 45 ++++++++++++++++++++------------------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index ff495015..fa229d2a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2776,9 +2776,9 @@ } }, "knub": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/knub/-/knub-27.0.0.tgz", - "integrity": "sha512-iA332KoD2LN4R6c24HFGxQc8QH2GwG5Az97Rie5eqT5KFiWV/f5UJ8EfWhIJ/bOmMHubtJ4j+tEdQIBidC+WxA==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/knub/-/knub-28.0.0.tgz", + "integrity": "sha512-dhLjlQP32AbkXoD0tUFrqa8/Emh/U5lvYXNCz+bIWiAlzYr1cQKRuh9qNjAL944sCWmtfhKXVMXRFS99P6M7qw==", "requires": { "escape-string-regexp": "^2.0.0", "knub-command-manager": "^7.0.0", diff --git a/backend/package.json b/backend/package.json index d8be4984..d110bee6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,10 +6,10 @@ "scripts": { "watch": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node start-dev.js\"", "build": "rimraf dist && tsc", - "start-bot-dev": "cross-env NODE_ENV=development node -r source-map-support/register -r ./register-tsconfig-paths.js dist/backend/src/index.js", - "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/index.js", - "start-api-dev": "cross-env NODE_ENV=development node -r source-map-support/register -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", - "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js dist/backend/src/api/index.js", + "start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=127.0.0.1:9229 dist/backend/src/index.js", + "start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict dist/backend/src/index.js", + "start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=127.0.0.1:9239 dist/backend/src/api/index.js", + "start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict dist/backend/src/api/index.js", "typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js", "migrate-prod": "npm run typeorm -- migration:run", "migrate-dev": "npm run build && npm run typeorm -- migration:run", @@ -32,7 +32,7 @@ "humanize-duration": "^3.15.0", "io-ts": "^2.0.0", "js-yaml": "^3.13.1", - "knub": "^27.0.0", + "knub": "^28.0.0", "knub-command-manager": "^7.0.0", "last-commit-log": "^2.1.0", "lodash.chunk": "^4.2.0", diff --git a/backend/src/index.ts b/backend/src/index.ts index 8490b3be..1128735d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,33 +24,34 @@ const RECENT_DISCORD_ERROR_EXIT_THRESHOLD = 5; setInterval(() => (recentPluginErrors = Math.max(0, recentPluginErrors - 1)), 2500); setInterval(() => (recentDiscordErrors = Math.max(0, recentDiscordErrors - 1)), 2500); -function errorHandler(err) { - // tslint:disable:no-console - console.error(err); +if (process.env.NODE_ENV === "production") { + const errorHandler = err => { + // tslint:disable:no-console + console.error(err); - if (err instanceof PluginError) { - // Tolerate a few recent plugin errors before crashing - if (++recentPluginErrors >= RECENT_PLUGIN_ERROR_EXIT_THRESHOLD) { - console.error(`Exiting after ${RECENT_PLUGIN_ERROR_EXIT_THRESHOLD} plugin errors`); + if (err instanceof PluginError) { + // Tolerate a few recent plugin errors before crashing + if (++recentPluginErrors >= RECENT_PLUGIN_ERROR_EXIT_THRESHOLD) { + console.error(`Exiting after ${RECENT_PLUGIN_ERROR_EXIT_THRESHOLD} plugin errors`); + process.exit(1); + } + } else if (err instanceof DiscordRESTError || err instanceof DiscordHTTPError) { + // Discord API errors, usually safe to just log instead of crash + // We still bail if we get a ton of them in a short amount of time + if (++recentDiscordErrors >= RECENT_DISCORD_ERROR_EXIT_THRESHOLD) { + console.error(`Exiting after ${RECENT_DISCORD_ERROR_EXIT_THRESHOLD} API errors`); + process.exit(1); + } + } else { + // On other errors, crash immediately process.exit(1); } - } else if (err instanceof DiscordRESTError || err instanceof DiscordHTTPError) { - // Discord API errors, usually safe to just log instead of crash - // We still bail if we get a ton of them in a short amount of time - if (++recentDiscordErrors >= RECENT_DISCORD_ERROR_EXIT_THRESHOLD) { - console.error(`Exiting after ${RECENT_DISCORD_ERROR_EXIT_THRESHOLD} API errors`); - process.exit(1); - } - } else { - // On other errors, crash immediately - process.exit(1); - } - // tslint:enable:no-console + // tslint:enable:no-console + }; + + process.on("uncaughtException", errorHandler); } -process.on("unhandledRejection", errorHandler); -process.on("uncaughtException", errorHandler); - // Verify required Node.js version const REQUIRED_NODE_VERSION = "10.14.2"; const requiredParts = REQUIRED_NODE_VERSION.split(".").map(v => parseInt(v, 10)); From c1b7967d106b275724e2ca51a4c2ef1df49b2d40 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 11:38:12 +0200 Subject: [PATCH 63/67] canActOn: add option to allow same level --- backend/src/plugins/ZeppelinPlugin.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index 65eecbaa..b0256611 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -71,14 +71,14 @@ export class ZeppelinPlugin extends Plug throw new PluginRuntimeError(message, this.runtimePluginName, this.guildId); } - protected canActOn(member1, member2) { - if (member1.id === member2.id || member2.id === this.bot.user.id) { + protected canActOn(member1: Member, member2: Member, allowSameLevel = false) { + if (member2.id === this.bot.user.id) { return false; } const ourLevel = this.getMemberLevel(member1); const memberLevel = this.getMemberLevel(member2); - return ourLevel > memberLevel; + return allowSameLevel ? ourLevel >= memberLevel : ourLevel > memberLevel; } /** @@ -241,7 +241,7 @@ export class ZeppelinPlugin extends Plug /** * Resolves a role from the passed string. The passed string can be a role ID, a role mention or a role name. * In the event of duplicate role names, this function will return the first one it comes across. - * @param roleResolvable + * @param roleResolvable */ async resolveRoleId(roleResolvable: string): Promise { const roleId = await resolveRoleId(this.bot, this.guildId, roleResolvable); From a0881e8298c595f042ecb1194ec719619735a981 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 11:39:26 +0200 Subject: [PATCH 64/67] resolveRoleId: fix return type --- backend/src/plugins/ZeppelinPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index b0256611..02807738 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -243,7 +243,7 @@ export class ZeppelinPlugin extends Plug * In the event of duplicate role names, this function will return the first one it comes across. * @param roleResolvable */ - async resolveRoleId(roleResolvable: string): Promise { + async resolveRoleId(roleResolvable: string): Promise { const roleId = await resolveRoleId(this.bot, this.guildId, roleResolvable); return roleId; } From bf44a04e2c03a0bf0cfc90c8f9710e98f0a4e14a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 11:39:54 +0200 Subject: [PATCH 65/67] Tweaks to Roles plugin before merging Separate role adding/removing to two separate commands for clearer help pages and to remove the conflict with the self-assignable roles plugin. Require the assignable_roles option to always be an array (even if an empty one). Allow role assignments to self. Log role additions/removals via these commands with the right moderator name (instead of relying on the auto-log from the event). --- backend/src/plugins/Roles.ts | 162 +++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 66 deletions(-) diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts index 1355333e..2ae0db00 100644 --- a/backend/src/plugins/Roles.ts +++ b/backend/src/plugins/Roles.ts @@ -1,21 +1,17 @@ import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; import * as t from "io-ts"; -import { tNullable } from "../utils"; +import { stripObjectToScalars, tNullable } from "../utils"; import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub"; -import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; +import { Attachment, Constants as ErisConstants, Guild, GuildChannel, Member, Message, TextChannel, User } from "eris"; import { GuildLogs } from "../data/GuildLogs"; +import { LogType } from "../data/LogType"; const ConfigSchema = t.type({ - can_assign: t.boolean, - assignable_roles: tNullable(t.array(t.string)) - }); + can_assign: t.boolean, + assignable_roles: t.array(t.string), +}); type TConfigSchema = t.TypeOf; -enum RoleActions{ - Add = 1, - Remove -}; - export class RolesPlugin extends ZeppelinPlugin { public static pluginName = "roles"; public static configSchema = ConfigSchema; @@ -26,9 +22,10 @@ export class RolesPlugin extends ZeppelinPlugin { Enables authorised users to add and remove whitelisted roles with a command. `), }; + protected logs: GuildLogs; - onLoad(){ + onLoad() { this.logs = new GuildLogs(this.guildId); } @@ -36,7 +33,7 @@ export class RolesPlugin extends ZeppelinPlugin { return { config: { can_assign: false, - assignable_roles: null + assignable_roles: [], }, overrides: [ { @@ -49,68 +46,101 @@ export class RolesPlugin extends ZeppelinPlugin { }; } - - @d.command("role", " [role:string$]",{ - extra: { - info: { - description: "Assign a permitted role to a user", - }, + @d.command("addrole", " [role:string$]", { + extra: { + info: { + description: "Add a role to the specified member", }, - }) + }, + }) @d.permission("can_assign") - async assignRole(msg: Message, args: {action: string; user: string; role: string}){ - const user = await this.resolveUser(args.user); + async addRoleCmd(msg: Message, args: { member: Member; role: string }) { + if (!this.canActOn(msg.member, args.member, true)) { + return this.sendErrorMessage(msg.channel, "Cannot add roles to this user: insufficient permissions"); + } + const roleId = await this.resolveRoleId(args.role); - if (user.discriminator == "0000") { - return this.sendErrorMessage(msg.channel, `User not found`); + if (!roleId) { + return this.sendErrorMessage(msg.channel, "Invalid role id"); } - //if the role doesnt exist, we can exit - let roleIds = (msg.channel as TextChannel).guild.roles.map(x => x.id) - if(!(roleIds.includes(roleId))){ - return this.sendErrorMessage(msg.channel, `Role not found`); - } - - // If the user exists as a guild member, make sure we can act on them first - const targetMember = await this.getMember(user.id); - if (targetMember && !this.canActOn(msg.member, targetMember)) { - this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: insufficient permissions"); - return; + const config = this.getConfigForMsg(msg); + if (!config.assignable_roles.includes(roleId)) { + return this.sendErrorMessage(msg.channel, "You cannot assign that role"); } - const action: string = args.action[0].toUpperCase() + args.action.slice(1).toLowerCase(); - if(!RoleActions[action]){ - this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: invalid action"); - return; + // Sanity check: make sure the role is configured properly + const role = (msg.channel as GuildChannel).guild.roles.get(roleId); + if (!role) { + this.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return this.sendErrorMessage(msg.channel, "You cannot assign that role"); } - //check if the role is allowed to be applied - let config = this.getConfigForMsg(msg) - if(!config.assignable_roles || !config.assignable_roles.includes(roleId)){ - this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); - return; - } - //at this point, everything has been verified, so it's ACTION TIME - switch(RoleActions[action]){ - case RoleActions.Add: - if(targetMember.roles.includes(roleId)){ - this.sendErrorMessage(msg.channel, "Role already applied to user"); - return; - } - await this.bot.addGuildMemberRole(this.guildId, user.id, roleId); - this.sendSuccessMessage(msg.channel, `Role added to user!`); - break; - case RoleActions.Remove: - if(!targetMember.roles.includes(roleId)){ - this.sendErrorMessage(msg.channel, "User does not have role"); - return; - } - await this.bot.removeGuildMemberRole(this.guildId, user.id, roleId); - this.sendSuccessMessage(msg.channel, `Role removed from user!`); - break; - default: - break; + if (args.member.roles.includes(roleId)) { + return this.sendErrorMessage(msg.channel, "Member already has that role"); } + + this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id); + + await args.member.addRole(roleId); + + this.logs.log(LogType.MEMBER_ROLE_ADD, { + member: stripObjectToScalars(args.member, ["user", "roles"]), + roles: role.name, + mod: stripObjectToScalars(msg.author), + }); + + this.sendSuccessMessage(msg.channel, "Role added to user!"); } - -} \ No newline at end of file + + @d.command("removerole", " [role:string$]", { + extra: { + info: { + description: "Remove a role from the specified member", + }, + }, + }) + @d.permission("can_assign") + async removeRoleCmd(msg: Message, args: { member: Member; role: string }) { + if (!this.canActOn(msg.member, args.member, true)) { + return this.sendErrorMessage(msg.channel, "Cannot remove roles from this user: insufficient permissions"); + } + + const roleId = await this.resolveRoleId(args.role); + if (!roleId) { + return this.sendErrorMessage(msg.channel, "Invalid role id"); + } + + const config = this.getConfigForMsg(msg); + if (!config.assignable_roles.includes(roleId)) { + return this.sendErrorMessage(msg.channel, "You cannot remove that role"); + } + + // Sanity check: make sure the role is configured properly + const role = (msg.channel as GuildChannel).guild.roles.get(roleId); + if (!role) { + this.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return this.sendErrorMessage(msg.channel, "You cannot remove that role"); + } + + if (!args.member.roles.includes(roleId)) { + return this.sendErrorMessage(msg.channel, "Member doesn't have that role"); + } + + this.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id); + + await args.member.removeRole(roleId); + + this.logs.log(LogType.MEMBER_ROLE_REMOVE, { + member: stripObjectToScalars(args.member, ["user", "roles"]), + roles: role.name, + mod: stripObjectToScalars(msg.author), + }); + + this.sendSuccessMessage(msg.channel, "Role removed from user!"); + } +} From 8c36e5fa0115e7101b1c340d6a8be0fc888e01d6 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 11:43:56 +0200 Subject: [PATCH 66/67] logs: make sure the passed member object is stripped --- backend/src/plugins/Logs.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/plugins/Logs.ts b/backend/src/plugins/Logs.ts index 921d87e3..a3f7cf75 100644 --- a/backend/src/plugins/Logs.ts +++ b/backend/src/plugins/Logs.ts @@ -356,9 +356,11 @@ export class LogsPlugin extends ZeppelinPlugin { async onMemberUpdate(_, member: Member, oldMember: Member) { if (!oldMember) return; + const logMember = stripObjectToScalars(member, ["user", "roles"]); + if (member.nick !== oldMember.nick) { this.guildLogs.log(LogType.MEMBER_NICK_CHANGE, { - member, + member: logMember, oldNick: oldMember.nick != null ? oldMember.nick : "", newNick: member.nick != null ? member.nick : "", }); @@ -379,7 +381,7 @@ export class LogsPlugin extends ZeppelinPlugin { this.guildLogs.log( LogType.MEMBER_ROLE_CHANGES, { - member, + member: logMember, addedRoles: addedRoles .map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) @@ -397,7 +399,7 @@ export class LogsPlugin extends ZeppelinPlugin { this.guildLogs.log( LogType.MEMBER_ROLE_ADD, { - member, + member: logMember, roles: addedRoles .map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) @@ -411,7 +413,7 @@ export class LogsPlugin extends ZeppelinPlugin { this.guildLogs.log( LogType.MEMBER_ROLE_REMOVE, { - member, + member: logMember, roles: removedRoles .map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) From 1aceb55a879e9a1cc366a062a8f1225cdda7ab3a Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 12 Jan 2020 11:47:54 +0200 Subject: [PATCH 67/67] Run prettier and check tslint on entire codebase Mainly to run these checks for the recent pull requests. --- backend/src/data/GuildLogs.ts | 5 +++- backend/src/data/GuildReminders.ts | 2 +- backend/src/data/entities/ApiLogin.ts | 5 +++- .../data/entities/ApiPermissionAssignment.ts | 5 +++- backend/src/data/entities/ApiUserInfo.ts | 10 +++++-- backend/src/data/entities/Case.ts | 5 +++- backend/src/data/entities/CaseNote.ts | 5 +++- backend/src/data/entities/Reminder.ts | 2 +- ...8445483917-CreateReminderCreatedAtField.ts | 28 +++++++++--------- backend/src/plugins/MessageSaver.ts | 5 +++- backend/src/plugins/ModActions.ts | 5 +++- backend/src/plugins/Reminders.ts | 29 +++++++++++-------- backend/src/plugins/Utility.ts | 23 +++++++-------- backend/src/plugins/availablePlugins.ts | 2 +- backend/src/utils.ts | 18 ++++++------ 15 files changed, 89 insertions(+), 60 deletions(-) diff --git a/backend/src/data/GuildLogs.ts b/backend/src/data/GuildLogs.ts index 042e5e5f..58488906 100644 --- a/backend/src/data/GuildLogs.ts +++ b/backend/src/data/GuildLogs.ts @@ -50,6 +50,9 @@ export class GuildLogs extends EventEmitter { } clearIgnoredLog(type: LogType, ignoreId: any) { - this.ignoredLogs.splice(this.ignoredLogs.findIndex(info => type === info.type && ignoreId === info.ignoreId), 1); + this.ignoredLogs.splice( + this.ignoredLogs.findIndex(info => type === info.type && ignoreId === info.ignoreId), + 1, + ); } } diff --git a/backend/src/data/GuildReminders.ts b/backend/src/data/GuildReminders.ts index 6425ff61..b370b465 100644 --- a/backend/src/data/GuildReminders.ts +++ b/backend/src/data/GuildReminders.ts @@ -41,7 +41,7 @@ export class GuildReminders extends BaseGuildRepository { channel_id: channelId, remind_at: remindAt, body, - created_at + created_at, }); } } diff --git a/backend/src/data/entities/ApiLogin.ts b/backend/src/data/entities/ApiLogin.ts index 76204224..5dc7ebeb 100644 --- a/backend/src/data/entities/ApiLogin.ts +++ b/backend/src/data/entities/ApiLogin.ts @@ -19,7 +19,10 @@ export class ApiLogin { @Column() expires_at: string; - @ManyToOne(type => ApiUserInfo, userInfo => userInfo.logins) + @ManyToOne( + type => ApiUserInfo, + userInfo => userInfo.logins, + ) @JoinColumn({ name: "user_id" }) userInfo: ApiUserInfo; } diff --git a/backend/src/data/entities/ApiPermissionAssignment.ts b/backend/src/data/entities/ApiPermissionAssignment.ts index b97ce26b..c454206c 100644 --- a/backend/src/data/entities/ApiPermissionAssignment.ts +++ b/backend/src/data/entities/ApiPermissionAssignment.ts @@ -18,7 +18,10 @@ export class ApiPermissionAssignment { @Column("simple-array") permissions: string[]; - @ManyToOne(type => ApiUserInfo, userInfo => userInfo.permissionAssignments) + @ManyToOne( + type => ApiUserInfo, + userInfo => userInfo.permissionAssignments, + ) @JoinColumn({ name: "target_id" }) userInfo: ApiUserInfo; } diff --git a/backend/src/data/entities/ApiUserInfo.ts b/backend/src/data/entities/ApiUserInfo.ts index 9d36eb2c..e546db60 100644 --- a/backend/src/data/entities/ApiUserInfo.ts +++ b/backend/src/data/entities/ApiUserInfo.ts @@ -20,9 +20,15 @@ export class ApiUserInfo { @Column() updated_at: string; - @OneToMany(type => ApiLogin, login => login.userInfo) + @OneToMany( + type => ApiLogin, + login => login.userInfo, + ) logins: ApiLogin[]; - @OneToMany(type => ApiPermissionAssignment, p => p.userInfo) + @OneToMany( + type => ApiPermissionAssignment, + p => p.userInfo, + ) permissionAssignments: ApiPermissionAssignment[]; } diff --git a/backend/src/data/entities/Case.ts b/backend/src/data/entities/Case.ts index 88a920e3..5b65e75a 100644 --- a/backend/src/data/entities/Case.ts +++ b/backend/src/data/entities/Case.ts @@ -29,6 +29,9 @@ export class Case { @Column() pp_name: string; - @OneToMany(type => CaseNote, note => note.case) + @OneToMany( + type => CaseNote, + note => note.case, + ) notes: CaseNote[]; } diff --git a/backend/src/data/entities/CaseNote.ts b/backend/src/data/entities/CaseNote.ts index 109f72de..4541717c 100644 --- a/backend/src/data/entities/CaseNote.ts +++ b/backend/src/data/entities/CaseNote.ts @@ -15,7 +15,10 @@ export class CaseNote { @Column() created_at: string; - @ManyToOne(type => Case, theCase => theCase.notes) + @ManyToOne( + type => Case, + theCase => theCase.notes, + ) @JoinColumn({ name: "case_id" }) case: Case; } diff --git a/backend/src/data/entities/Reminder.ts b/backend/src/data/entities/Reminder.ts index b232c796..d7e33972 100644 --- a/backend/src/data/entities/Reminder.ts +++ b/backend/src/data/entities/Reminder.ts @@ -16,5 +16,5 @@ export class Reminder { @Column() body: string; - @Column() created_at: string + @Column() created_at: string; } diff --git a/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts b/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts index b078d18f..74ba8cf8 100644 --- a/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts +++ b/backend/src/migrations/1578445483917-CreateReminderCreatedAtField.ts @@ -1,18 +1,18 @@ -import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"; +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; export class CreateReminderCreatedAtField1578445483917 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + "reminders", + new TableColumn({ + name: "created_at", + type: "datetime", + isNullable: false, + }), + ); + } - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumn("reminders", - new TableColumn({ - name: "created_at", - type: "datetime", - isNullable: false - })); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn("reminders", "created_at"); - } - + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("reminders", "created_at"); + } } diff --git a/backend/src/plugins/MessageSaver.ts b/backend/src/plugins/MessageSaver.ts index d8567b05..ae413f33 100644 --- a/backend/src/plugins/MessageSaver.ts +++ b/backend/src/plugins/MessageSaver.ts @@ -125,7 +125,10 @@ export class MessageSaverPlugin extends ZeppelinPlugin { await msg.channel.createMessage(`Saving pins from <#${args.channel.id}>...`); const pins = await args.channel.getPins(); - const { savedCount, failed } = await this.saveMessagesToDB(args.channel, pins.map(m => m.id)); + const { savedCount, failed } = await this.saveMessagesToDB( + args.channel, + pins.map(m => m.id), + ); if (failed.length) { msg.channel.createMessage( diff --git a/backend/src/plugins/ModActions.ts b/backend/src/plugins/ModActions.ts index 7d4e28dc..9e0ce862 100644 --- a/backend/src/plugins/ModActions.ts +++ b/backend/src/plugins/ModActions.ts @@ -197,7 +197,10 @@ export class ModActionsPlugin extends ZeppelinPlugin { } clearIgnoredEvent(type: IgnoredEventType, userId: any) { - this.ignoredEvents.splice(this.ignoredEvents.findIndex(info => type === info.type && userId === info.userId), 1); + this.ignoredEvents.splice( + this.ignoredEvents.findIndex(info => type === info.type && userId === info.userId), + 1, + ); } formatReasonWithAttachments(reason: string, attachments: Attachment[]) { diff --git a/backend/src/plugins/Reminders.ts b/backend/src/plugins/Reminders.ts index 3f15161a..dd1767e0 100644 --- a/backend/src/plugins/Reminders.ts +++ b/backend/src/plugins/Reminders.ts @@ -70,19 +70,18 @@ export class RemindersPlugin extends ZeppelinPlugin { const channel = this.guild.channels.get(reminder.channel_id); if (channel && channel instanceof TextChannel) { try { - //Only show created at date if one exists - if(moment(reminder.created_at).isValid()){ + // Only show created at date if one exists + if (moment(reminder.created_at).isValid()) { const target = moment(); - const diff = target.diff(moment(reminder.created_at , "YYYY-MM-DD HH:mm:ss")); - const result = humanizeDuration(diff, { largest: 2, round: true }); + const diff = target.diff(moment(reminder.created_at, "YYYY-MM-DD HH:mm:ss")); + const result = humanizeDuration(diff, { largest: 2, round: true }); await channel.createMessage( - disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body} \n\`Set at ${reminder.created_at} (${result} ago)\``), - ); - } - else{ - await channel.createMessage( - disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body}`), + disableLinkPreviews( + `Reminder for <@!${reminder.user_id}>: ${reminder.body} \n\`Set at ${reminder.created_at} (${result} ago)\``, + ), ); + } else { + await channel.createMessage(disableLinkPreviews(`Reminder for <@!${reminder.user_id}>: ${reminder.body}`)); } } catch (e) { // Probably random Discord internal server error or missing permissions or somesuch @@ -138,7 +137,13 @@ export class RemindersPlugin extends ZeppelinPlugin { } const reminderBody = args.reminder || `https://discordapp.com/channels/${this.guildId}/${msg.channel.id}/${msg.id}`; - await this.reminders.add(msg.author.id, msg.channel.id, reminderTime.format("YYYY-MM-DD HH:mm:ss"), reminderBody, moment().format("YYYY-MM-DD HH:mm:ss")); + await this.reminders.add( + msg.author.id, + msg.channel.id, + reminderTime.format("YYYY-MM-DD HH:mm:ss"), + reminderBody, + moment().format("YYYY-MM-DD HH:mm:ss"), + ); const msUntilReminder = reminderTime.diff(now); const timeUntilReminder = humanizeDuration(msUntilReminder, { largest: 2, round: true }); @@ -163,7 +168,7 @@ export class RemindersPlugin extends ZeppelinPlugin { const lines = Array.from(reminders.entries()).map(([i, reminder]) => { const num = i + 1; const paddedNum = num.toString().padStart(longestNum, " "); - const target = moment(reminder.remind_at , "YYYY-MM-DD HH:mm:ss"); + const target = moment(reminder.remind_at, "YYYY-MM-DD HH:mm:ss"); const diff = target.diff(moment()); const result = humanizeDuration(diff, { largest: 2, round: true }); return `\`${paddedNum}.\` \`${reminder.remind_at} (${result})\` ${reminder.body}`; diff --git a/backend/src/plugins/Utility.ts b/backend/src/plugins/Utility.ts index 3db9b28b..348cb90a 100644 --- a/backend/src/plugins/Utility.ts +++ b/backend/src/plugins/Utility.ts @@ -797,11 +797,11 @@ export class UtilityPlugin extends ZeppelinPlugin { name: "compact", shortcut: "c", isSwitch: true, - } - ] + }, + ], }) @d.permission("can_info") - async infoCmd(msg: Message, args: { user?: User | UnknownUser, compact?: boolean }) { + async infoCmd(msg: Message, args: { user?: User | UnknownUser; compact?: boolean }) { const user = args.user || msg.author; let member; @@ -823,22 +823,21 @@ export class UtilityPlugin extends ZeppelinPlugin { embed.title = `${user.username}#${user.discriminator}`; embed.thumbnail = { url: user.avatarURL }; - if(args.compact){ + if (args.compact) { embed.fields.push({ name: "User information", - value: - trimLines(` + value: trimLines(` Profile: <@!${user.id}> Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** `), - }); + }); if (member) { const joinedAt = moment(member.joinedAt); const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { largest: 2, round: true, }); - embed.fields[0].value += `\nJoined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**` + embed.fields[0].value += `\nJoined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**`; } else { embed.fields.push({ name: "!! USER IS NOT ON THE SERVER !!", @@ -847,8 +846,7 @@ export class UtilityPlugin extends ZeppelinPlugin { } msg.channel.createMessage({ embed }); return; - } - else{ + } else { embed.fields.push({ name: "User information", value: @@ -857,7 +855,7 @@ export class UtilityPlugin extends ZeppelinPlugin { Profile: <@!${user.id}> Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** `) + embedPadding, - }); + }); } } else { embed.title = `Unknown user`; @@ -897,7 +895,6 @@ export class UtilityPlugin extends ZeppelinPlugin { name: "!! USER IS NOT ON THE SERVER !!", value: embedPadding, }); - } const cases = (await this.cases.getByUserId(user.id)).filter(c => !c.is_hidden); @@ -920,7 +917,7 @@ export class UtilityPlugin extends ZeppelinPlugin { `), }); } - + msg.channel.createMessage({ embed }); } diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 8e6d3374..f30034ff 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -27,7 +27,7 @@ import { LocatePlugin } from "./LocateUser"; import { GuildConfigReloader } from "./GuildConfigReloader"; import { ChannelArchiverPlugin } from "./ChannelArchiver"; import { AutomodPlugin } from "./Automod"; -import { RolesPlugin} from "./Roles"; +import { RolesPlugin } from "./Roles"; /** * Plugins available to be loaded for individual guilds diff --git a/backend/src/utils.ts b/backend/src/utils.ts index f12a04e1..1cb5af74 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -956,25 +956,25 @@ export async function resolveMember(bot: Client, guild: Guild, value: string): P return null; } -export async function resolveRoleId(bot: Client, guildId: string, value: string){ - if(value == null){ +export async function resolveRoleId(bot: Client, guildId: string, value: string) { + if (value == null) { return null; } - //role mention + // Role mention const mentionMatch = value.match(/^<@&?(\d+)>$/); - if(mentionMatch){ + if (mentionMatch) { return mentionMatch[1]; } - //role name - let roleList = await bot.getRESTGuildRoles(guildId); - let role = roleList.filter(x => x.name.toLocaleLowerCase() == value.toLocaleLowerCase()); - if(role[0]){ + // Role name + const roleList = await bot.getRESTGuildRoles(guildId); + const role = roleList.filter(x => x.name.toLocaleLowerCase() === value.toLocaleLowerCase()); + if (role[0]) { return role[0].id; } - //role ID + // Role ID const idMatch = value.match(/^\d+$/); if (idMatch) { return value;