diff --git a/src/QueuedEventEmitter.ts b/src/QueuedEventEmitter.ts index 62c4f542..5ac30890 100644 --- a/src/QueuedEventEmitter.ts +++ b/src/QueuedEventEmitter.ts @@ -11,12 +11,13 @@ export class QueuedEventEmitter { this.queue = new Queue(); } - on(eventName: string, listener: Listener) { + on(eventName: string, listener: Listener): Listener { if (!this.listeners.has(eventName)) { this.listeners.set(eventName, []); } this.listeners.get(eventName).push(listener); + return listener; } off(eventName: string, listener: Listener) { @@ -28,6 +29,15 @@ export class QueuedEventEmitter { listeners.splice(listeners.indexOf(listener), 1); } + once(eventName: string, listener: Listener): Listener { + const handler = this.on(eventName, (...args) => { + const result = listener(...args); + this.off(eventName, handler); + return result; + }); + return handler; + } + emit(eventName: string, args: any[] = []): Promise { const listeners = [...(this.listeners.get(eventName) || []), ...(this.listeners.get("*") || [])]; diff --git a/src/data/GuildSavedMessages.ts b/src/data/GuildSavedMessages.ts index 0dc040e1..e0b903e6 100644 --- a/src/data/GuildSavedMessages.ts +++ b/src/data/GuildSavedMessages.ts @@ -146,6 +146,7 @@ export class GuildSavedMessages extends BaseRepository { const inserted = await this.messages.findOne(data.id); this.events.emit("create", [inserted]); + this.events.emit(`create:${data.id}`, [inserted]); } async createFromMsg(msg: Message, overrides = {}) { @@ -180,6 +181,7 @@ export class GuildSavedMessages extends BaseRepository { if (deleted) { this.events.emit("delete", [deleted]); + this.events.emit(`delete:${id}`, [deleted]); } } @@ -227,6 +229,7 @@ export class GuildSavedMessages extends BaseRepository { ); this.events.emit("update", [newMessage, oldMessage]); + this.events.emit(`update:${id}`, [newMessage, oldMessage]); } async saveEditFromMsg(msg: Message) { @@ -247,4 +250,31 @@ export class GuildSavedMessages extends BaseRepository { this.toBePermanent.add(id); } } + + async onceMessageAvailable(id: string, handler: (msg: SavedMessage) => any, timeout: number = 60 * 1000) { + let called = false; + let onceEventListener; + let timeoutFn; + + const callHandler = async (msg: SavedMessage) => { + this.events.off(`create:${id}`, onceEventListener); + clearTimeout(timeoutFn); + + if (called) return; + called = true; + + await handler(msg); + }; + + onceEventListener = this.events.once(`create:${id}`, callHandler); + timeoutFn = setTimeout(() => { + called = true; + callHandler(null); + }, timeout); + + const messageInDB = await this.find(id); + if (messageInDB) { + callHandler(messageInDB); + } + } } diff --git a/src/data/GuildTags.ts b/src/data/GuildTags.ts index df3cfdf1..f403ebe2 100644 --- a/src/data/GuildTags.ts +++ b/src/data/GuildTags.ts @@ -1,13 +1,16 @@ import { Tag } from "./entities/Tag"; import { getRepository, Repository } from "typeorm"; import { BaseRepository } from "./BaseRepository"; +import { TagResponse } from "./entities/TagResponse"; export class GuildTags extends BaseRepository { private tags: Repository; + private tagResponses: Repository; constructor(guildId) { super(guildId); this.tags = getRepository(Tag); + this.tagResponses = getRepository(TagResponse); } async all(): Promise { @@ -57,4 +60,30 @@ export class GuildTags extends BaseRepository { tag }); } + + async findResponseByCommandMessageId(messageId: string): Promise { + return this.tagResponses.findOne({ + where: { + guild_id: this.guildId, + command_message_id: messageId + } + }); + } + + async findResponseByResponseMessageId(messageId: string): Promise { + return this.tagResponses.findOne({ + where: { + guild_id: this.guildId, + response_message_id: messageId + } + }); + } + + async addResponse(cmdMessageId, responseMessageId) { + await this.tagResponses.insert({ + guild_id: this.guildId, + command_message_id: cmdMessageId, + response_message_id: responseMessageId + }); + } } diff --git a/src/data/entities/TagResponse.ts b/src/data/entities/TagResponse.ts new file mode 100644 index 00000000..7fa00c8d --- /dev/null +++ b/src/data/entities/TagResponse.ts @@ -0,0 +1,14 @@ +import { Entity, Column, PrimaryColumn, CreateDateColumn } from "typeorm"; + +@Entity("tag_responses") +export class TagResponse { + @Column() + @PrimaryColumn() + id: string; + + @Column() guild_id: string; + + @Column() command_message_id: string; + + @Column() response_message_id: string; +} diff --git a/src/migrations/1546770935261-CreateTagResponsesTable.ts b/src/migrations/1546770935261-CreateTagResponsesTable.ts new file mode 100644 index 00000000..942f61bc --- /dev/null +++ b/src/migrations/1546770935261-CreateTagResponsesTable.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateTagResponsesTable1546770935261 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "tag_responses", + columns: [ + { + name: "id", + type: "int", + unsigned: true, + isGenerated: true, + generationStrategy: "increment", + isPrimary: true + }, + { + name: "guild_id", + type: "bigint", + unsigned: true + }, + { + name: "command_message_id", + type: "bigint", + unsigned: true + }, + { + name: "response_message_id", + type: "bigint", + unsigned: true + } + ], + indices: [ + { + columnNames: ["guild_id"] + } + ], + foreignKeys: [ + { + columnNames: ["command_message_id"], + referencedTableName: "messages", + referencedColumnNames: ["id"], + onDelete: "CASCADE" + }, + { + columnNames: ["response_message_id"], + referencedTableName: "messages", + referencedColumnNames: ["id"], + onDelete: "CASCADE" + } + ] + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("tag_responses"); + } +} diff --git a/src/plugins/Tags.ts b/src/plugins/Tags.ts index abc23c9c..fd08aa2b 100644 --- a/src/plugins/Tags.ts +++ b/src/plugins/Tags.ts @@ -1,17 +1,24 @@ import { Plugin, decorators as d } from "knub"; -import { Message } from "eris"; +import { Message, TextChannel } from "eris"; import { errorMessage, successMessage } from "../utils"; import { GuildTags } from "../data/GuildTags"; +import { GuildSavedMessages } from "../data/GuildSavedMessages"; +import { SavedMessage } from "../data/entities/SavedMessage"; export class TagsPlugin extends Plugin { public static pluginName = "tags"; protected tags: GuildTags; + protected savedMessages: GuildSavedMessages; + + private onMessageCreateFn; + private onMessageDeleteFn; getDefaultOptions() { return { config: { - prefix: "!!" + prefix: "!!", + deleteWithCommand: true }, permissions: { @@ -32,6 +39,18 @@ export class TagsPlugin extends Plugin { onLoad() { this.tags = GuildTags.getInstance(this.guildId); + this.savedMessages = GuildSavedMessages.getInstance(this.guildId); + + this.onMessageCreateFn = this.onMessageCreate.bind(this); + this.savedMessages.events.on("create", this.onMessageCreateFn); + + this.onMessageDeleteFn = this.onMessageDelete.bind(this); + this.savedMessages.events.on("delete", this.onMessageDeleteFn); + } + + onUnload() { + this.savedMessages.events.off("create", this.onMessageCreateFn); + this.savedMessages.events.off("delete", this.onMessageDeleteFn); } @d.command("tag list") @@ -72,20 +91,54 @@ export class TagsPlugin extends Plugin { msg.channel.createMessage(successMessage(`Tag set! Use it with: \`${prefix}${args.tag}\``)); } - @d.event("messageCreate") - @d.permission("use") - async onMessageCreate(msg: Message) { - if (!msg.content) return; - if (msg.type !== 0) return; - if (!msg.author || msg.author.bot) return; + async onMessageCreate(msg: SavedMessage) { + const member = this.guild.members.get(msg.user_id); + if (!this.hasPermission("use", { member, channelId: msg.channel_id })) return; - const prefix = this.configValueForMsg(msg, "prefix"); - if (!msg.content.startsWith(prefix)) return; + if (!msg.data.content) return; + if (msg.is_bot) return; - const withoutPrefix = msg.content.slice(prefix.length); + const prefix = this.configValueForMemberIdAndChannelId(msg.user_id, msg.channel_id, "prefix"); + if (!msg.data.content.startsWith(prefix)) return; + + const withoutPrefix = msg.data.content.slice(prefix.length); const tag = await this.tags.find(withoutPrefix); if (!tag) return; - msg.channel.createMessage(tag.body); + const channel = this.guild.channels.get(msg.channel_id) as TextChannel; + const responseMsg = await channel.createMessage(tag.body); + + // Save the command-response message pair once the message is in our database + this.savedMessages.onceMessageAvailable(responseMsg.id, async theMsg => { + await this.tags.addResponse(msg.id, responseMsg.id); + }); + } + + async onMessageDelete(msg: SavedMessage) { + // Command message was deleted -> delete the response as well + const commandMsgResponse = await this.tags.findResponseByCommandMessageId(msg.id); + if (commandMsgResponse) { + const channel = this.guild.channels.get(msg.channel_id) as TextChannel; + if (!channel) return; + + const responseMsg = await this.savedMessages.find(commandMsgResponse.response_message_id); + if (!responseMsg || responseMsg.deleted_at != null) return; + + await channel.deleteMessage(commandMsgResponse.response_message_id); + return; + } + + // Response was deleted -> delete the command message as well + const responseMsgResponse = await this.tags.findResponseByResponseMessageId(msg.id); + if (responseMsgResponse) { + const channel = this.guild.channels.get(msg.channel_id) as TextChannel; + if (!channel) return; + + const commandMsg = await this.savedMessages.find(responseMsgResponse.command_message_id); + if (!commandMsg || commandMsg.deleted_at != null) return; + + await channel.deleteMessage(responseMsgResponse.command_message_id); + return; + } } }