diff --git a/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts b/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts new file mode 100644 index 00000000..38116ed3 --- /dev/null +++ b/backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts @@ -0,0 +1,45 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { ConfigSchema, AutoReactionsPluginType } from "./types"; +import { PluginOptions } from "knub"; +import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd"; +import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd"; +import { MessageCreateEvt } from "./events/MessageCreateEvt"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { GuildAutoReactions } from "src/data/GuildAutoReactions"; + +const defaultOptions: PluginOptions = { + config: { + can_manage: false, + }, + overrides: [ + { + level: ">=100", + config: { + can_manage: true, + }, + }, + ], +}; + +export const AutoReactionsPlugin = zeppelinPlugin()("auto_reactions", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + NewAutoReactionsCmd, + DisableAutoReactionsCmd, + ], + + // prettier-ignore + events: [ + MessageCreateEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id); + state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id); + }, +}); diff --git a/backend/src/plugins/AutoReactions/commands/DisableAutoReactionsCmd.ts b/backend/src/plugins/AutoReactions/commands/DisableAutoReactionsCmd.ts new file mode 100644 index 00000000..1d95e9ba --- /dev/null +++ b/backend/src/plugins/AutoReactions/commands/DisableAutoReactionsCmd.ts @@ -0,0 +1,24 @@ +import { autoReactionsCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; + +export const DisableAutoReactionsCmd = autoReactionsCmd({ + trigger: "auto_reactions disable", + permission: "can_manage", + usage: "!auto_reactions disable 629990160477585428", + + signature: { + channelId: ct.channelId(), + }, + + async run({ message: msg, args, pluginData }) { + const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId); + if (!autoReaction) { + sendErrorMessage(pluginData, msg.channel, `Auto-reactions aren't enabled in <#${args.channelId}>`); + return; + } + + await pluginData.state.autoReactions.removeFromChannel(args.channelId); + sendSuccessMessage(pluginData, msg.channel, `Auto-reactions disabled in <#${args.channelId}>`); + }, +}); diff --git a/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts b/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts new file mode 100644 index 00000000..d649a205 --- /dev/null +++ b/backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts @@ -0,0 +1,47 @@ +import { autoReactionsCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { isEmoji, customEmojiRegex, canUseEmoji } from "src/utils"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; + +export const NewAutoReactionsCmd = autoReactionsCmd({ + trigger: "auto_reactions", + permission: "can_manage", + usage: "!auto_reactions 629990160477585428 👍 👎", + + signature: { + channelId: ct.channelId(), + reactions: ct.string({ rest: true }), + }, + + async run({ message: msg, args, pluginData }) { + const finalReactions = []; + + for (const reaction of args.reactions) { + if (!isEmoji(reaction)) { + sendErrorMessage(pluginData, msg.channel, "One or more of the specified reactions were invalid!"); + return; + } + + let savedValue; + + const customEmojiMatch = reaction.match(customEmojiRegex); + if (customEmojiMatch) { + // Custom emoji + if (!canUseEmoji(pluginData.client, customEmojiMatch[2])) { + sendErrorMessage(pluginData, msg.channel, "I can only use regular emojis and custom emojis from this server"); + return; + } + + savedValue = `${customEmojiMatch[1]}:${customEmojiMatch[2]}`; + } else { + // Unicode emoji + savedValue = reaction; + } + + finalReactions.push(savedValue); + } + + await pluginData.state.autoReactions.set(args.channelId, finalReactions); + sendSuccessMessage(pluginData, msg.channel, `Auto-reactions set for <#${args.channelId}>`); + }, +}); diff --git a/backend/src/plugins/AutoReactions/events/MessageCreateEvt.ts b/backend/src/plugins/AutoReactions/events/MessageCreateEvt.ts new file mode 100644 index 00000000..ff1bb756 --- /dev/null +++ b/backend/src/plugins/AutoReactions/events/MessageCreateEvt.ts @@ -0,0 +1,43 @@ +import { autoReactionsEvt } from "../types"; +import { isDiscordRESTError } from "src/utils"; +import { logger } from "knub"; +import { LogType } from "src/data/LogType"; + +export const MessageCreateEvt = autoReactionsEvt({ + event: "messageCreate", + allowOutsideOfGuild: false, + + async listener(meta) { + const pluginData = meta.pluginData; + const msg = meta.args.message; + + const autoReaction = await pluginData.state.autoReactions.getForChannel(msg.channel.id); + if (!autoReaction) return; + + for (const reaction of autoReaction.reactions) { + try { + await msg.addReaction(reaction); + } catch (e) { + if (isDiscordRESTError(e)) { + logger.warn( + `Could not apply auto-reaction to ${msg.channel.id}/${msg.id} in guild ${pluginData.guild.name} (${pluginData.guild.id}) (error code ${e.code})`, + ); + + if (e.code === 10008) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Could not apply auto-reactions in <#${msg.channel.id}> for message \`${msg.id}\`. Make sure nothing is deleting the message before the reactions are applied.`, + }); + } else { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Could not apply auto-reactions in <#${msg.channel.id}> for message \`${msg.id}\`. Error code ${e.code}.`, + }); + } + + return; + } else { + throw e; + } + } + } + }, +}); diff --git a/backend/src/plugins/AutoReactions/types.ts b/backend/src/plugins/AutoReactions/types.ts new file mode 100644 index 00000000..0f50993e --- /dev/null +++ b/backend/src/plugins/AutoReactions/types.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; +import { BasePluginType, command, eventListener } from "knub"; +import { GuildLogs } from "src/data/GuildLogs"; +import { GuildSavedMessages } from "src/data/GuildSavedMessages"; +import { GuildAutoReactions } from "src/data/GuildAutoReactions"; + +export const ConfigSchema = t.type({ + can_manage: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface AutoReactionsPluginType extends BasePluginType { + config: TConfigSchema; + state: { + logs: GuildLogs; + savedMessages: GuildSavedMessages; + autoReactions: GuildAutoReactions; + }; +} + +export const autoReactionsCmd = command(); +export const autoReactionsEvt = eventListener(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 14376e34..14794d80 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,12 +1,14 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { AutoReactionsPlugin } from "./AutoReactions/AutoReactionsPlugin"; import { RemindersPlugin } from "./Reminders/RemindersPlugin"; import { UsernameSaverPlugin } from "./UsernameSaver/UsernameSaverPlugin"; import { WelcomeMessagePlugin } from "./WelcomeMessage/WelcomeMessagePlugin"; // prettier-ignore export const guildPlugins: Array> = [ + AutoReactionsPlugin, LocateUserPlugin, RemindersPlugin, UsernameSaverPlugin,