sendou.ink/app/modules/discord-webhook.server.ts
2026-07-21 18:14:39 +03:00

78 lines
2.2 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) {
const webhookUrl = process.env.MOD_DISCORD_WEBHOOK_URL;
if (!webhookUrl) {
logger.info(
"MOD_DISCORD_WEBHOOK_URL not set, skipping mod 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(
`Mod Discord webhook responded with status ${response.status}`,
);
}
})
.catch((error) => {
logger.error("Failed to send mod 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, "\\$&");
}