From 69c3896c5756cd28c86e73ae397231befdab4227 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Thu, 16 Jul 2020 21:41:50 +0200 Subject: [PATCH 1/7] Migrate UsernameSaver to new Plugin structure --- .../UsernameSaver/UsernameSaverPlugin.ts | 21 +++++++++++++++++++ .../UsernameSaver/events/MessageCreateEvt.ts | 11 ++++++++++ .../events/VoiceChannelJoinEvt.ts | 11 ++++++++++ backend/src/plugins/UsernameSaver/types.ts | 12 +++++++++++ .../plugins/UsernameSaver/updateUsername.ts | 12 +++++++++++ backend/src/plugins/availablePlugins.ts | 2 ++ 6 files changed, 69 insertions(+) create mode 100644 backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts create mode 100644 backend/src/plugins/UsernameSaver/events/MessageCreateEvt.ts create mode 100644 backend/src/plugins/UsernameSaver/events/VoiceChannelJoinEvt.ts create mode 100644 backend/src/plugins/UsernameSaver/types.ts create mode 100644 backend/src/plugins/UsernameSaver/updateUsername.ts diff --git a/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts new file mode 100644 index 00000000..592258ea --- /dev/null +++ b/backend/src/plugins/UsernameSaver/UsernameSaverPlugin.ts @@ -0,0 +1,21 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; +import { UsernameSaverPluginType } from "./types"; +import { MessageCreateEvt } from "./events/MessageCreateEvt"; +import { VoiceChannelJoinEvt } from "./events/VoiceChannelJoinEvt"; + +export const UsernameSaverPlugin = zeppelinPlugin()("username_saver", { + // prettier-ignore + events: [ + MessageCreateEvt, + VoiceChannelJoinEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.usernameHistory = new UsernameHistory(); + state.updateQueue = new Queue(); + }, +}); diff --git a/backend/src/plugins/UsernameSaver/events/MessageCreateEvt.ts b/backend/src/plugins/UsernameSaver/events/MessageCreateEvt.ts new file mode 100644 index 00000000..14c6aded --- /dev/null +++ b/backend/src/plugins/UsernameSaver/events/MessageCreateEvt.ts @@ -0,0 +1,11 @@ +import { usernameEvent } from "../types"; +import { updateUsername } from "../updateUsername"; + +export const MessageCreateEvt = usernameEvent({ + event: "messageCreate", + + async listener(meta) { + if (meta.args.message.author.bot) return; + meta.pluginData.state.updateQueue.add(() => updateUsername(meta.pluginData, meta.args.message.author)); + }, +}); diff --git a/backend/src/plugins/UsernameSaver/events/VoiceChannelJoinEvt.ts b/backend/src/plugins/UsernameSaver/events/VoiceChannelJoinEvt.ts new file mode 100644 index 00000000..e26ea435 --- /dev/null +++ b/backend/src/plugins/UsernameSaver/events/VoiceChannelJoinEvt.ts @@ -0,0 +1,11 @@ +import { usernameEvent } from "../types"; +import { updateUsername } from "../updateUsername"; + +export const VoiceChannelJoinEvt = usernameEvent({ + event: "voiceChannelJoin", + + async listener(meta) { + if (meta.args.member.bot) return; + meta.pluginData.state.updateQueue.add(() => updateUsername(meta.pluginData, meta.args.member.user)); + }, +}); diff --git a/backend/src/plugins/UsernameSaver/types.ts b/backend/src/plugins/UsernameSaver/types.ts new file mode 100644 index 00000000..eeb078e7 --- /dev/null +++ b/backend/src/plugins/UsernameSaver/types.ts @@ -0,0 +1,12 @@ +import { BasePluginType, eventListener } from "knub"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; + +export interface UsernameSaverPluginType extends BasePluginType { + state: { + usernameHistory: UsernameHistory; + updateQueue: Queue; + }; +} + +export const usernameEvent = eventListener(); diff --git a/backend/src/plugins/UsernameSaver/updateUsername.ts b/backend/src/plugins/UsernameSaver/updateUsername.ts new file mode 100644 index 00000000..6b5129a9 --- /dev/null +++ b/backend/src/plugins/UsernameSaver/updateUsername.ts @@ -0,0 +1,12 @@ +import { User } from "eris"; +import { PluginData } from "knub"; +import { UsernameSaverPluginType } from "./types"; + +export async function updateUsername(pluginData: PluginData, user: User) { + if (!user) return; + const newUsername = `${user.username}#${user.discriminator}`; + const latestEntry = await pluginData.state.usernameHistory.getLastEntry(user.id); + if (!latestEntry || newUsername !== latestEntry.username) { + await pluginData.state.usernameHistory.addEntry(user.id, newUsername); + } +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..3e33f936 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,10 +1,12 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { UsernameSaverPlugin } from "./UsernameSaver/UsernameSaverPlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, + UsernameSaverPlugin, UtilityPlugin, ]; From e03f0006e7c18cc0a0ab2e028f5932f1a5334912 Mon Sep 17 00:00:00 2001 From: Nils <7890309+DarkView@users.noreply.github.com> Date: Thu, 16 Jul 2020 22:35:10 +0200 Subject: [PATCH 2/7] Change rest to catchAll --- backend/src/plugins/LocateUser/commands/FollowCmd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/plugins/LocateUser/commands/FollowCmd.ts b/backend/src/plugins/LocateUser/commands/FollowCmd.ts index d0e41a79..5d44a3e6 100644 --- a/backend/src/plugins/LocateUser/commands/FollowCmd.ts +++ b/backend/src/plugins/LocateUser/commands/FollowCmd.ts @@ -13,7 +13,7 @@ export const FollowCmd = locateUserCommand({ signature: { member: ct.resolvedMember(), - reminder: ct.string({ required: false, rest: true }), + reminder: ct.string({ required: false, catchAll: true }), duration: ct.delay({ option: true, shortcut: "d" }), active: ct.bool({ option: true, shortcut: "a" }), From 7ce1ebfee276fdc78f251724d22738e9a9693fb2 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Thu, 16 Jul 2020 22:54:02 +0200 Subject: [PATCH 3/7] Migrate Reminders to new Plugin structure --- .../src/plugins/Reminders/RemindersPlugin.ts | 50 ++++++++++++++ .../plugins/Reminders/commands/RemindCmd.ts | 65 +++++++++++++++++++ .../Reminders/commands/RemindersCmd.ts | 31 +++++++++ .../Reminders/commands/RemindersDeleteCmd.ts | 29 +++++++++ backend/src/plugins/Reminders/types.ts | 22 +++++++ .../Reminders/utils/postDueRemindersLoop.ts | 48 ++++++++++++++ backend/src/plugins/availablePlugins.ts | 2 + 7 files changed, 247 insertions(+) create mode 100644 backend/src/plugins/Reminders/RemindersPlugin.ts create mode 100644 backend/src/plugins/Reminders/commands/RemindCmd.ts create mode 100644 backend/src/plugins/Reminders/commands/RemindersCmd.ts create mode 100644 backend/src/plugins/Reminders/commands/RemindersDeleteCmd.ts create mode 100644 backend/src/plugins/Reminders/types.ts create mode 100644 backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts diff --git a/backend/src/plugins/Reminders/RemindersPlugin.ts b/backend/src/plugins/Reminders/RemindersPlugin.ts new file mode 100644 index 00000000..c183eb7d --- /dev/null +++ b/backend/src/plugins/Reminders/RemindersPlugin.ts @@ -0,0 +1,50 @@ +import { PluginOptions } from "knub"; +import { ConfigSchema, RemindersPluginType } from "./types"; +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { GuildReminders } from "src/data/GuildReminders"; +import { postDueRemindersLoop } from "./utils/postDueRemindersLoop"; +import { RemindCmd } from "./commands/RemindCmd"; +import { RemindersCmd } from "./commands/RemindersCmd"; +import { RemindersDeleteCmd } from "./commands/RemindersDeleteCmd"; + +const defaultOptions: PluginOptions = { + config: { + can_use: false, + }, + overrides: [ + { + level: ">=50", + config: { + can_use: true, + }, + }, + ], +}; + +export const RemindersPlugin = zeppelinPlugin()("reminders", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + RemindCmd, + RemindersCmd, + RemindersDeleteCmd, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.reminders = GuildReminders.getGuildInstance(guild.id); + state.tries = new Map(); + state.unloaded = false; + + state.postRemindersTimeout = null; + postDueRemindersLoop(pluginData); + }, + + onUnload(pluginData) { + clearTimeout(pluginData.state.postRemindersTimeout); + pluginData.state.unloaded = true; + }, +}); diff --git a/backend/src/plugins/Reminders/commands/RemindCmd.ts b/backend/src/plugins/Reminders/commands/RemindCmd.ts new file mode 100644 index 00000000..faeacb70 --- /dev/null +++ b/backend/src/plugins/Reminders/commands/RemindCmd.ts @@ -0,0 +1,65 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import moment from "moment-timezone"; +import { convertDelayStringToMS } from "src/utils"; +import humanizeDuration from "humanize-duration"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { remindersCommand } from "../types"; + +export const RemindCmd = remindersCommand({ + trigger: ["remind", "remindme"], + usage: "!remind 3h Remind me of this in 3 hours please", + permission: "can_use", + + signature: { + time: ct.string(), + reminder: ct.string({ required: false, catchAll: true }), + }, + + async run({ message: msg, args, pluginData }) { + const now = moment(); + + let reminderTime; + if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}$/)) { + // Date in YYYY-MM-DD format, remind at current time on that date + reminderTime = moment(args.time, "YYYY-M-D").set({ + hour: now.hour(), + minute: now.minute(), + second: now.second(), + }); + } else if (args.time.match(/^\d{4}-\d{1,2}-\d{1,2}T\d{2}:\d{2}$/)) { + // Date and time in YYYY-MM-DD[T]HH:mm format + reminderTime = moment(args.time, "YYYY-M-D[T]HH:mm").second(0); + } else { + // "Delay string" i.e. e.g. "2h30m" + const ms = convertDelayStringToMS(args.time); + if (ms === null) { + sendErrorMessage(pluginData, msg.channel, "Invalid reminder time"); + return; + } + + reminderTime = moment().add(ms, "millisecond"); + } + + if (!reminderTime.isValid() || reminderTime.isBefore(now)) { + sendErrorMessage(pluginData, msg.channel, "Invalid reminder time"); + return; + } + + const reminderBody = args.reminder || `https://discord.com/channels/${this.guildId}/${msg.channel.id}/${msg.id}`; + await pluginData.state.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 }); + sendSuccessMessage( + pluginData, + msg.channel, + `I will remind you in **${timeUntilReminder}** at **${reminderTime.format("YYYY-MM-DD, HH:mm")}**`, + ); + }, +}); diff --git a/backend/src/plugins/Reminders/commands/RemindersCmd.ts b/backend/src/plugins/Reminders/commands/RemindersCmd.ts new file mode 100644 index 00000000..0273880d --- /dev/null +++ b/backend/src/plugins/Reminders/commands/RemindersCmd.ts @@ -0,0 +1,31 @@ +import { remindersCommand } from "../types"; +import { sendErrorMessage } from "src/pluginUtils"; +import { sorter, createChunkedMessage } from "src/utils"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; + +export const RemindersCmd = remindersCommand({ + trigger: "reminders", + permission: "can_use", + + async run({ message: msg, args, pluginData }) { + const reminders = await pluginData.state.reminders.getRemindersByUserId(msg.author.id); + if (reminders.length === 0) { + sendErrorMessage(pluginData, msg.channel, "No reminders"); + return; + } + + reminders.sort(sorter("remind_at")); + const longestNum = (reminders.length + 1).toString().length; + 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 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")); + }, +}); diff --git a/backend/src/plugins/Reminders/commands/RemindersDeleteCmd.ts b/backend/src/plugins/Reminders/commands/RemindersDeleteCmd.ts new file mode 100644 index 00000000..e31677b4 --- /dev/null +++ b/backend/src/plugins/Reminders/commands/RemindersDeleteCmd.ts @@ -0,0 +1,29 @@ +import { remindersCommand } from "../types"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { sorter } from "src/utils"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; + +export const RemindersDeleteCmd = remindersCommand({ + trigger: ["reminders delete", "reminders d"], + permission: "can_use", + + signature: { + num: ct.number(), + }, + + async run({ message: msg, args, pluginData }) { + const reminders = await pluginData.state.reminders.getRemindersByUserId(msg.author.id); + reminders.sort(sorter("remind_at")); + const lastNum = reminders.length + 1; + + if (args.num > lastNum || args.num < 0) { + sendErrorMessage(pluginData, msg.channel, "Unknown reminder"); + return; + } + + const toDelete = reminders[args.num - 1]; + await pluginData.state.reminders.delete(toDelete.id); + + sendSuccessMessage(pluginData, msg.channel, "Reminder deleted"); + }, +}); diff --git a/backend/src/plugins/Reminders/types.ts b/backend/src/plugins/Reminders/types.ts new file mode 100644 index 00000000..8154e6a2 --- /dev/null +++ b/backend/src/plugins/Reminders/types.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; +import { BasePluginType, command } from "knub"; +import { GuildReminders } from "src/data/GuildReminders"; + +export const ConfigSchema = t.type({ + can_use: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface RemindersPluginType extends BasePluginType { + config: TConfigSchema; + + state: { + reminders: GuildReminders; + tries: Map; + + postRemindersTimeout; + unloaded: boolean; + }; +} + +export const remindersCommand = command(); diff --git a/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts b/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts new file mode 100644 index 00000000..18d94a2b --- /dev/null +++ b/backend/src/plugins/Reminders/utils/postDueRemindersLoop.ts @@ -0,0 +1,48 @@ +import { TextChannel } from "eris"; +import { PluginData } from "knub"; +import { RemindersPluginType } from "../types"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; +import { disableLinkPreviews } from "knub/dist/helpers"; +import { SECONDS } from "src/utils"; + +const REMINDER_LOOP_TIME = 10 * SECONDS; +const MAX_TRIES = 3; + +export async function postDueRemindersLoop(pluginData: PluginData) { + const pendingReminders = await pluginData.state.reminders.getDueReminders(); + for (const reminder of pendingReminders) { + const channel = pluginData.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()) { + 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 + const tries = pluginData.state.tries.get(reminder.id) || 0; + if (tries < MAX_TRIES) { + pluginData.state.tries.set(reminder.id, tries + 1); + continue; + } + } + } + + await pluginData.state.reminders.delete(reminder.id); + } + + if (!pluginData.state.unloaded) { + pluginData.state.postRemindersTimeout = setTimeout(() => postDueRemindersLoop(pluginData), REMINDER_LOOP_TIME); + } +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..16d5849c 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,10 +1,12 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { RemindersPlugin } from "./Reminders/RemindersPlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, + RemindersPlugin, UtilityPlugin, ]; From 2c4e683630c1307ce6548d33014dcdc529bc29a7 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Mon, 20 Jul 2020 00:23:47 +0200 Subject: [PATCH 4/7] Migrate AutoReactions to new Plugin structure --- .../AutoReactions/AutoReactionsPlugin.ts | 45 ++++++++++++++++++ .../commands/DisableAutoReactionsCmd.ts | 24 ++++++++++ .../commands/NewAutoReactionsCmd.ts | 47 +++++++++++++++++++ .../AutoReactions/events/MessageCreateEvt.ts | 43 +++++++++++++++++ backend/src/plugins/AutoReactions/types.ts | 22 +++++++++ backend/src/plugins/availablePlugins.ts | 2 + 6 files changed, 183 insertions(+) create mode 100644 backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts create mode 100644 backend/src/plugins/AutoReactions/commands/DisableAutoReactionsCmd.ts create mode 100644 backend/src/plugins/AutoReactions/commands/NewAutoReactionsCmd.ts create mode 100644 backend/src/plugins/AutoReactions/events/MessageCreateEvt.ts create mode 100644 backend/src/plugins/AutoReactions/types.ts 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 0dce5a2c..430061c6 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,9 +1,11 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { AutoReactionsPlugin } from "./AutoReactions/AutoReactionsPlugin"; // prettier-ignore export const guildPlugins: Array> = [ + AutoReactionsPlugin, LocateUserPlugin, UtilityPlugin, ]; From f83d7122b9ddb0dae06c0773419cf2d4089d21d8 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Mon, 20 Jul 2020 23:22:42 +0200 Subject: [PATCH 5/7] Migrate MessageSaver to new Plugin structure --- .../MessageSaver/MessageSaverPlugin.ts | 47 ++++++++++++++++ .../MessageSaver/commands/SaveMessagesToDB.ts | 31 +++++++++++ .../MessageSaver/commands/SavePinsToDB.ts | 35 ++++++++++++ .../MessageSaver/events/SaveMessagesEvts.ts | 53 +++++++++++++++++++ .../plugins/MessageSaver/saveMessagesToDB.ts | 35 ++++++++++++ backend/src/plugins/MessageSaver/types.ts | 18 +++++++ backend/src/plugins/availablePlugins.ts | 2 + 7 files changed, 221 insertions(+) create mode 100644 backend/src/plugins/MessageSaver/MessageSaverPlugin.ts create mode 100644 backend/src/plugins/MessageSaver/commands/SaveMessagesToDB.ts create mode 100644 backend/src/plugins/MessageSaver/commands/SavePinsToDB.ts create mode 100644 backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts create mode 100644 backend/src/plugins/MessageSaver/saveMessagesToDB.ts create mode 100644 backend/src/plugins/MessageSaver/types.ts diff --git a/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts new file mode 100644 index 00000000..2e2f7e2e --- /dev/null +++ b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts @@ -0,0 +1,47 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { ConfigSchema, MessageSaverPluginType } from "./types"; +import { GuildSavedMessages } from "../../data/GuildSavedMessages"; +import { PluginOptions } from "knub"; +import { MessageCreateEvt, MessageUpdateEvt, MessageDeleteEvt, MessageDeleteBulkEvt } from "./events/SaveMessagesEvts"; +import { SaveMessagesToDBCmd } from "./commands/SaveMessagesToDB"; +import { SavePinsToDBCmd } from "./commands/SavePinsToDB"; + +const defaultOptions: PluginOptions = { + config: { + can_manage: false, + }, + overrides: [ + { + level: ">=100", + config: { + can_manage: true, + }, + }, + ], +}; + +export const MessageSaverPlugin = zeppelinPlugin()("message_saver", { + configSchema: ConfigSchema, + defaultOptions, + + showInDocs: false, + + // prettier-ignore + commands: [ + SaveMessagesToDBCmd, + SavePinsToDBCmd, + ], + + // prettier-ignore + events: [ + MessageCreateEvt, + MessageUpdateEvt, + MessageDeleteEvt, + MessageDeleteBulkEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id); + }, +}); diff --git a/backend/src/plugins/MessageSaver/commands/SaveMessagesToDB.ts b/backend/src/plugins/MessageSaver/commands/SaveMessagesToDB.ts new file mode 100644 index 00000000..431acb13 --- /dev/null +++ b/backend/src/plugins/MessageSaver/commands/SaveMessagesToDB.ts @@ -0,0 +1,31 @@ +import { messageSaverCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { saveMessagesToDB } from "../saveMessagesToDB"; +import { TextChannel } from "eris"; +import { sendSuccessMessage } from "src/pluginUtils"; + +export const SaveMessagesToDBCmd = messageSaverCmd({ + trigger: "save_messages_to_db", + permission: "can_manage", + source: "guild", + + signature: { + channel: ct.textChannel(), + ids: ct.string({ catchAll: true }), + }, + + async run({ message: msg, args, pluginData }) { + await msg.channel.createMessage("Saving specified messages..."); + const { savedCount, failed } = await saveMessagesToDB(pluginData, args.channel, args.ids.trim().split(" ")); + + if (failed.length) { + sendSuccessMessage( + pluginData, + msg.channel, + `Saved ${savedCount} messages. The following messages could not be saved: ${failed.join(", ")}`, + ); + } else { + sendSuccessMessage(pluginData, msg.channel, `Saved ${savedCount} messages!`); + } + }, +}); diff --git a/backend/src/plugins/MessageSaver/commands/SavePinsToDB.ts b/backend/src/plugins/MessageSaver/commands/SavePinsToDB.ts new file mode 100644 index 00000000..ee462244 --- /dev/null +++ b/backend/src/plugins/MessageSaver/commands/SavePinsToDB.ts @@ -0,0 +1,35 @@ +import { messageSaverCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { saveMessagesToDB } from "../saveMessagesToDB"; +import { sendSuccessMessage } from "src/pluginUtils"; + +export const SavePinsToDBCmd = messageSaverCmd({ + trigger: "save_pins_to_db", + permission: "can_manage", + source: "guild", + + signature: { + channel: ct.textChannel(), + }, + + async run({ message: msg, args, pluginData }) { + await msg.channel.createMessage(`Saving pins from <#${args.channel.id}>...`); + + const pins = await args.channel.getPins(); + const { savedCount, failed } = await saveMessagesToDB( + pluginData, + args.channel, + pins.map(m => m.id), + ); + + if (failed.length) { + sendSuccessMessage( + pluginData, + msg.channel, + `Saved ${savedCount} messages. The following messages could not be saved: ${failed.join(", ")}`, + ); + } else { + sendSuccessMessage(pluginData, msg.channel, `Saved ${savedCount} messages!`); + } + }, +}); diff --git a/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts b/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts new file mode 100644 index 00000000..5aaafe5e --- /dev/null +++ b/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts @@ -0,0 +1,53 @@ +import { messageSaverEvt } from "../types"; +import { Message } from "eris"; + +export const MessageCreateEvt = messageSaverEvt({ + event: "messageCreate", + allowOutsideOfGuild: false, + + async listener(meta) { + // Only save regular chat messages + if (meta.args.message.type !== 0) { + return; + } + + await meta.pluginData.state.savedMessages.createFromMsg(meta.args.message); + }, +}); + +export const MessageUpdateEvt = messageSaverEvt({ + event: "messageUpdate", + allowOutsideOfGuild: false, + + async listener(meta) { + if (meta.args.message.type !== 0) { + return; + } + + await meta.pluginData.state.savedMessages.saveEditFromMsg(meta.args.message); + }, +}); + +export const MessageDeleteEvt = messageSaverEvt({ + event: "messageDelete", + allowOutsideOfGuild: false, + + async listener(meta) { + const msg = meta.args.message as Message; + if (msg.type != null && msg.type !== 0) { + return; + } + + await meta.pluginData.state.savedMessages.markAsDeleted(msg.id); + }, +}); + +export const MessageDeleteBulkEvt = messageSaverEvt({ + event: "messageDeleteBulk", + allowOutsideOfGuild: false, + + async listener(meta) { + const ids = meta.args.messages.map(m => m.id); + await meta.pluginData.state.savedMessages.markBulkAsDeleted(ids); + }, +}); diff --git a/backend/src/plugins/MessageSaver/saveMessagesToDB.ts b/backend/src/plugins/MessageSaver/saveMessagesToDB.ts new file mode 100644 index 00000000..7dd6e575 --- /dev/null +++ b/backend/src/plugins/MessageSaver/saveMessagesToDB.ts @@ -0,0 +1,35 @@ +import { MessageSaverPluginType } from "./types"; +import { PluginData } from "knub"; +import { TextChannel, Message } from "eris"; + +export async function saveMessagesToDB( + pluginData: PluginData, + channel: TextChannel, + ids: string[], +) { + const failed = []; + for (const id of ids) { + const savedMessage = await pluginData.state.savedMessages.find(id); + if (savedMessage) continue; + + let thisMsg: Message; + + try { + thisMsg = await channel.getMessage(id); + + if (!thisMsg) { + failed.push(id); + continue; + } + + await pluginData.state.savedMessages.createFromMsg(thisMsg, { is_permanent: true }); + } catch (e) { + failed.push(id); + } + } + + return { + savedCount: ids.length - failed.length, + failed, + }; +} diff --git a/backend/src/plugins/MessageSaver/types.ts b/backend/src/plugins/MessageSaver/types.ts new file mode 100644 index 00000000..1eb6909a --- /dev/null +++ b/backend/src/plugins/MessageSaver/types.ts @@ -0,0 +1,18 @@ +import * as t from "io-ts"; +import { BasePluginType, command, eventListener } from "knub"; +import { GuildSavedMessages } from "../../data/GuildSavedMessages"; + +export const ConfigSchema = t.type({ + can_manage: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface MessageSaverPluginType extends BasePluginType { + config: TConfigSchema; + state: { + savedMessages: GuildSavedMessages; + }; +} + +export const messageSaverCmd = command(); +export const messageSaverEvt = eventListener(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..4ea42fd6 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,10 +1,12 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { MessageSaverPlugin } from "./MessageSaver/MessageSaverPlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, + MessageSaverPlugin, UtilityPlugin, ]; From bb3eb05ec216583478d7ea050cac2ec0ba0c6eea Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Tue, 21 Jul 2020 00:10:09 +0200 Subject: [PATCH 6/7] Migrate NameHistory to new Plugin structure --- .../plugins/NameHistory/NameHistoryPlugin.ts | 46 +++++++++++++++++ .../plugins/NameHistory/commands/NamesCmd.ts | 51 +++++++++++++++++++ .../NameHistory/events/UpdateNameEvts.ts | 18 +++++++ backend/src/plugins/NameHistory/types.ts | 22 ++++++++ .../src/plugins/NameHistory/updateNickname.ts | 12 +++++ backend/src/plugins/availablePlugins.ts | 2 + 6 files changed, 151 insertions(+) create mode 100644 backend/src/plugins/NameHistory/NameHistoryPlugin.ts create mode 100644 backend/src/plugins/NameHistory/commands/NamesCmd.ts create mode 100644 backend/src/plugins/NameHistory/events/UpdateNameEvts.ts create mode 100644 backend/src/plugins/NameHistory/types.ts create mode 100644 backend/src/plugins/NameHistory/updateNickname.ts diff --git a/backend/src/plugins/NameHistory/NameHistoryPlugin.ts b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts new file mode 100644 index 00000000..19dabb02 --- /dev/null +++ b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts @@ -0,0 +1,46 @@ +import { PluginOptions } from "knub"; +import { NameHistoryPluginType, ConfigSchema } from "./types"; +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { GuildNicknameHistory } from "src/data/GuildNicknameHistory"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; +import { NamesCmd } from "./commands/NamesCmd"; +import { ChannelJoinEvt, MessageCreateEvt } from "./events/UpdateNameEvts"; + +const defaultOptions: PluginOptions = { + config: { + can_view: false, + }, + overrides: [ + { + level: ">=50", + config: { + can_view: true, + }, + }, + ], +}; + +export const NameHistoryPlugin = zeppelinPlugin()("name_history", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + NamesCmd, + ], + + // prettier-ignore + events: [ + ChannelJoinEvt, + MessageCreateEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.nicknameHistory = GuildNicknameHistory.getGuildInstance(guild.id); + state.usernameHistory = new UsernameHistory(); + state.updateQueue = new Queue(); + }, +}); diff --git a/backend/src/plugins/NameHistory/commands/NamesCmd.ts b/backend/src/plugins/NameHistory/commands/NamesCmd.ts new file mode 100644 index 00000000..3e5889c3 --- /dev/null +++ b/backend/src/plugins/NameHistory/commands/NamesCmd.ts @@ -0,0 +1,51 @@ +import { nameHistoryCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { disableCodeBlocks, createChunkedMessage } from "knub/dist/helpers"; +import { NICKNAME_RETENTION_PERIOD } from "src/data/cleanup/nicknames"; +import { DAYS } from "src/utils"; +import { MAX_NICKNAME_ENTRIES_PER_USER } from "src/data/GuildNicknameHistory"; +import { MAX_USERNAME_ENTRIES_PER_USER } from "src/data/UsernameHistory"; +import { sendErrorMessage } from "src/pluginUtils"; + +export const NamesCmd = nameHistoryCmd({ + trigger: "names", + permission: "can_view", + + signature: { + userId: ct.userId(), + }, + + async run({ message: msg, args, pluginData }) { + const nicknames = await pluginData.state.nicknameHistory.getByUserId(args.userId); + const usernames = await pluginData.state.usernameHistory.getByUserId(args.userId); + + if (nicknames.length === 0 && usernames.length === 0) { + return sendErrorMessage(pluginData, msg.channel, "No name history found"); + } + + const nicknameRows = nicknames.map( + r => `\`[${r.timestamp}]\` ${r.nickname ? `**${disableCodeBlocks(r.nickname)}**` : "*None*"}`, + ); + const usernameRows = usernames.map(r => `\`[${r.timestamp}]\` **${disableCodeBlocks(r.username)}**`); + + const user = pluginData.client.users.get(args.userId); + const currentUsername = user ? `${user.username}#${user.discriminator}` : args.userId; + + const nicknameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); + const usernameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); + + let message = `Name history for **${currentUsername}**:`; + if (nicknameRows.length) { + message += `\n\n__Last ${MAX_NICKNAME_ENTRIES_PER_USER} nicknames within ${nicknameDays} days:__\n${nicknameRows.join( + "\n", + )}`; + } + if (usernameRows.length) { + message += `\n\n__Last ${MAX_USERNAME_ENTRIES_PER_USER} usernames within ${usernameDays} days:__\n${usernameRows.join( + "\n", + )}`; + } + + createChunkedMessage(msg.channel, message); + }, +}); diff --git a/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts new file mode 100644 index 00000000..3c6ca433 --- /dev/null +++ b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts @@ -0,0 +1,18 @@ +import { nameHistoryEvt } from "../types"; +import { updateNickname } from "../updateNickname"; + +export const ChannelJoinEvt = nameHistoryEvt({ + event: "voiceChannelJoin", + + async listener(meta) { + meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.member)); + }, +}); + +export const MessageCreateEvt = nameHistoryEvt({ + event: "messageCreate", + + async listener(meta) { + meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.message.member)); + }, +}); diff --git a/backend/src/plugins/NameHistory/types.ts b/backend/src/plugins/NameHistory/types.ts new file mode 100644 index 00000000..ad9f6833 --- /dev/null +++ b/backend/src/plugins/NameHistory/types.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; +import { BasePluginType, command, eventListener } from "knub"; +import { GuildNicknameHistory } from "src/data/GuildNicknameHistory"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; + +export const ConfigSchema = t.type({ + can_view: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface NameHistoryPluginType extends BasePluginType { + config: TConfigSchema; + state: { + nicknameHistory: GuildNicknameHistory; + usernameHistory: UsernameHistory; + updateQueue: Queue; + }; +} + +export const nameHistoryCmd = command(); +export const nameHistoryEvt = eventListener(); diff --git a/backend/src/plugins/NameHistory/updateNickname.ts b/backend/src/plugins/NameHistory/updateNickname.ts new file mode 100644 index 00000000..b00e01fa --- /dev/null +++ b/backend/src/plugins/NameHistory/updateNickname.ts @@ -0,0 +1,12 @@ +import { Member } from "eris"; +import { PluginData } from "knub"; +import { NameHistoryPluginType } from "./types"; + +export async function updateNickname(pluginData: PluginData, member: Member) { + if (!member) return; + const latestEntry = await pluginData.state.nicknameHistory.getLastEntry(member.id); + if (!latestEntry || latestEntry.nickname !== member.nick) { + if (!latestEntry && member.nick == null) return; // No need to save "no nickname" if there's no previous data + await pluginData.state.nicknameHistory.addEntry(member.id, member.nick); + } +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..51be85ce 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,10 +1,12 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { NameHistoryPlugin } from "./NameHistory/NameHistoryPlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, + NameHistoryPlugin, UtilityPlugin, ]; From 4e2dd700300731e7117cd390b7ad6f29cad21762 Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Thu, 16 Jul 2020 20:49:43 +0200 Subject: [PATCH 7/7] Migrate WelcomeMessage to new Plugin structure --- .../WelcomeMessage/WelcomeMessagePlugin.ts | 29 +++++++++++ .../events/GuildMemberAddEvt.ts | 51 +++++++++++++++++++ backend/src/plugins/WelcomeMessage/types.ts | 20 ++++++++ backend/src/plugins/availablePlugins.ts | 2 + 4 files changed, 102 insertions(+) create mode 100644 backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts create mode 100644 backend/src/plugins/WelcomeMessage/events/GuildMemberAddEvt.ts create mode 100644 backend/src/plugins/WelcomeMessage/types.ts diff --git a/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts b/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts new file mode 100644 index 00000000..b34e5659 --- /dev/null +++ b/backend/src/plugins/WelcomeMessage/WelcomeMessagePlugin.ts @@ -0,0 +1,29 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { PluginOptions } from "knub"; +import { WelcomeMessagePluginType, ConfigSchema } from "./types"; +import { GuildLogs } from "src/data/GuildLogs"; +import { GuildMemberAddEvt } from "./events/GuildMemberAddEvt"; + +const defaultOptions: PluginOptions = { + config: { + send_dm: false, + send_to_channel: null, + message: "", + }, +}; + +export const WelcomeMessagePlugin = zeppelinPlugin()("welcome_message", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + events: [ + GuildMemberAddEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.logs = new GuildLogs(guild.id); + }, +}); diff --git a/backend/src/plugins/WelcomeMessage/events/GuildMemberAddEvt.ts b/backend/src/plugins/WelcomeMessage/events/GuildMemberAddEvt.ts new file mode 100644 index 00000000..8e33080e --- /dev/null +++ b/backend/src/plugins/WelcomeMessage/events/GuildMemberAddEvt.ts @@ -0,0 +1,51 @@ +import { welcomeEvent } from "../types"; +import { renderTemplate } from "src/templateFormatter"; +import { stripObjectToScalars, createChunkedMessage } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { TextChannel } from "eris"; + +export const GuildMemberAddEvt = welcomeEvent({ + event: "guildMemberAdd", + + async listener(meta) { + const pluginData = meta.pluginData; + const member = meta.args.member; + + const config = pluginData.config.get(); + if (!config.message) return; + if (!config.send_dm && !config.send_to_channel) return; + + const formatted = await renderTemplate(config.message, { + member: stripObjectToScalars(member, ["user"]), + }); + + if (config.send_dm) { + const dmChannel = await member.user.getDMChannel(); + if (!dmChannel) return; + + try { + await createChunkedMessage(dmChannel, formatted); + } catch (e) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Failed send a welcome DM to {userMention(member)}`, + member: stripObjectToScalars(member), + }); + } + } + + if (config.send_to_channel) { + const channel = meta.args.guild.channels.get(config.send_to_channel); + if (!channel || !(channel instanceof TextChannel)) return; + + try { + await createChunkedMessage(channel, formatted); + } catch (e) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Failed send a welcome message for {userMention(member)} to {channelMention(channel)}`, + member: stripObjectToScalars(member), + channel: stripObjectToScalars(channel), + }); + } + } + }, +}); diff --git a/backend/src/plugins/WelcomeMessage/types.ts b/backend/src/plugins/WelcomeMessage/types.ts new file mode 100644 index 00000000..eef61be9 --- /dev/null +++ b/backend/src/plugins/WelcomeMessage/types.ts @@ -0,0 +1,20 @@ +import * as t from "io-ts"; +import { BasePluginType, eventListener } from "knub"; +import { tNullable } from "src/utils"; +import { GuildLogs } from "src/data/GuildLogs"; + +export const ConfigSchema = t.type({ + send_dm: t.boolean, + send_to_channel: tNullable(t.string), + message: t.string, +}); +export type TConfigSchema = t.TypeOf; + +export interface WelcomeMessagePluginType extends BasePluginType { + config: TConfigSchema; + state: { + logs: GuildLogs; + }; +} + +export const welcomeEvent = eventListener(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..099c7bcf 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,11 +1,13 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { WelcomeMessagePlugin } from "./WelcomeMessage/WelcomeMessagePlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, UtilityPlugin, + WelcomeMessagePlugin, ]; export const globalPlugins = [];