diff --git a/src/data/GuildAutoReactions.ts b/src/data/GuildAutoReactions.ts new file mode 100644 index 00000000..a0d1976c --- /dev/null +++ b/src/data/GuildAutoReactions.ts @@ -0,0 +1,57 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { AutoReaction } from "./entities/AutoReaction"; + +export class GuildAutoReactions extends BaseRepository { + private autoReactions: Repository; + + constructor(guildId) { + super(guildId); + this.autoReactions = getRepository(AutoReaction); + } + + async all(): Promise { + return this.autoReactions.find({ + where: { + guild_id: this.guildId + } + }); + } + + async getForChannel(channelId: string): Promise { + return this.autoReactions.findOne({ + where: { + guild_id: this.guildId, + channel_id: channelId + } + }); + } + + async removeFromChannel(channelId: string) { + await this.autoReactions.delete({ + guild_id: this.guildId, + channel_id: channelId + }); + } + + async set(channelId: string, reactions: string[]) { + const existingRecord = await this.getForChannel(channelId); + if (existingRecord) { + this.autoReactions.update( + { + guild_id: this.guildId, + channel_id: channelId + }, + { + reactions + } + ); + } else { + await this.autoReactions.insert({ + guild_id: this.guildId, + channel_id: channelId, + reactions + }); + } + } +} diff --git a/src/data/entities/AutoReaction.ts b/src/data/entities/AutoReaction.ts new file mode 100644 index 00000000..bb9a7278 --- /dev/null +++ b/src/data/entities/AutoReaction.ts @@ -0,0 +1,15 @@ +import { Entity, Column, PrimaryColumn } from "typeorm"; +import { ISavedMessageData } from "./SavedMessage"; + +@Entity("auto_reactions") +export class AutoReaction { + @Column() + @PrimaryColumn() + guild_id: string; + + @Column() + @PrimaryColumn() + channel_id: string; + + @Column("simple-array") reactions: string[]; +} diff --git a/src/index.ts b/src/index.ts index 658c7cbb..d251d304 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,7 @@ import { MutesPlugin } from "./plugins/Mutes"; import { SlowmodePlugin } from "./plugins/Slowmode"; import { StarboardPlugin } from "./plugins/Starboard"; import { NameHistoryPlugin } from "./plugins/NameHistory"; +import { AutoReactions } from "./plugins/AutoReactions"; // Run latest database migrations logger.info("Running database migrations"); @@ -94,7 +95,8 @@ connect().then(async conn => { SpamPlugin, TagsPlugin, SlowmodePlugin, - StarboardPlugin + StarboardPlugin, + AutoReactions ], globalPlugins: [BotControlPlugin, LogServerPlugin], diff --git a/src/migrations/1547290549908-CreateAutoReactionsTable.ts b/src/migrations/1547290549908-CreateAutoReactionsTable.ts new file mode 100644 index 00000000..385e9624 --- /dev/null +++ b/src/migrations/1547290549908-CreateAutoReactionsTable.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateAutoReactionsTable1547290549908 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "auto_reactions", + columns: [ + { + name: "guild_id", + type: "bigint", + unsigned: true + }, + { + name: "channel_id", + type: "bigint", + unsigned: true + }, + { + name: "reactions", + type: "text" + } + ] + }) + ); + await queryRunner.createPrimaryKey("auto_reactions", ["guild_id", "channel_id"]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("auto_reactions", true); + } +} diff --git a/src/plugins/AutoReactions.ts b/src/plugins/AutoReactions.ts new file mode 100644 index 00000000..6d04358e --- /dev/null +++ b/src/plugins/AutoReactions.ts @@ -0,0 +1,113 @@ +import { Plugin, decorators as d } from "knub"; +import { GuildSavedMessages } from "../data/GuildSavedMessages"; +import { SavedMessage } from "../data/entities/SavedMessage"; +import { GuildAutoReactions } from "../data/GuildAutoReactions"; +import { Message } from "eris"; +import { + CustomEmoji, + customEmojiRegex, + errorMessage, + isEmoji, + isSnowflake, + successMessage, + unicodeEmojiRegex +} from "../utils"; + +export class AutoReactions extends Plugin { + public static pluginName = "auto_reactions"; + + protected savedMessages: GuildSavedMessages; + protected autoReactions: GuildAutoReactions; + + private onMessageCreateFn; + + getDefaultOptions() { + return { + permissions: { + use: false + }, + + overrides: [ + { + level: ">=100", + permissions: { + use: true + } + } + ] + }; + } + + onLoad() { + this.savedMessages = GuildSavedMessages.getInstance(this.guildId); + this.autoReactions = GuildAutoReactions.getInstance(this.guildId); + + this.onMessageCreateFn = this.savedMessages.events.on("create", this.onMessageCreate.bind(this)); + } + + onUnload() { + this.savedMessages.events.off("create", this.onMessageCreateFn); + } + + @d.command("auto_reactions", " ") + async setAutoReactionsCmd(msg: Message, args: { channelId: string; reactions: string[] }) { + const guildEmojis = this.guild.emojis as CustomEmoji[]; + const guildEmojiIds = guildEmojis.map(e => e.id); + + const finalReactions = []; + + for (const reaction of args.reactions) { + if (!isEmoji(reaction)) { + console.log("invalid:", reaction); + msg.channel.createMessage(errorMessage("One or more of the specified reactions were invalid!")); + return; + } + + let savedValue; + + const customEmojiMatch = reaction.match(customEmojiRegex); + if (customEmojiMatch) { + // Custom emoji + if (!guildEmojiIds.includes(customEmojiMatch[0])) { + msg.channel.createMessage(errorMessage("I can only use regular emojis and custom emojis from this server")); + + return; + } + + savedValue = `${customEmojiMatch[0]}:${customEmojiMatch[1]}`; + } else { + // Unicode emoji + savedValue = reaction; + } + + finalReactions.push(savedValue); + } + + await this.autoReactions.set(args.channelId, finalReactions); + msg.channel.createMessage(successMessage(`Auto-reactions set for <#${args.channelId}>`)); + } + + @d.command("auto_reactions disable", "") + async disableAutoReactionsCmd(msg: Message, args: { channelId: string }) { + const autoReaction = await this.autoReactions.getForChannel(args.channelId); + if (!autoReaction) { + msg.channel.createMessage(errorMessage(`Auto-reactions aren't enabled in <#${args.channelId}>`)); + return; + } + + await this.autoReactions.removeFromChannel(args.channelId); + msg.channel.createMessage(successMessage(`Auto-reactions disabled in <#${args.channelId}>`)); + } + + async onMessageCreate(msg: SavedMessage) { + const autoReaction = await this.autoReactions.getForChannel(msg.channel_id); + if (!autoReaction) return; + + const realMsg = await this.bot.getMessage(msg.channel_id, msg.id); + if (!realMsg) return; + + for (const reaction of autoReaction.reactions) { + await realMsg.addReaction(reaction); + } + } +} diff --git a/src/plugins/ReactionRoles.ts b/src/plugins/ReactionRoles.ts index 1eada720..8cb57ec8 100644 --- a/src/plugins/ReactionRoles.ts +++ b/src/plugins/ReactionRoles.ts @@ -1,16 +1,12 @@ import { Plugin, decorators as d } from "knub"; -import { errorMessage, isSnowflake } from "../utils"; +import { CustomEmoji, errorMessage, isSnowflake } from "../utils"; import { GuildReactionRoles } from "../data/GuildReactionRoles"; -import { Channel, Emoji, Message, TextChannel } from "eris"; +import { Channel, Message, TextChannel } from "eris"; type ReactionRolePair = [string, string]; -type CustomEmoji = { - id: string; -} & Emoji; - export class ReactionRolesPlugin extends Plugin { - public static pluginName = 'reaction_roles'; + public static pluginName = "reaction_roles"; protected reactionRoles: GuildReactionRoles; @@ -81,7 +77,7 @@ export class ReactionRolesPlugin extends Plugin { const newRolePairs: ReactionRolePair[] = args.reactionRolePairs .trim() .split("\n") - .map(v => v.split("=").map(v => v.trim())) + .map(v => v.split("=").map(v => v.trim())) // tslint:disable-line .map( (pair): ReactionRolePair => { const customEmojiMatch = pair[0].match(/^<:(?:.*?):(\d+)>$/); diff --git a/src/utils.ts b/src/utils.ts index f47f0a5f..8e952805 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import at = require("lodash.at"); -import { Guild, GuildAuditLogEntry, TextableChannel } from "eris"; +import { Emoji, Guild, GuildAuditLogEntry, TextableChannel } from "eris"; import url from "url"; import tlds from "tlds"; import emojiRegex from "emoji-regex"; @@ -176,6 +176,10 @@ export function getEmojiInString(str: string): string[] { return str.match(matchAllEmojiRegex) || []; } +export function isEmoji(str: string): boolean { + return str.match(`^(${unicodeEmojiRegex.source})|(${customEmojiRegex.source})$`) !== null; +} + export function trimLines(str: string) { return str .trim() @@ -288,3 +292,7 @@ export function noop() { } export const DBDateFormat = "YYYY-MM-DD HH:mm:ss"; + +export type CustomEmoji = { + id: string; +} & Emoji;