mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
90
apps/web-react/app/modules/discord-webhook.server.ts
Normal file
90
apps/web-react/app/modules/discord-webhook.server.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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. */
|
||||
export function escapeMarkdown(text: string) {
|
||||
return text.replace(/[\\`*_~|()[\]]/g, "\\$&");
|
||||
}
|
||||
Reference in New Issue
Block a user