mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-15 07:36:27 -05:00
81 lines
2.5 KiB
TypeScript
81 lines
2.5 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;
|
|
}
|
|
|
|
/** Fire-and-forget embed to the mod channel: never throws, skipped with a log line when `MOD_DISCORD_WEBHOOK_URL` is unset. */
|
|
export function sendModDiscordWebhook(embed: ModWebhookEmbed) {
|
|
sendDiscordWebhook("MOD_DISCORD_WEBHOOK_URL", embed);
|
|
}
|
|
|
|
/** Embed to the SendouQ canceled matches channel, fire-and-forget like {@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 user text to fit an embed field value without breaking its markdown. */
|
|
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. */
|
|
export function escapeMarkdown(text: string) {
|
|
return text.replace(/[\\`*_~|()[\]]/g, "\\$&");
|
|
}
|