From de9db230039b2450d166ccdf7d654056fc6cfa7e Mon Sep 17 00:00:00 2001 From: Hossein Mehrabi Date: Mon, 29 Jan 2024 16:17:16 +0330 Subject: [PATCH] feat: implement discord notification extension --- package.json | 3 + src/extensions/discord-notification/config.ts | 69 ++++ .../discord-notification.ts | 350 ++++++++++++++++++ src/extensions/discord-notification/index.ts | 26 ++ src/extensions/discord-notification/types.ts | 26 ++ src/index.ts | 3 + src/utils/async.utils.ts | 56 +++ yarn.lock | 212 ++++++++++- 8 files changed, 738 insertions(+), 7 deletions(-) create mode 100644 src/extensions/discord-notification/config.ts create mode 100644 src/extensions/discord-notification/discord-notification.ts create mode 100644 src/extensions/discord-notification/index.ts create mode 100644 src/extensions/discord-notification/types.ts create mode 100644 src/utils/async.utils.ts diff --git a/package.json b/package.json index 946280d..219f2dd 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@strapi/plugin-i18n": "4.16.2", "@strapi/plugin-users-permissions": "4.16.2", "@strapi/strapi": "4.16.2", + "discord.js": "^14.14.1", + "ejs": "^3.1.9", "pg": "8.8.0", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -28,6 +30,7 @@ "styled-components": "5.3.3" }, "devDependencies": { + "@types/ejs": "^3.1.5", "patch-package": "^8.0.0" }, "engines": { diff --git a/src/extensions/discord-notification/config.ts b/src/extensions/discord-notification/config.ts new file mode 100644 index 0000000..3d6e846 --- /dev/null +++ b/src/extensions/discord-notification/config.ts @@ -0,0 +1,69 @@ +import { Attribute } from "@strapi/strapi"; +import { DiscordNotificationConfig } from "./types"; + +const DISCORD_NOTIFICATION_USERNAME = + process.env.DISCORD_NOTIFICATION_USERNAME || "Logos Press Engine"; +const DISCORD_NOTIFICATION_AVATAR_URL = + process.env.DISCORD_NOTIFICATION_AVATAR_URL || + "https://press.logos.co/logo.png"; +const DISCORD_NOTIFICATION_WEBHOOK_URL = + process.env.DISCORD_NOTIFICATION_WEBHOOK_URL || ""; +const LPE_WEBSITE_URL = process.env.LPE_WEBSITE_URL || "https://press.logos.co"; + +export const discordNotificationConfig: DiscordNotificationConfig = { + username: DISCORD_NOTIFICATION_USERNAME, + avatarUrl: DISCORD_NOTIFICATION_AVATAR_URL, + webhookUrl: DISCORD_NOTIFICATION_WEBHOOK_URL, + dataTypes: [ + { + uid: "api::post.post", + enabled: true, + titleField: "title", + ignoreFields: ["summary", "body", "channels", "credits", "updatedAt"], + getUrl: (data: Attribute.GetValues<"api::post.post">) => + data.type === "Article" + ? `${LPE_WEBSITE_URL}/articles/${data.slug}` + : `${LPE_WEBSITE_URL}/podcasts/${data.podcast_show?.slug}/${data.slug}`, + getPreviewUrl: (data: Attribute.GetValues<"api::post.post">) => + `${LPE_WEBSITE_URL}/preview/post/${data.slug}`, + }, + { + uid: "api::author.author", + enabled: false, + titleField: "name", + }, + { + uid: "api::podcast-show.podcast-show", + enabled: true, + titleField: "name", + getUrl: (data: Attribute.GetValues<"api::podcast-show.podcast-show">) => + `${LPE_WEBSITE_URL}/podcasts/${data.slug}`, + ignoreFields: ["description"], + }, + { + uid: "api::page.page", + enabled: true, + titleField: "title", + ignoreFields: ["body", "updatedAt"], + getUrl: (data: Attribute.GetValues<"api::page.page">) => + `${LPE_WEBSITE_URL}/${data.slug}`, + getPreviewUrl: (data: Attribute.GetValues<"api::page.page">) => + `${LPE_WEBSITE_URL}/preview/page/${data.slug}`, + }, + { + uid: "api::tag.tag", + enabled: false, + titleField: "name", + }, + { + uid: "admin::user", + enabled: false, + titleField: "username", + }, + { + uid: "plugin::upload.file", + enabled: false, + titleField: "name", + }, + ], +}; diff --git a/src/extensions/discord-notification/discord-notification.ts b/src/extensions/discord-notification/discord-notification.ts new file mode 100644 index 0000000..2e24664 --- /dev/null +++ b/src/extensions/discord-notification/discord-notification.ts @@ -0,0 +1,350 @@ +import { Event } from "@strapi/database/dist/lifecycles/types"; +import { Common, Strapi } from "@strapi/strapi"; +import { WebhookClient } from "discord.js"; +import ejs from "ejs"; +import { isEqual } from "lodash"; +import { settle } from "../../utils/async.utils"; +import { DataType, DiscordNotificationConfig } from "./types"; + +const template = ejs.compile(` +<% if (action === "deleted") { -%> +:x: **<%- singularName %> deleted** +<% } -%> +<% if (action === "created") { -%> +:white_check_mark: **New <%- singularName.toLowerCase() %> created** +<% } -%> +<% if (action === "updated") { -%> +:pencil: **<%- singularName %> updated** +<% } -%> +<% if (action === "published") { -%> +:fire: **<%- singularName %> published** +<% } -%> +<% if (action === "unpublished") { -%> +:bangbang: **<%- singularName %> unpublished** +<% } -%> + +**[Info]** +*Title:* <%- title -%> +<% if (action === "updated" || action === "created") { %> +*Publication status:* <%= isPublished ? "published" : "draft" -%> +<% } %> + +<% if (changes) { -%> +**[Changes]** +<%- changes -%> +<% } %> + +<% if (url && isPublished) { -%> +You can visit the <%- singularName.toLowerCase() %> here: <%- url -%> +<% } else if (url && !isPublished) { -%> +You can visit the preview of the <%- singularName.toLowerCase() %> here: <%- url -%> +<% } -%> +`); + +export class DiscordNotification { + private client: WebhookClient; + + constructor( + private readonly strapi: Strapi, + private readonly config: DiscordNotificationConfig + ) {} + + init = () => { + this.client = new WebhookClient({ + url: this.config.webhookUrl, + }); + + this.config.dataTypes.forEach((dataType) => { + this.register(dataType); + }); + }; + + private register = (dataType: DataType) => { + const { strapi } = this; + + strapi.db.lifecycles.subscribe({ + models: [dataType.uid], + afterCreate: async (event) => this.onEvent(event, dataType), + afterUpdate: async (event) => this.onEvent(event, dataType), + afterDelete: async (event) => this.onEvent(event, dataType), + beforeUpdate: async (event) => this.onEvent(event, dataType), + beforeUpdateMany: async (event) => { + const { strapi } = this; + + const records = await strapi.query(dataType.uid).findMany({ + where: event.params.where, + populate: true, + }); + + event.state = { + ...event.state, + prev: records, + }; + }, + afterUpdateMany: async (event) => + void (await Promise.all( + (event.params.where?.id?.["$in"] || []) + .filter((id) => !!id) + .map((id) => + this.onEvent( + { + ...event, + params: { ...event.params, where: { id: id } }, + action: "afterUpdate", + state: { + ...event.state, + prev: ((event.state?.prev || []) as any[]).find( + (record: any) => record.id === id + ), + }, + }, + dataType + ) + ) + )), + }); + }; + + onEvent = async (event: Event, dataType: DataType) => { + const [_r, err] = await settle(() => this.handleEvent(event, dataType)); + if (err) { + this.strapi.log.error(err); + this.strapi.log.warn( + `Failed to send Discord notification for ${event.model.uid} ${event.action} event` + ); + } + }; + + handleEvent = async (event: Event, dataType: DataType) => { + const { strapi, config } = this; + + if ( + !["afterCreate", "afterUpdate", "afterDelete", "beforeUpdate"].includes( + event.action + ) + ) + return; + + const postId = + event.action === "afterCreate" + ? (event as any).result.id + : event.params.where.id; + + if (event.model.uid !== dataType.uid) return; + + if (event.action === "beforeUpdate") { + const record = await this.getRecord(dataType.uid, postId); + + event.state = { + ...event.state, + prev: record, + }; + + return; + } + + const record = await this.getRecord(dataType.uid, postId); + + const isPublished = record?.publishedAt !== null; + let action: "created" | "updated" | "deleted" | "published" | "unpublished"; + + if (event.action === "afterCreate") { + action = "created"; + } else if (event.action === "afterUpdate") { + action = "updated"; + + if (!isPublished && !!(event.state.prev as any)?.publishedAt) { + action = "unpublished"; + } else if (isPublished && !(event.state.prev as any)?.publishedAt) { + action = "published"; + } + } else if (event.action === "afterDelete") { + action = "deleted"; + } + + const url = + action === "deleted" + ? null + : isPublished + ? await dataType.getUrl?.(record) + : await dataType.getPreviewUrl?.(record); + + const changes = + event.action === "afterUpdate" + ? this.findChanges( + dataType, + event.model, + event.state?.prev || {}, + record || {} + ) + : []; + + const title = + event.params.data?.[dataType.titleField] ?? + (record?.title || "") ?? + (event as any).result.title; + + const singularName = + event.model.singularName.charAt(0).toUpperCase() + + event.model.singularName.slice(1); + + const msg = template({ + title, + singularName, + isPublished, + changes: this.changesToString(changes), + url, + action, + }); + + this.client.send({ + content: msg, + avatarURL: this.config.avatarUrl, + username: this.config.username || "Discord", + }); + }; + + changesToString = ( + changes: { + attribute: string; + previous: any; + current: any; + include?: boolean; + }[] + ) => { + let str = changes + .filter((change) => change.include) + .map((change) => { + let name = change.attribute.replace("_", " "); + name = name.charAt(0).toUpperCase() + name.slice(1); + + let res = `*${name}:* `; + + if (Array.isArray(change.previous) && Array.isArray(change.current)) { + const removed = change.previous.filter( + (item) => !change.current.includes(item) + ); + + removed.forEach((item, index) => { + res += `\n- ~~${item}~~`; + }); + + change.current.forEach((item) => { + res += `\n- ${item}`; + }); + + return res; + } + + if (change.previous && change.previous.length > 0) + res += `~~${change.previous}~~ -> `; + + if (change.current && change.current.length > 0) + res += `\`${change.current}\``; + else res += `\`[empty]\``; + + return res; + }) + .join("\n"); + + const excluded = changes.filter((change) => !change.include); + if (excluded.length > 0) + str += `\n\nother fields changed: ${excluded + .map((change) => `\`${change.attribute}\``) + .join(", ")}`; + + return str; + }; + + findChanges = ( + dataType: DataType, + model: Event["model"], + previous: any, + current: any + ) => { + const changes: { + attribute: string; + previous: any; + current: any; + include?: boolean; + }[] = []; + + const updated = { ...current }; + + const getChangedField = (val: any, key?: string) => { + if (Array.isArray(val)) { + return val.filter((v) => !!v).map((v) => getChangedField(v, key)); + } + + if (val === null) return ""; + if (typeof val === "undefined") return ""; + + if (typeof val === "object") { + return (key && val[key]) || val[Object.keys(val)[0]] || ""; + } + + return `${val}`; + }; + + Object.keys(updated).forEach((key) => { + const attr = model.attributes[key]; + + if (!attr) return; + + if (isEqual(updated[key], previous[key])) { + return; + } + + const isRelation = attr.type === "relation"; + const target = isRelation && (attr as any).target; + const conf = this.getConfigByUid(target); + + const oldValue = getChangedField(previous[key], conf?.titleField); + const newValue = getChangedField(updated[key], conf?.titleField); + + changes.push({ + attribute: key, + previous: oldValue, + current: newValue, + include: !(dataType.ignoreFields || []).includes(key), + }); + }); + + return changes; + }; + + getRecord = async (uid: Common.UID.ContentType, id: string) => { + const relations = this.getRelations(uid); + return this.strapi.query(uid).findOne({ + where: { id }, + populate: Object.fromEntries( + (await relations).map((rel) => [rel.key, true]) + ), + }); + }; + + getRelations = async (uid: Common.UID.ContentType) => { + const model = this.strapi.contentTypes[uid]; + + const relations = Object.entries(model.attributes) + .filter( + ([key, attr]) => attr.type === "relation" || attr.type === "media" + ) + .map(([key, attr]) => { + const target = + attr.type === "media" ? "plugin::upload.file" : (attr as any).target; + + return { + key, + target, + field: this.getConfigByUid(target)?.titleField || "id", + }; + }); + + return relations; + }; + + getConfigByUid = (uid: Common.UID.ContentType) => { + return this.config.dataTypes.find((dataType) => dataType.uid === uid); + }; +} diff --git a/src/extensions/discord-notification/index.ts b/src/extensions/discord-notification/index.ts new file mode 100644 index 0000000..39561de --- /dev/null +++ b/src/extensions/discord-notification/index.ts @@ -0,0 +1,26 @@ +import { Strapi } from "@strapi/strapi"; +import { settleSync } from "../../utils/async.utils"; +import { discordNotificationConfig } from "./config"; +import { DiscordNotification } from "./discord-notification"; + +export * from "./discord-notification"; + +const register = async ({ strapi }: { strapi: Strapi }) => {}; + +const bootstrap = async ({ strapi }: { strapi: Strapi }) => { + const discordNotification = new DiscordNotification( + strapi, + discordNotificationConfig + ); + + const [_r, err] = settleSync(() => discordNotification.init()); + if (err) { + strapi.log.error(err); + strapi.log.warn("Failed to initialize Discord notification extension."); + } +}; + +export const discordNotificationExtension = { + register, + bootstrap, +}; diff --git a/src/extensions/discord-notification/types.ts b/src/extensions/discord-notification/types.ts new file mode 100644 index 0000000..ecc0ced --- /dev/null +++ b/src/extensions/discord-notification/types.ts @@ -0,0 +1,26 @@ +import { Attribute, Common } from "@strapi/strapi"; + +export type DataType< + U extends Common.UID.CollectionType = Common.UID.CollectionType +> = { + uid: U; + enabled: boolean; + titleField: string; + ignoreFields?: string[]; + + getUrl?: ( + data: Attribute.GetValues + ) => string | null | Promise; + + getPreviewUrl?: ( + data: Attribute.GetValues + ) => string | null | Promise; +}; + +export type DiscordNotificationConfig = { + webhookUrl: string; + username: string; + avatarUrl?: string; + + dataTypes: DataType[]; +}; diff --git a/src/index.ts b/src/index.ts index 0f8fec4..9e4d45a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { Strapi } from "@strapi/strapi"; +import { discordNotificationExtension } from "./extensions/discord-notification"; import { searchExtension } from "./extensions/search"; export default { @@ -10,6 +11,7 @@ export default { */ async register({ strapi }) { await searchExtension.register({ strapi }); + await discordNotificationExtension.register({ strapi }); }, /** @@ -21,5 +23,6 @@ export default { */ async bootstrap({ strapi }: { strapi: Strapi }) { await searchExtension.bootstrap({ strapi }); + await discordNotificationExtension.bootstrap({ strapi }); }, }; diff --git a/src/utils/async.utils.ts b/src/utils/async.utils.ts new file mode 100644 index 0000000..eef2581 --- /dev/null +++ b/src/utils/async.utils.ts @@ -0,0 +1,56 @@ +export const settle = async ( + promise: Promise | (() => R | Promise), +): Promise<[R, undefined] | [undefined, E]> => { + try { + const result: R = + typeof promise === 'function' ? await promise() : await promise + return [result, undefined] + } catch (error) { + return [undefined, error as E] + } +} + +export const settleSync = ( + func: () => R, +): [R, undefined] | [undefined, E] => { + try { + return [func(), undefined] + } catch (error) { + return [undefined, error as E] + } +} + +export type CreatePromiseResult = { + promise: Promise + reject: (err: E) => void + resolve: (result: T) => void + callback: (data: T, error?: E) => void +} + +export const createPromise = (): CreatePromiseResult< + T, + E +> => { + let resolve: any, reject: any + + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + + const callback = (data: T, error?: E) => { + if (error) return void reject(error) + resolve(data) + } + + return { + reject, + resolve, + promise, + + callback, + } +} + +export const sleep = (ms: number) => + new Promise(() => setTimeout(Promise.resolve, ms)) diff --git a/yarn.lock b/yarn.lock index e585a85..1235410 100644 --- a/yarn.lock +++ b/yarn.lock @@ -840,6 +840,71 @@ enabled "2.0.x" kuler "^2.0.0" +"@discordjs/builders@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.7.0.tgz#e2478c7e55b0f4c40837edb8f102bce977323a37" + integrity sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw== + dependencies: + "@discordjs/formatters" "^0.3.3" + "@discordjs/util" "^1.0.2" + "@sapphire/shapeshift" "^3.9.3" + discord-api-types "0.37.61" + fast-deep-equal "^3.1.3" + ts-mixer "^6.0.3" + tslib "^2.6.2" + +"@discordjs/collection@1.5.3": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" + integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== + +"@discordjs/collection@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-2.0.0.tgz#409b80c74eb8486cc4ee6a9b83426aaff1380f8c" + integrity sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w== + +"@discordjs/formatters@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@discordjs/formatters/-/formatters-0.3.3.tgz#b16fdd79bb819680ab7e519193004e9dc124a749" + integrity sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w== + dependencies: + discord-api-types "0.37.61" + +"@discordjs/rest@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-2.2.0.tgz#f4ec00d3faff965c00a879b7e87bb4b6f4446966" + integrity sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A== + dependencies: + "@discordjs/collection" "^2.0.0" + "@discordjs/util" "^1.0.2" + "@sapphire/async-queue" "^1.5.0" + "@sapphire/snowflake" "^3.5.1" + "@vladfrangu/async_event_emitter" "^2.2.2" + discord-api-types "0.37.61" + magic-bytes.js "^1.5.0" + tslib "^2.6.2" + undici "5.27.2" + +"@discordjs/util@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-1.0.2.tgz#dc1896d764452b1bd9707eb9aa99ccfbb30bd1c0" + integrity sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw== + +"@discordjs/ws@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@discordjs/ws/-/ws-1.0.2.tgz#3933b12d4686aabf6a95dfe5fb6e744342a661d1" + integrity sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q== + dependencies: + "@discordjs/collection" "^2.0.0" + "@discordjs/rest" "^2.1.0" + "@discordjs/util" "^1.0.2" + "@sapphire/async-queue" "^1.5.0" + "@types/ws" "^8.5.9" + "@vladfrangu/async_event_emitter" "^2.2.2" + discord-api-types "0.37.61" + tslib "^2.6.2" + ws "^8.14.2" + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1285,6 +1350,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz#1205014625790c7ff0e471644a878a65d1e34ab0" integrity sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw== +"@fastify/busboy@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" + integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA== + "@floating-ui/core@^1.5.3": version "1.5.3" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" @@ -2044,6 +2114,29 @@ colors "~1.2.1" string-argv "~0.3.1" +"@sapphire/async-queue@^1.5.0": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.2.tgz#2982dce16e5b8b1ea792604d20c23c0585877b97" + integrity sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg== + +"@sapphire/shapeshift@^3.9.3": + version "3.9.6" + resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.9.6.tgz#bd9629c08641f5b94ae094e23f092187a3ed9a7d" + integrity sha512-4+Na/fxu2SEepZRb9z0dbsVh59QtwPuBg/UVaDib3av7ZY14b14+z09z6QVn0P6Dv6eOU2NDTsjIi0mbtgP56g== + dependencies: + fast-deep-equal "^3.1.3" + lodash "^4.17.21" + +"@sapphire/snowflake@3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.1.tgz#254521c188b49e8b2d4cc048b475fb2b38737fec" + integrity sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA== + +"@sapphire/snowflake@^3.5.1": + version "3.5.3" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" + integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== + "@sentry/core@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785" @@ -2829,6 +2922,11 @@ "@types/keygrip" "*" "@types/node" "*" +"@types/ejs@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117" + integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg== + "@types/eslint-scope@^3.7.3": version "3.7.7" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" @@ -3128,6 +3226,20 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/ws@8.5.9": + version "8.5.9" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.9.tgz#384c489f99c83225a53f01ebc3eddf3b8e202a8c" + integrity sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.9": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + "@ucast/core@^1.0.0", "@ucast/core@^1.4.1", "@ucast/core@^1.6.1": version "1.10.2" resolved "https://registry.yarnpkg.com/@ucast/core/-/core-1.10.2.tgz#30b6b893479823265368e528b61b042f752f2c92" @@ -3192,6 +3304,11 @@ "@types/babel__core" "^7.20.2" react-refresh "^0.14.0" +"@vladfrangu/async_event_emitter@^2.2.2": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz#d3537432c6db6444680a596271dff8ea407343b3" + integrity sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug== + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -4060,7 +4177,7 @@ caniuse-lite@^1.0.30001565: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4" integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== -chalk@4.1.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@4.1.2, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -4931,6 +5048,31 @@ direction@^1.0.3: resolved "https://registry.yarnpkg.com/direction/-/direction-1.0.4.tgz#2b86fb686967e987088caf8b89059370d4837442" integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ== +discord-api-types@0.37.61: + version "0.37.61" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.61.tgz#9dd8e58c624237e6f1b23be2d29579af268b8c5b" + integrity sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw== + +discord.js@^14.14.1: + version "14.14.1" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.14.1.tgz#9a2bea23bba13819705ab87612837610abce9ee3" + integrity sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w== + dependencies: + "@discordjs/builders" "^1.7.0" + "@discordjs/collection" "1.5.3" + "@discordjs/formatters" "^0.3.3" + "@discordjs/rest" "^2.1.0" + "@discordjs/util" "^1.0.2" + "@discordjs/ws" "^1.0.2" + "@sapphire/snowflake" "3.5.1" + "@types/ws" "8.5.9" + discord-api-types "0.37.61" + fast-deep-equal "3.1.3" + lodash.snakecase "4.1.1" + tslib "2.6.2" + undici "5.27.2" + ws "8.14.2" + dkim-signer@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/dkim-signer/-/dkim-signer-0.2.2.tgz#aa81ec071eeed3622781baa922044d7800e5f308" @@ -5068,6 +5210,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +ejs@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.4.601: version "1.4.626" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.626.tgz#c20e1706354a31721b65e81496800534dd04b222" @@ -5487,6 +5636,13 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -6845,6 +7001,16 @@ iterall@^1.3.0: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +jake@^10.8.5: + version "10.8.7" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" @@ -7339,6 +7505,11 @@ lodash.isplainobject@4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== +lodash.snakecase@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -7451,6 +7622,11 @@ luxon@^1.26.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.1.tgz#528cdf3624a54506d710290a2341aa8e6e6c61b0" integrity sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw== +magic-bytes.js@^1.5.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.8.0.tgz#8362793c60cd77c2dd77db6420be727192df68e2" + integrity sha512-lyWpfvNGVb5lu8YUAbER0+UMBTdR63w2mcSUlhhBTyVbxJvjgqwyAf3AZD6MprgK0uHuBoWXSDAMWLupX83o3Q== + mailcomposer@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-3.12.0.tgz#9c5e1188aa8e1c62ec8b86bd43468102b639e8f9" @@ -7682,7 +7858,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1: +minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -10277,16 +10453,21 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +ts-mixer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.3.tgz#69bd50f406ff39daa369885b16c77a6194c7cae6" + integrity sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ== + +tslib@2.6.2, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.0, tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tslib@^1.10.0, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.0: - version "2.6.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" - integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== - tsscmp@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -10388,6 +10569,13 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici@5.27.2: + version "5.27.2" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.27.2.tgz#a270c563aea5b46cc0df2550523638c95c5d4411" + integrity sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ== + dependencies: + "@fastify/busboy" "^2.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -10843,11 +11031,21 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.14.2: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + ws@^7.3.1, ws@^7.4.6: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.14.2: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"