From 913120a1feb73d288cdd8026851591aa34a1dab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Bl=C3=B6meke?= Date: Wed, 26 Jun 2019 01:04:11 +0200 Subject: [PATCH 1/3] Fully functional LocatePlugin with alerts in DB and reminders --- src/PluginRuntimeError.ts | 4 +- src/data/GuildVCAlerts.ts | 54 +++++++ src/data/entities/VCAlert.ts | 20 +++ .../1561391921385-AddVCAlertTable.ts | 58 +++++++ src/plugins/LocateUser.ts | 148 ++++++++++++++++++ src/plugins/availablePlugins.ts | 2 + 6 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 src/data/GuildVCAlerts.ts create mode 100644 src/data/entities/VCAlert.ts create mode 100644 src/migrations/1561391921385-AddVCAlertTable.ts create mode 100644 src/plugins/LocateUser.ts diff --git a/src/PluginRuntimeError.ts b/src/PluginRuntimeError.ts index b5364c70..553abd98 100644 --- a/src/PluginRuntimeError.ts +++ b/src/PluginRuntimeError.ts @@ -11,11 +11,11 @@ export class PluginRuntimeError { this.guildId = guildId; } - [util.inspect.custom](depth, options) { + [util.inspect.custom]() { return `PRE [${this.pluginName}] [${this.guildId}] ${this.message}`; } toString() { - return this[util.inspect.custom](); + return this[util.inspect.custom]; } } diff --git a/src/data/GuildVCAlerts.ts b/src/data/GuildVCAlerts.ts new file mode 100644 index 00000000..d1666605 --- /dev/null +++ b/src/data/GuildVCAlerts.ts @@ -0,0 +1,54 @@ +import { BaseGuildRepository } from "./BaseGuildRepository"; +import { getRepository, Repository } from "typeorm"; +import { VCAlert } from "./entities/VCAlert"; + +export class GuildVCAlerts extends BaseGuildRepository { + private allAlerts: Repository; + + constructor(guildId) { + super(guildId); + this.allAlerts = getRepository(VCAlert); + } + + async getOutdatedAlerts(): Promise { + return this.allAlerts + .createQueryBuilder() + .where("guild_id = :guildId", { guildId: this.guildId }) + .andWhere("expires_at <= NOW()") + .getMany(); + } + + async getAllGuildAlerts(): Promise { + return this.allAlerts + .createQueryBuilder() + .where("guild_id = :guildId", { guildId: this.guildId }) + .getMany(); + } + + async getAlertsByUserId(userId: string): Promise { + return this.allAlerts.find({ + where: { + guild_id: this.guildId, + user_id: userId, + }, + }); + } + + async delete(id) { + await this.allAlerts.delete({ + guild_id: this.guildId, + id, + }); + } + + async add(requestorId: string, userId: string, channelId: string, expiresAt: string, body: string) { + await this.allAlerts.insert({ + guild_id: this.guildId, + requestor_id: requestorId, + user_id: userId, + channel_id: channelId, + expires_at: expiresAt, + body, + }); + } +} diff --git a/src/data/entities/VCAlert.ts b/src/data/entities/VCAlert.ts new file mode 100644 index 00000000..4bdf965e --- /dev/null +++ b/src/data/entities/VCAlert.ts @@ -0,0 +1,20 @@ +import { Entity, Column, PrimaryColumn } from "typeorm"; + +@Entity("vc_alerts") +export class VCAlert { + @Column() + @PrimaryColumn() + id: number; + + @Column() guild_id: string; + + @Column() requestor_id: string; + + @Column() user_id: string; + + @Column() channel_id: string; + + @Column() expires_at: string; + + @Column() body: string; +} diff --git a/src/migrations/1561391921385-AddVCAlertTable.ts b/src/migrations/1561391921385-AddVCAlertTable.ts new file mode 100644 index 00000000..afbdbe8e --- /dev/null +++ b/src/migrations/1561391921385-AddVCAlertTable.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class AddVCAlertTable1561391921385 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "vc_alerts", + columns: [ + { + name: "id", + type: "int", + unsigned: true, + isGenerated: true, + generationStrategy: "increment", + isPrimary: true, + }, + { + name: "guild_id", + type: "bigint", + unsigned: true, + }, + { + name: "requestor_id", + type: "bigint", + unsigned: true, + }, + { + name: "user_id", + type: "bigint", + unsigned: true, + }, + { + name: "channel_id", + type: "bigint", + unsigned: true, + }, + { + name: "expires_at", + type: "datetime", + }, + { + name: "body", + type: "text", + }, + ], + indices: [ + { + columnNames: ["guild_id", "user_id"], + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("vc_alerts", true, false, true); + } +} diff --git a/src/plugins/LocateUser.ts b/src/plugins/LocateUser.ts new file mode 100644 index 00000000..00452a25 --- /dev/null +++ b/src/plugins/LocateUser.ts @@ -0,0 +1,148 @@ +import { decorators as d, IPluginOptions, getInviteLink, logger } from "knub"; +import { ZeppelinPlugin } from "./ZeppelinPlugin"; +import humanizeDuration from "humanize-duration"; +import { Message, Member, Guild, TextableChannel, VoiceChannel, Channel } from "eris"; +import { GuildVCAlerts } from "../data/GuildVCAlerts"; +import moment = require("moment"); +import { resolveMember } from "../utils"; + +const ALERT_LOOP_TIME = 30 * 1000; + +interface ILocatePluginConfig { + can_where: boolean; + can_alert: boolean; +} + +export class LocatePlugin extends ZeppelinPlugin { + public static pluginName = "locate_user"; + + private alerts: GuildVCAlerts; + private outdatedAlertsTimeout; + private usersWithAlerts: string[] = []; + + getDefaultOptions(): IPluginOptions { + return { + config: { + can_where: false, + can_alert: false, + }, + overrides: [ + { + level: ">=50", + config: { + can_where: true, + can_alert: true, + }, + }, + ], + }; + } + + onLoad() { + this.alerts = GuildVCAlerts.getGuildInstance(this.guildId); + this.outdatedAlertsLoop(); + this.fillActiveAlertsList(); + } + + async outdatedAlertsLoop() { + const outdatedAlerts = await this.alerts.getOutdatedAlerts(); + + for (const alert of outdatedAlerts) { + await this.alerts.delete(alert.id); + await this.removeUserIDFromActiveAlerts(alert.user_id); + } + + this.outdatedAlertsTimeout = setTimeout(() => this.outdatedAlertsLoop(), ALERT_LOOP_TIME); + } + + async fillActiveAlertsList() { + const allAlerts = await this.alerts.getAllGuildAlerts(); + + allAlerts.forEach(alert => { + if (!this.usersWithAlerts.includes(alert.user_id)) { + this.usersWithAlerts.push(alert.user_id); + } + }); + } + + @d.command("where", "", {}) + @d.permission("can_where") + async whereCmd(msg: Message, args: { member: Member; time?: number; reminder?: string }) { + let member = await resolveMember(this.bot, this.guild, args.member.id); + sendWhere(this.guild, member, msg.channel, `${msg.member.mention} |`); + } + + @d.command("alert", " [duration:delay] [reminder:string$]", {}) + @d.permission("can_alert") + async notifyRequest(msg: Message, args: { member: Member; duration?: number; reminder?: string }) { + let time = args.duration || 600000; + let alertTime = moment().add(time, "millisecond"); + let body = args.reminder || "None"; + + this.alerts.add(msg.author.id, args.member.id, msg.channel.id, alertTime.format("YYYY-MM-DD HH:mm:ss"), body); + if (!this.usersWithAlerts.includes(args.member.id)) { + this.usersWithAlerts.push(args.member.id); + } + + msg.channel.createMessage( + `If ${args.member.mention} joins or switches VC in the next ${humanizeDuration(time)} i will notify you`, + ); + } + + @d.event("voiceChannelJoin") + async userJoinedVC(member: Member, channel: Channel) { + if (this.usersWithAlerts.includes(member.id)) { + this.sendAlerts(member.id); + await this.removeUserIDFromActiveAlerts(member.id); + } + } + + @d.event("voiceChannelSwitch") + async userSwitchedVC(member: Member, newChannel: Channel, oldChannel: Channel) { + if (this.usersWithAlerts.includes(member.id)) { + this.sendAlerts(member.id); + await this.removeUserIDFromActiveAlerts(member.id); + } + } + + async sendAlerts(userid: string) { + const triggeredAlerts = await this.alerts.getAlertsByUserId(userid); + const member = await resolveMember(this.bot, this.guild, userid); + + triggeredAlerts.forEach(alert => { + let prepend = `<@!${alert.requestor_id}>, an alert requested by you has triggered!\nReminder: \`${alert.body}\`\n`; + sendWhere(this.guild, member, this.bot.getChannel(alert.channel_id), prepend); + this.alerts.delete(alert.id); + }); + } + + async removeUserIDFromActiveAlerts(userid: string) { + const index = this.usersWithAlerts.indexOf(userid); + if (index > -1) { + this.usersWithAlerts.splice(index, 1); + } + } +} + +export async function sendWhere(guild: Guild, member: Member, channel: TextableChannel, prepend: string) { + let voice = await (guild.channels.get(member.voiceState.channelID)); + + if (voice == null) { + channel.createMessage(prepend + "That user is not in a channel"); + } else { + let invite = await createInvite(voice); + channel.createMessage( + prepend + ` ${member.mention} is in the following channel: ${voice.name} https://${getInviteLink(invite)}`, + ); + } +} + +export async function createInvite(vc: VoiceChannel) { + let existingInvites = await vc.getInvites(); + + if (existingInvites.length !== 0) { + return existingInvites[0]; + } else { + return vc.createInvite(undefined); + } +} diff --git a/src/plugins/availablePlugins.ts b/src/plugins/availablePlugins.ts index 82974437..a6644c95 100644 --- a/src/plugins/availablePlugins.ts +++ b/src/plugins/availablePlugins.ts @@ -22,6 +22,7 @@ import { BotControlPlugin } from "./BotControl"; import { UsernameSaver } from "./UsernameSaver"; import { CustomEventsPlugin } from "./CustomEvents"; import { GuildInfoSaverPlugin } from "./GuildInfoSaver"; +import { LocatePlugin } from "./LocateUser"; /** * Plugins available to be loaded for individual guilds @@ -49,6 +50,7 @@ export const availablePlugins = [ WelcomeMessagePlugin, CustomEventsPlugin, GuildInfoSaverPlugin, + LocatePlugin, ]; /** From 317442508305f4c8d8dd28a4f331517c6af53fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Bl=C3=B6meke?= Date: Fri, 28 Jun 2019 23:26:24 +0200 Subject: [PATCH 2/3] Made it possible to list all alerts and delete them Might be needed since you can potentially set reminders for far far in the future --- src/data/GuildVCAlerts.ts | 9 ++++++++ src/plugins/LocateUser.ts | 47 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/data/GuildVCAlerts.ts b/src/data/GuildVCAlerts.ts index d1666605..9da16dec 100644 --- a/src/data/GuildVCAlerts.ts +++ b/src/data/GuildVCAlerts.ts @@ -34,6 +34,15 @@ export class GuildVCAlerts extends BaseGuildRepository { }); } + async getAlertsByRequestorId(requestorId: string): Promise { + return this.allAlerts.find({ + where: { + guild_id: this.guildId, + requestor_id: requestorId, + }, + }); + } + async delete(id) { await this.allAlerts.delete({ guild_id: this.guildId, diff --git a/src/plugins/LocateUser.ts b/src/plugins/LocateUser.ts index 00452a25..c74d2c00 100644 --- a/src/plugins/LocateUser.ts +++ b/src/plugins/LocateUser.ts @@ -4,7 +4,7 @@ import humanizeDuration from "humanize-duration"; import { Message, Member, Guild, TextableChannel, VoiceChannel, Channel } from "eris"; import { GuildVCAlerts } from "../data/GuildVCAlerts"; import moment = require("moment"); -import { resolveMember } from "../utils"; +import { resolveMember, sorter, createChunkedMessage, errorMessage, successMessage } from "../utils"; const ALERT_LOOP_TIME = 30 * 1000; @@ -72,9 +72,11 @@ export class LocatePlugin extends ZeppelinPlugin { sendWhere(this.guild, member, msg.channel, `${msg.member.mention} |`); } - @d.command("alert", " [duration:delay] [reminder:string$]", {}) + @d.command("vcalert", " [duration:delay] [reminder:string$]", { + aliases: ["vca"], + }) @d.permission("can_alert") - async notifyRequest(msg: Message, args: { member: Member; duration?: number; reminder?: string }) { + async vcalertCmd(msg: Message, args: { member: Member; duration?: number; reminder?: string }) { let time = args.duration || 600000; let alertTime = moment().add(time, "millisecond"); let body = args.reminder || "None"; @@ -89,6 +91,45 @@ export class LocatePlugin extends ZeppelinPlugin { ); } + @d.command("vcalerts") + @d.permission("can_alert") + async listVcalertCmd(msg: Message) { + const alerts = await this.alerts.getAlertsByRequestorId(msg.member.id); + if (alerts.length === 0) { + this.sendErrorMessage(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}\` Member: <@!${alert.user_id}> Reminder: \`${alert.body}\``; + }); + createChunkedMessage(msg.channel, lines.join("\n")); + } + + @d.command("vcalerts delete", "", { + aliases: ["vcalerts d"], + }) + @d.permission("can_alert") + async deleteVcalertCmd(msg: Message, args: { num: number }) { + const alerts = await this.alerts.getAlertsByRequestorId(msg.member.id); + alerts.sort(sorter("expires_at")); + const lastNum = alerts.length + 1; + + if (args.num > lastNum || args.num < 0) { + msg.channel.createMessage(errorMessage("Unknown alert")); + return; + } + + const toDelete = alerts[args.num - 1]; + await this.alerts.delete(toDelete.id); + + msg.channel.createMessage(successMessage("Alert deleted")); + } + @d.event("voiceChannelJoin") async userJoinedVC(member: Member, channel: Channel) { if (this.usersWithAlerts.includes(member.id)) { From d4e76a0fd7ea175d899078932f2a4c8107a946c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Bl=C3=B6meke?= Date: Fri, 28 Jun 2019 23:53:10 +0200 Subject: [PATCH 3/3] Delete all vcalerts when a user gets banned Since they are unnecessary either way, they cannot be triggered anymore and would just idle in the DB --- src/plugins/LocateUser.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/LocateUser.ts b/src/plugins/LocateUser.ts index c74d2c00..fcdc0a9d 100644 --- a/src/plugins/LocateUser.ts +++ b/src/plugins/LocateUser.ts @@ -1,7 +1,7 @@ import { decorators as d, IPluginOptions, getInviteLink, logger } from "knub"; import { ZeppelinPlugin } from "./ZeppelinPlugin"; import humanizeDuration from "humanize-duration"; -import { Message, Member, Guild, TextableChannel, VoiceChannel, Channel } from "eris"; +import { Message, Member, Guild, TextableChannel, VoiceChannel, Channel, User } from "eris"; import { GuildVCAlerts } from "../data/GuildVCAlerts"; import moment = require("moment"); import { resolveMember, sorter, createChunkedMessage, errorMessage, successMessage } from "../utils"; @@ -146,6 +146,14 @@ export class LocatePlugin extends ZeppelinPlugin { } } + @d.event("guildBanAdd") + async onGuildBanAdd(_, user: User) { + const alerts = await this.alerts.getAlertsByUserId(user.id); + alerts.forEach(alert => { + this.alerts.delete(alert.id); + }); + } + async sendAlerts(userid: string) { const triggeredAlerts = await this.alerts.getAlertsByUserId(userid); const member = await resolveMember(this.bot, this.guild, userid);