From b6c822e82645ec28428f8ddcff82c9653ad73e0b Mon Sep 17 00:00:00 2001 From: Jonathan <54381371+yaboyaxis@users.noreply.github.com> Date: Fri, 2 Apr 2021 23:09:12 -0400 Subject: [PATCH] feat: AFK Command --- backend/src/data/AFK.ts | 31 +++++++++++++ backend/src/data/entities/AFK.ts | 14 ++++++ .../src/migrations/1617410382003-AFKStatus.ts | 34 ++++++++++++++ backend/src/plugins/AFK/AFKPlugin.ts | 46 +++++++++++++++++++ backend/src/plugins/AFK/commands/AFKCmd.ts | 46 +++++++++++++++++++ .../plugins/AFK/events/AFKNotificationEvt.ts | 28 +++++++++++ .../plugins/AFK/functions/buildAFKMessages.ts | 23 ++++++++++ .../AFK/functions/parseStatusMessage.ts | 18 ++++++++ backend/src/plugins/AFK/types.ts | 20 ++++++++ backend/src/plugins/availablePlugins.ts | 2 + 10 files changed, 262 insertions(+) create mode 100644 backend/src/data/AFK.ts create mode 100644 backend/src/data/entities/AFK.ts create mode 100644 backend/src/migrations/1617410382003-AFKStatus.ts create mode 100644 backend/src/plugins/AFK/AFKPlugin.ts create mode 100644 backend/src/plugins/AFK/commands/AFKCmd.ts create mode 100644 backend/src/plugins/AFK/events/AFKNotificationEvt.ts create mode 100644 backend/src/plugins/AFK/functions/buildAFKMessages.ts create mode 100644 backend/src/plugins/AFK/functions/parseStatusMessage.ts create mode 100644 backend/src/plugins/AFK/types.ts diff --git a/backend/src/data/AFK.ts b/backend/src/data/AFK.ts new file mode 100644 index 00000000..483f47e4 --- /dev/null +++ b/backend/src/data/AFK.ts @@ -0,0 +1,31 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { AFK as AFKEntity } from "./entities/AFK"; + +export class AFK extends BaseRepository { + private afk: Repository; + + constructor() { + super(); + this.afk = getRepository(AFKEntity); + } + + async getUserAFKStatus(user_id: string) { + return await this.afk.findOne({ user_id }); + } + + async setAfkStatus(user_id: string, status: string) { + const settings = new AFKEntity(); + settings.user_id = user_id; + settings.status = status; + + return await this.afk.save(settings); + } + + async clearAFKStatus(user_id: string) { + const afk = await this.afk.findOne({ user_id }); + if (!afk) return; + + return await this.afk.delete({ user_id }); + } +} diff --git a/backend/src/data/entities/AFK.ts b/backend/src/data/entities/AFK.ts new file mode 100644 index 00000000..174a0431 --- /dev/null +++ b/backend/src/data/entities/AFK.ts @@ -0,0 +1,14 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity("afk") +export class AFK { + @Column() + @PrimaryColumn() + id: string; + + @Column() + user_id: string; + + @Column() + status: string; +} diff --git a/backend/src/migrations/1617410382003-AFKStatus.ts b/backend/src/migrations/1617410382003-AFKStatus.ts new file mode 100644 index 00000000..b179785d --- /dev/null +++ b/backend/src/migrations/1617410382003-AFKStatus.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class AFKStatus1617410382003 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "afk", + columns: [ + { + name: "id", + type: "int", + isPrimary: true, + isGenerated: true, + generationStrategy: "increment", + }, + { + name: "user_id", + type: "bigint", + }, + { + name: "status", + type: "varchar", + length: "255", + } + ] + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + return queryRunner.dropTable("afk"); + } +} diff --git a/backend/src/plugins/AFK/AFKPlugin.ts b/backend/src/plugins/AFK/AFKPlugin.ts new file mode 100644 index 00000000..839d6650 --- /dev/null +++ b/backend/src/plugins/AFK/AFKPlugin.ts @@ -0,0 +1,46 @@ +import { PluginOptions } from "knub"; +import { AFKPluginType, ConfigSchema } from './types'; +import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; +import { AFK } from "src/data/AFK"; + +import { AfkSetCmd } from "./commands/AFKCmd"; +import { AFKNotificationEvt } from "./events/AFKNotificationEvt"; + +const defaultOptions: PluginOptions = { + config: { + can_afk: false, + allow_links: false, + allow_invites: false, + }, + overrides: [ + { + level: '>=50', + config: { + can_afk: true, + allow_links: true, + allow_invites: true, + } + } + ] +} + +export const AFKPlugin = zeppelinGuildPlugin()("afk", { + showInDocs: true, + info: { + prettyName: "AFK", + description: "Allows you to set your AFK Status.", + }, + + configSchema: ConfigSchema, + defaultOptions, + + commands: [AfkSetCmd], + events: [AFKNotificationEvt], + + onLoad(pluginData) { + const { state } = pluginData; + + state.afkUsers = new AFK(); + } +}) + diff --git a/backend/src/plugins/AFK/commands/AFKCmd.ts b/backend/src/plugins/AFK/commands/AFKCmd.ts new file mode 100644 index 00000000..ee34a235 --- /dev/null +++ b/backend/src/plugins/AFK/commands/AFKCmd.ts @@ -0,0 +1,46 @@ +import { afkCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { parseStatusMessage } from "../functions/parseStatusMessage"; + +export const AfkSetCmd = afkCmd({ + trigger: ['afk', 'afk set'], + permission: 'can_afk', + + signature: { + status: ct.string({ rest: true, required: true }), + }, + + async run({ message: msg, args, pluginData }) { + // Checks if the user is AFK, if so, return. + const isAfk = await pluginData.state.afkUsers.getUserAFKStatus(msg.author.id); + if (isAfk) return; + + const status = args.status.join(" "); + + // Check status length + if (status.length > 124) { + sendErrorMessage(pluginData, msg.channel, "Status length is above **124** characters."); + return; + } + + // Checks status based on configuration options + const parsed = parseStatusMessage(pluginData, msg.member, status); + if (typeof parsed === 'string') { + sendErrorMessage(pluginData, msg.channel, parsed); + return; + } + + // Set user status + const afk = await pluginData.state.afkUsers.setAfkStatus( + msg.author.id, + status, + ); + + sendSuccessMessage(pluginData, msg.channel, `AFK Status set to: **${afk.status}**`, { + roles: false, + everyone: false, + users: false, + }); + } +}) \ No newline at end of file diff --git a/backend/src/plugins/AFK/events/AFKNotificationEvt.ts b/backend/src/plugins/AFK/events/AFKNotificationEvt.ts new file mode 100644 index 00000000..626a4bd7 --- /dev/null +++ b/backend/src/plugins/AFK/events/AFKNotificationEvt.ts @@ -0,0 +1,28 @@ +import { sendUserMentionMessage, sendWelcomeBackMessage } from "../functions/buildAFKMessages"; +import { afkEvt } from "../types"; + +export const AFKNotificationEvt = afkEvt({ + event: 'messageCreate', + + listener: async ({ pluginData, args: { message } }) => { + // Mention Check (if someone mentions the AFK user) + if (message.mentions.length) { + const afk = await pluginData.state.afkUsers.getUserAFKStatus(message.mentions[0].id); + if (!afk) return; + + sendUserMentionMessage(message, afk.status); + + return; + } + + // Self AFK Check (if user is the one that's AFK) + const afk = await pluginData.state.afkUsers.getUserAFKStatus(message.author.id); + if (!afk) return; + + try { + await pluginData.state.afkUsers.clearAFKStatus(message.author.id); + } catch (err) {} + + sendWelcomeBackMessage(message); + } +}); \ No newline at end of file diff --git a/backend/src/plugins/AFK/functions/buildAFKMessages.ts b/backend/src/plugins/AFK/functions/buildAFKMessages.ts new file mode 100644 index 00000000..8d858744 --- /dev/null +++ b/backend/src/plugins/AFK/functions/buildAFKMessages.ts @@ -0,0 +1,23 @@ +import { Message } from "eris"; + +export function sendUserMentionMessage(message: Message, status: string) { + return message.channel.createMessage({ + allowedMentions: { + users: [message.author.id], + everyone: false, + roles: false, + }, + content: `<@!${message.author.id}>, the user mentioned is currently AFK: **${status}**`, + }); +} + +export function sendWelcomeBackMessage(message: Message) { + return message.channel.createMessage({ + allowedMentions: { + users: [message.author.id], + everyone: false, + roles: false, + }, + content: `<@!${message.author.id}>, welcome back!`, + }); +} \ No newline at end of file diff --git a/backend/src/plugins/AFK/functions/parseStatusMessage.ts b/backend/src/plugins/AFK/functions/parseStatusMessage.ts new file mode 100644 index 00000000..48f84d66 --- /dev/null +++ b/backend/src/plugins/AFK/functions/parseStatusMessage.ts @@ -0,0 +1,18 @@ +import { Member } from "eris"; +import { GuildPluginData } from "knub"; +import { hasPermission } from "src/pluginUtils"; +import { AFKPluginType } from "../types"; + +// https://github.com/sapphire-project/utilities/blob/main/packages/discord-utilities/src/lib/regexes.ts#L58 +const HttpUrlRegex = /^https?:\/\//; + +// https://github.com/sapphire-project/utilities/blob/main/packages/discord-utilities/src/lib/regexes.ts#L25 +const DiscordInviteLinkRegex = /^(?:https?:\/\/)?(?:www\.)?(?:discord\.gg\/|discord(?:app)?\.com\/invite\/)?(?[\w\d-]{2,})$/i; + +export function parseStatusMessage(pluginData: GuildPluginData, member: Member, status: string) { + const allow_links = hasPermission(pluginData, "allow_links", { member }); + const allow_invites = hasPermission(pluginData, "allow_invites", { member }); + + if (!allow_links && HttpUrlRegex.test(status)) return "Links are not allowed in an AFK status!"; + if (!allow_invites && DiscordInviteLinkRegex.test(status)) return "Invites are not allowed in an AFK status!"; +} \ No newline at end of file diff --git a/backend/src/plugins/AFK/types.ts b/backend/src/plugins/AFK/types.ts new file mode 100644 index 00000000..bf80a3f4 --- /dev/null +++ b/backend/src/plugins/AFK/types.ts @@ -0,0 +1,20 @@ +import * as t from 'io-ts'; +import { BasePluginType, guildCommand, guildEventListener } from 'knub'; +import { AFK } from '../../data/AFK'; + +export const ConfigSchema = t.type({ + can_afk: t.boolean, + allow_links: t.boolean, + allow_invites: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface AFKPluginType extends BasePluginType { + config: TConfigSchema; + state: { + afkUsers: AFK; + } +} + +export const afkCmd = guildCommand(); +export const afkEvt = guildEventListener(); \ No newline at end of file diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index e1e4e936..8e406bb8 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -33,6 +33,7 @@ import { BotControlPlugin } from "./BotControl/BotControlPlugin"; import { GuildAccessMonitorPlugin } from "./GuildAccessMonitor/GuildAccessMonitorPlugin"; import { TimeAndDatePlugin } from "./TimeAndDate/TimeAndDatePlugin"; import { CountersPlugin } from "./Counters/CountersPlugin"; +import { AFKPlugin } from './AFK/AFKPlugin'; // prettier-ignore export const guildPlugins: Array> = [ @@ -67,6 +68,7 @@ export const guildPlugins: Array> = [ CustomEventsPlugin, TimeAndDatePlugin, CountersPlugin, + AFKPlugin, ]; // prettier-ignore