mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-04 11:22:40 -05:00
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
import { logger } from "~/utils/logger";
|
|
import { SENDOU_INK_BASE_URL, userAdminPage, userPage } from "~/utils/urls";
|
|
|
|
const EMBED_FIELD_VALUE_MAX_LENGTH = 1000;
|
|
|
|
interface ModWebhookEmbed {
|
|
title: string;
|
|
fields: Array<{ name: string; value: string }>;
|
|
}
|
|
|
|
export interface WebhookUser {
|
|
username: string;
|
|
discordId: string;
|
|
customUrl: string | null;
|
|
}
|
|
|
|
/**
|
|
* Posts a rich embed to the mod channel Discord webhook.
|
|
* Fire-and-forget: meant to be called without awaiting, never throws, skipped with a log
|
|
* line when `MOD_DISCORD_WEBHOOK_URL` is unset (e.g. in development).
|
|
*/
|
|
export function sendModDiscordWebhook(embed: ModWebhookEmbed) {
|
|
sendDiscordWebhook("MOD_DISCORD_WEBHOOK_URL", embed);
|
|
}
|
|
|
|
/**
|
|
* Posts a rich embed to the SendouQ canceled matches channel Discord webhook.
|
|
* Fire-and-forget (see {@link sendModDiscordWebhook}).
|
|
*/
|
|
export function sendSQCancelDiscordWebhook(embed: ModWebhookEmbed) {
|
|
sendDiscordWebhook("SQ_CANCEL_DISCORD_WEBHOOK_URL", embed);
|
|
}
|
|
|
|
function sendDiscordWebhook(
|
|
envVarName: "MOD_DISCORD_WEBHOOK_URL" | "SQ_CANCEL_DISCORD_WEBHOOK_URL",
|
|
embed: ModWebhookEmbed,
|
|
) {
|
|
const webhookUrl = process.env[envVarName];
|
|
if (!webhookUrl) {
|
|
logger.info(`${envVarName} not set, skipping Discord webhook`);
|
|
return;
|
|
}
|
|
|
|
const body = {
|
|
embeds: [{ ...embed, timestamp: new Date().toISOString() }],
|
|
allowed_mentions: { parse: [] },
|
|
};
|
|
|
|
fetch(webhookUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
logger.error(
|
|
`Discord webhook responded with status ${response.status}`,
|
|
);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
logger.error("Failed to send Discord webhook", error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escapes then truncates free-text user input so it fits inside a Discord embed field
|
|
* value without breaking the embed's markdown formatting.
|
|
*/
|
|
export function truncateEmbedValue(text: string) {
|
|
const escaped = escapeMarkdown(text);
|
|
if (escaped.length <= EMBED_FIELD_VALUE_MAX_LENGTH) return escaped;
|
|
|
|
return `${escaped.slice(0, EMBED_FIELD_VALUE_MAX_LENGTH)}…`;
|
|
}
|
|
|
|
/** Markdown link to the user's admin tab (moderation actions & notes). */
|
|
export function userAdminPageLink(user: WebhookUser) {
|
|
return `[${escapeMarkdown(user.username)}](${SENDOU_INK_BASE_URL}${userAdminPage(user)})`;
|
|
}
|
|
|
|
/** Markdown link to the user's profile page. */
|
|
export function userPageLink(user: WebhookUser) {
|
|
return `[${escapeMarkdown(user.username)}](${SENDOU_INK_BASE_URL}${userPage(user)})`;
|
|
}
|
|
|
|
/** Backslash-escapes Discord markdown so user text can't forge links or break formatting. */
|
|
function escapeMarkdown(text: string) {
|
|
return text.replace(/[\\`*_~|()[\]]/g, "\\$&");
|
|
}
|