feat: implement discord notification extension

This commit is contained in:
Hossein Mehrabi 2024-01-29 16:17:16 +03:30
parent d18abbe2d9
commit de9db23003
No known key found for this signature in database
GPG Key ID: 45C04964191AFAA1
8 changed files with 738 additions and 7 deletions

View File

@ -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": {

View File

@ -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",
},
],
};

View File

@ -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);
};
}

View File

@ -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,
};

View File

@ -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<U>
) => string | null | Promise<string | null>;
getPreviewUrl?: (
data: Attribute.GetValues<U>
) => string | null | Promise<string | null>;
};
export type DiscordNotificationConfig = {
webhookUrl: string;
username: string;
avatarUrl?: string;
dataTypes: DataType[];
};

View File

@ -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 });
},
};

56
src/utils/async.utils.ts Normal file
View File

@ -0,0 +1,56 @@
export const settle = async <R, E = Error>(
promise: Promise<R> | (() => R | Promise<R>),
): 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 = <R, E = Error>(
func: () => R,
): [R, undefined] | [undefined, E] => {
try {
return [func(), undefined]
} catch (error) {
return [undefined, error as E]
}
}
export type CreatePromiseResult<T = any, E = Error> = {
promise: Promise<T>
reject: (err: E) => void
resolve: (result: T) => void
callback: (data: T, error?: E) => void
}
export const createPromise = <T = any, E = Error>(): CreatePromiseResult<
T,
E
> => {
let resolve: any, reject: any
const promise = new Promise<T>((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))

212
yarn.lock
View File

@ -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"