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) => {