From 63efaf84eee23cc1b8c5cda4729a0e707544a49d Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Wed, 8 Jul 2020 02:53:44 +0200 Subject: [PATCH] Migrate LocateUser to new Plugin structure --- .../plugins/LocateUser/LocateUserPlugin.ts | 60 ++++++++++++++++++ .../plugins/LocateUser/commands/FollowCmd.ts | 63 +++++++++++++++++++ .../LocateUser/commands/ListFollowCmd.ts | 57 +++++++++++++++++ .../plugins/LocateUser/commands/WhereCmd.ts | 20 ++++++ .../LocateUser/events/ChannelJoinEvt.ts | 22 +++++++ .../LocateUser/events/ChannelLeaveEvt.ts | 19 ++++++ .../LocateUser/events/GuildBanAddEvt.ts | 12 ++++ backend/src/plugins/LocateUser/types.ts | 15 +++++ .../LocateUser/utils/createOrReuseInvite.ts | 11 ++++ .../LocateUser/utils/fillAlertsList.ts | 9 +++ .../plugins/LocateUser/utils/moveMember.ts | 25 ++++++++ .../plugins/LocateUser/utils/outdatedLoop.ts | 17 +++++ .../utils/removeUserIdFromActiveAlerts.ts | 6 ++ .../plugins/LocateUser/utils/sendAlerts.ts | 20 ++++++ .../src/plugins/LocateUser/utils/sendWhere.ts | 22 +++++++ backend/src/plugins/availablePlugins.ts | 2 + 16 files changed, 380 insertions(+) create mode 100644 backend/src/plugins/LocateUser/LocateUserPlugin.ts create mode 100644 backend/src/plugins/LocateUser/commands/FollowCmd.ts create mode 100644 backend/src/plugins/LocateUser/commands/ListFollowCmd.ts create mode 100644 backend/src/plugins/LocateUser/commands/WhereCmd.ts create mode 100644 backend/src/plugins/LocateUser/events/ChannelJoinEvt.ts create mode 100644 backend/src/plugins/LocateUser/events/ChannelLeaveEvt.ts create mode 100644 backend/src/plugins/LocateUser/events/GuildBanAddEvt.ts create mode 100644 backend/src/plugins/LocateUser/types.ts create mode 100644 backend/src/plugins/LocateUser/utils/createOrReuseInvite.ts create mode 100644 backend/src/plugins/LocateUser/utils/fillAlertsList.ts create mode 100644 backend/src/plugins/LocateUser/utils/moveMember.ts create mode 100644 backend/src/plugins/LocateUser/utils/outdatedLoop.ts create mode 100644 backend/src/plugins/LocateUser/utils/removeUserIdFromActiveAlerts.ts create mode 100644 backend/src/plugins/LocateUser/utils/sendAlerts.ts create mode 100644 backend/src/plugins/LocateUser/utils/sendWhere.ts diff --git a/backend/src/plugins/LocateUser/LocateUserPlugin.ts b/backend/src/plugins/LocateUser/LocateUserPlugin.ts new file mode 100644 index 00000000..c71bb3d5 --- /dev/null +++ b/backend/src/plugins/LocateUser/LocateUserPlugin.ts @@ -0,0 +1,60 @@ +import { PluginOptions } from "knub"; +import { LocateUserPluginType, ConfigSchema } from "./types"; +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { GuildVCAlerts } from "src/data/GuildVCAlerts"; +import { outdatedAlertsLoop } from "./utils/outdatedLoop"; +import { fillActiveAlertsList } from "./utils/fillAlertsList"; +import { WhereCmd } from "./commands/WhereCmd"; +import { FollowCmd } from "./commands/FollowCmd"; +import { ListFollowCmd, DeleteFollowCmd } from "./commands/ListFollowCmd"; +import { ChannelJoinEvt, ChannelSwitchEvt } from "./events/ChannelJoinEvt"; +import { ChannelLeaveEvt } from "./events/ChannelLeaveEvt"; +import { GuildBanAddEvt } from "./events/GuildBanAddEvt"; + +const defaultOptions: PluginOptions = { + config: { + can_where: false, + can_alert: false, + }, + overrides: [ + { + level: ">=50", + config: { + can_where: true, + can_alert: true, + }, + }, + ], +}; + +export const LocateUserPlugin = zeppelinPlugin()("locate_user", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + WhereCmd, + FollowCmd, + ListFollowCmd, + DeleteFollowCmd, + ], + + events: [ChannelJoinEvt, ChannelSwitchEvt, ChannelLeaveEvt, GuildBanAddEvt], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.alerts = GuildVCAlerts.getGuildInstance(guild.id); + state.outdatedAlertsTimeout = null; + state.usersWithAlerts = []; + state.unloaded = false; + + outdatedAlertsLoop(pluginData); + fillActiveAlertsList(pluginData); + }, + + onUnload(pluginData) { + clearTimeout(pluginData.state.outdatedAlertsTimeout); + pluginData.state.unloaded = true; + }, +}); diff --git a/backend/src/plugins/LocateUser/commands/FollowCmd.ts b/backend/src/plugins/LocateUser/commands/FollowCmd.ts new file mode 100644 index 00000000..d0e41a79 --- /dev/null +++ b/backend/src/plugins/LocateUser/commands/FollowCmd.ts @@ -0,0 +1,63 @@ +import { locateUserCommand } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; +import { MINUTES, SECONDS } from "src/utils"; +import { sendSuccessMessage } from "src/pluginUtils"; + +export const FollowCmd = locateUserCommand({ + trigger: ["follow", "f"], + description: "Sets up an alert that notifies you any time `` switches or joins voice channels", + usage: "!f 108552944961454080", + permission: "can_alert", + + signature: { + member: ct.resolvedMember(), + reminder: ct.string({ required: false, rest: true }), + + duration: ct.delay({ option: true, shortcut: "d" }), + active: ct.bool({ option: true, shortcut: "a" }), + }, + + async run({ message: msg, args, pluginData }) { + const time = args.duration || 10 * MINUTES; + const alertTime = moment().add(time, "millisecond"); + const body = args.reminder || "None"; + const active = args.active || false; + + if (time < 30 * SECONDS) { + this.sendErrorMessage(msg.channel, "Sorry, but the minimum duration for an alert is 30 seconds!"); + return; + } + + await pluginData.state.alerts.add( + msg.author.id, + args.member.id, + msg.channel.id, + alertTime.format("YYYY-MM-DD HH:mm:ss"), + body, + active, + ); + if (!pluginData.state.usersWithAlerts.includes(args.member.id)) { + pluginData.state.usersWithAlerts.push(args.member.id); + } + + if (active) { + sendSuccessMessage( + pluginData, + msg.channel, + `Every time ${args.member.mention} joins or switches VC in the next ${humanizeDuration( + time, + )} i will notify and move you.\nPlease make sure to be in a voice channel, otherwise i cannot move you!`, + ); + } else { + sendSuccessMessage( + pluginData, + msg.channel, + `Every time ${args.member.mention} joins or switches VC in the next ${humanizeDuration( + time, + )} i will notify you`, + ); + } + }, +}); diff --git a/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts b/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts new file mode 100644 index 00000000..6f77a0b7 --- /dev/null +++ b/backend/src/plugins/LocateUser/commands/ListFollowCmd.ts @@ -0,0 +1,57 @@ +import { locateUserCommand } from "../types"; +import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sorter, createChunkedMessage } from "src/utils"; + +export const ListFollowCmd = locateUserCommand({ + trigger: ["follows", "fs"], + description: "Displays all of your active alerts ordered by expiration time", + usage: "!fs", + permission: "can_alert", + + async run({ message: msg, pluginData }) { + const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.member.id); + if (alerts.length === 0) { + sendErrorMessage(pluginData, msg.channel, "You have no active alerts!"); + return; + } + + alerts.sort(sorter("expires_at")); + const longestNum = (alerts.length + 1).toString().length; + const lines = Array.from(alerts.entries()).map(([i, alert]) => { + const num = i + 1; + const paddedNum = num.toString().padStart(longestNum, " "); + return `\`${paddedNum}.\` \`${alert.expires_at}\` **Target:** <@!${alert.user_id}> **Reminder:** \`${ + alert.body + }\` **Active:** ${alert.active.valueOf()}`; + }); + await createChunkedMessage(msg.channel, lines.join("\n")); + }, +}); + +export const DeleteFollowCmd = locateUserCommand({ + trigger: ["follows delete", "fs d"], + description: + "Deletes the alert at the position .\nThe value needed for can be found using `!follows` (`!fs`)", + usage: "!fs d ", + permission: "can_alert", + + signature: { + num: ct.number({ required: true }), + }, + + async run({ message: msg, args, pluginData }) { + const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.member.id); + alerts.sort(sorter("expires_at")); + + if (args.num > alerts.length || args.num <= 0) { + sendErrorMessage(pluginData, msg.channel, "Unknown alert!"); + return; + } + + const toDelete = alerts[args.num - 1]; + await pluginData.state.alerts.delete(toDelete.id); + + sendSuccessMessage(pluginData, msg.channel, "Alert deleted"); + }, +}); diff --git a/backend/src/plugins/LocateUser/commands/WhereCmd.ts b/backend/src/plugins/LocateUser/commands/WhereCmd.ts new file mode 100644 index 00000000..58b18846 --- /dev/null +++ b/backend/src/plugins/LocateUser/commands/WhereCmd.ts @@ -0,0 +1,20 @@ +import { locateUserCommand } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { resolveMember } from "src/utils"; +import { sendWhere } from "../utils/sendWhere"; + +export const WhereCmd = locateUserCommand({ + trigger: ["where", "w"], + description: "Posts an instant invite to the voice channel that `` is in", + usage: "!w 108552944961454080", + permission: "can_where", + + signature: { + member: ct.resolvedMember(), + }, + + async run({ message: msg, args, pluginData }) { + const member = await resolveMember(pluginData.client, pluginData.guild, args.member.id); + sendWhere.call(this, pluginData.guild, member, msg.channel, `${msg.member.mention} | `); + }, +}); diff --git a/backend/src/plugins/LocateUser/events/ChannelJoinEvt.ts b/backend/src/plugins/LocateUser/events/ChannelJoinEvt.ts new file mode 100644 index 00000000..38c519c7 --- /dev/null +++ b/backend/src/plugins/LocateUser/events/ChannelJoinEvt.ts @@ -0,0 +1,22 @@ +import { locateUserEvent } from "../types"; +import { sendAlerts } from "../utils/sendAlerts"; + +export const ChannelJoinEvt = locateUserEvent({ + event: "voiceChannelJoin", + + async listener(meta) { + if (meta.pluginData.state.usersWithAlerts.includes(meta.args.member.id)) { + sendAlerts(meta.pluginData, meta.args.member.id); + } + }, +}); + +export const ChannelSwitchEvt = locateUserEvent({ + event: "voiceChannelSwitch", + + async listener(meta) { + if (meta.pluginData.state.usersWithAlerts.includes(meta.args.member.id)) { + sendAlerts(meta.pluginData, meta.args.member.id); + } + }, +}); diff --git a/backend/src/plugins/LocateUser/events/ChannelLeaveEvt.ts b/backend/src/plugins/LocateUser/events/ChannelLeaveEvt.ts new file mode 100644 index 00000000..c9164171 --- /dev/null +++ b/backend/src/plugins/LocateUser/events/ChannelLeaveEvt.ts @@ -0,0 +1,19 @@ +import { locateUserEvent } from "../types"; +import { sendAlerts } from "../utils/sendAlerts"; +import { VoiceChannel, TextableChannel } from "eris"; + +export const ChannelLeaveEvt = locateUserEvent({ + event: "voiceChannelLeave", + + async listener(meta) { + const triggeredAlerts = await meta.pluginData.state.alerts.getAlertsByUserId(meta.args.member.id); + const voiceChannel = meta.args.oldChannel as VoiceChannel; + + triggeredAlerts.forEach(alert => { + const txtChannel = meta.pluginData.client.getChannel(alert.channel_id) as TextableChannel; + txtChannel.createMessage( + `🔴 <@!${alert.requestor_id}> the user <@!${alert.user_id}> disconnected out of \`${voiceChannel.name}\``, + ); + }); + }, +}); diff --git a/backend/src/plugins/LocateUser/events/GuildBanAddEvt.ts b/backend/src/plugins/LocateUser/events/GuildBanAddEvt.ts new file mode 100644 index 00000000..599f3aa9 --- /dev/null +++ b/backend/src/plugins/LocateUser/events/GuildBanAddEvt.ts @@ -0,0 +1,12 @@ +import { locateUserEvent } from "../types"; + +export const GuildBanAddEvt = locateUserEvent({ + event: "guildBanAdd", + + async listener(meta) { + const alerts = await meta.pluginData.state.alerts.getAlertsByUserId(meta.args.user.id); + alerts.forEach(alert => { + meta.pluginData.state.alerts.delete(alert.id); + }); + }, +}); diff --git a/backend/src/plugins/LocateUser/types.ts b/backend/src/plugins/LocateUser/types.ts new file mode 100644 index 00000000..b928862d --- /dev/null +++ b/backend/src/plugins/LocateUser/types.ts @@ -0,0 +1,15 @@ +import * as t from "io-ts"; +import { BasePluginType, command, eventListener } from "knub"; + +export const ConfigSchema = t.type({ + can_where: t.boolean, + can_alert: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface LocateUserPluginType extends BasePluginType { + config: TConfigSchema; +} + +export const locateUserCommand = command(); +export const locateUserEvent = eventListener(); diff --git a/backend/src/plugins/LocateUser/utils/createOrReuseInvite.ts b/backend/src/plugins/LocateUser/utils/createOrReuseInvite.ts new file mode 100644 index 00000000..9f453e21 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/createOrReuseInvite.ts @@ -0,0 +1,11 @@ +import { VoiceChannel } from "eris"; + +export async function createOrReuseInvite(vc: VoiceChannel) { + const existingInvites = await vc.getInvites(); + + if (existingInvites.length !== 0) { + return existingInvites[0]; + } else { + return vc.createInvite(undefined); + } +} diff --git a/backend/src/plugins/LocateUser/utils/fillAlertsList.ts b/backend/src/plugins/LocateUser/utils/fillAlertsList.ts new file mode 100644 index 00000000..675972e6 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/fillAlertsList.ts @@ -0,0 +1,9 @@ +export async function fillActiveAlertsList(pluginData) { + const allAlerts = await pluginData.state.alerts.getAllGuildAlerts(); + + allAlerts.forEach(alert => { + if (!pluginData.state.usersWithAlerts.includes(alert.user_id)) { + pluginData.state.usersWithAlerts.push(alert.user_id); + } + }); +} diff --git a/backend/src/plugins/LocateUser/utils/moveMember.ts b/backend/src/plugins/LocateUser/utils/moveMember.ts new file mode 100644 index 00000000..82de1174 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/moveMember.ts @@ -0,0 +1,25 @@ +import { Member, TextableChannel } from "eris"; +import { PluginData } from "knub"; +import { LocateUserPluginType } from "../types"; +import { sendErrorMessage } from "src/pluginUtils"; + +export async function moveMember( + pluginData: PluginData, + toMoveID: string, + target: Member, + errorChannel: TextableChannel, +) { + const modMember: Member = await this.bot.getRESTGuildMember(pluginData.guild.id, toMoveID); + if (modMember.voiceState.channelID != null) { + try { + await modMember.edit({ + channelID: target.voiceState.channelID, + }); + } catch (e) { + sendErrorMessage(pluginData, errorChannel, "Failed to move you. Are you in a voice channel?"); + return; + } + } else { + sendErrorMessage(pluginData, errorChannel, "Failed to move you. Are you in a voice channel?"); + } +} diff --git a/backend/src/plugins/LocateUser/utils/outdatedLoop.ts b/backend/src/plugins/LocateUser/utils/outdatedLoop.ts new file mode 100644 index 00000000..92d81ef9 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/outdatedLoop.ts @@ -0,0 +1,17 @@ +import { SECONDS } from "src/utils"; +import { removeUserIdFromActiveAlerts } from "./removeUserIdFromActiveAlerts"; + +const ALERT_LOOP_TIME = 30 * SECONDS; + +export async function outdatedAlertsLoop(pluginData) { + const outdatedAlerts = await pluginData.state.alerts.getOutdatedAlerts(); + + for (const alert of outdatedAlerts) { + await pluginData.state.alerts.delete(alert.id); + await removeUserIdFromActiveAlerts(pluginData, alert.user_id); + } + + if (!pluginData.state.unloaded) { + pluginData.state.outdatedAlertsTimeout = setTimeout(() => this.outdatedAlertsLoop(pluginData), ALERT_LOOP_TIME); + } +} diff --git a/backend/src/plugins/LocateUser/utils/removeUserIdFromActiveAlerts.ts b/backend/src/plugins/LocateUser/utils/removeUserIdFromActiveAlerts.ts new file mode 100644 index 00000000..9493e416 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/removeUserIdFromActiveAlerts.ts @@ -0,0 +1,6 @@ +export async function removeUserIdFromActiveAlerts(pluginData, userId: string) { + const index = pluginData.state.usersWithAlerts.indexOf(userId); + if (index > -1) { + pluginData.state.usersWithAlerts.splice(index, 1); + } +} diff --git a/backend/src/plugins/LocateUser/utils/sendAlerts.ts b/backend/src/plugins/LocateUser/utils/sendAlerts.ts new file mode 100644 index 00000000..14ac1405 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/sendAlerts.ts @@ -0,0 +1,20 @@ +import { PluginData } from "knub"; +import { LocateUserPluginType } from "../types"; +import { resolveMember } from "src/utils"; +import { sendWhere } from "./sendWhere"; +import { TextableChannel } from "eris"; +import { moveMember } from "./moveMember"; + +export async function sendAlerts(pluginData: PluginData, userId: string) { + const triggeredAlerts = await pluginData.state.alerts.getAlertsByUserId(userId); + const member = await resolveMember(pluginData.client, pluginData.guild, userId); + + triggeredAlerts.forEach(alert => { + const prepend = `<@!${alert.requestor_id}>, an alert requested by you has triggered!\nReminder: \`${alert.body}\`\n`; + const txtChannel = pluginData.client.getChannel(alert.channel_id) as TextableChannel; + sendWhere.call(this, pluginData.guild, member, txtChannel, prepend); + if (alert.active) { + moveMember(pluginData, alert.requestor_id, member, txtChannel); + } + }); +} diff --git a/backend/src/plugins/LocateUser/utils/sendWhere.ts b/backend/src/plugins/LocateUser/utils/sendWhere.ts new file mode 100644 index 00000000..87d0c690 --- /dev/null +++ b/backend/src/plugins/LocateUser/utils/sendWhere.ts @@ -0,0 +1,22 @@ +import { Guild, Member, TextableChannel, VoiceChannel } from "eris"; +import { getInviteLink } from "knub/dist/helpers"; +import { createOrReuseInvite } from "./createOrReuseInvite"; + +export async function sendWhere(guild: Guild, member: Member, channel: TextableChannel, prepend: string) { + const voice = guild.channels.get(member.voiceState.channelID) as VoiceChannel; + + if (voice == null) { + channel.createMessage(prepend + "That user is not in a channel"); + } else { + let invite = null; + try { + invite = await createOrReuseInvite(voice); + } catch (e) { + this.sendErrorMessage(channel, "Cannot create an invite to that channel!"); + return; + } + channel.createMessage( + prepend + `${member.mention} is in the following channel: \`${voice.name}\` ${getInviteLink(invite)}`, + ); + } +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index f604fbdf..6cb3688f 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,7 +1,9 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; +import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; // prettier-ignore export const guildPlugins = [ + LocateUserPlugin, UtilityPlugin, ];