feat: webhook support

This commit is contained in:
neoarz
2026-04-22 22:02:36 -04:00
parent 2d003eaaee
commit 734a6cdde3
5 changed files with 407 additions and 35 deletions

190
webhook.ts Normal file
View File

@@ -0,0 +1,190 @@
import type { PluginNative } from "@utils/types";
import type {
ClaimRequest,
FinderProfile,
WebhookEmbed,
WebhookField,
WebhookPayload,
WebhookResult
} from "./types";
const SUCCESS_COLOR = 0x43b581;
const FAILURE_COLOR = 0xf04747;
const TEST_COLOR = 0x5865f2;
const WEBHOOK_NAME = "NitroSniper";
function parseWebhookUrl(webhookUrl: string) {
const trimmed = webhookUrl.trim();
if (!trimmed) return null;
try {
return new URL(trimmed);
} catch {
throw new Error("Webhook URL is invalid.");
}
}
function getNative() {
const native = (globalThis as any).VencordNative?.pluginHelpers?.NitroSniper as PluginNative<typeof import("./native")> | undefined;
if (!native) {
throw new Error("Webhook sending requires desktop native support.");
}
return native;
}
function createPayload(embeds: WebhookEmbed[]): WebhookPayload {
return {
username: WEBHOOK_NAME,
embeds,
allowed_mentions: {
parse: []
}
};
}
function buildUserProfileUrl(userId?: string) {
return userId ? `https://discord.com/users/${userId}` : null;
}
function buildMessageUrl(request: ClaimRequest) {
if (!request.channelId || !request.messageId) return null;
return `https://discordapp.com/channels/${request.guildId ?? "@me"}/${request.channelId}/${request.messageId}`;
}
function buildAuthorField(request: ClaimRequest): WebhookField | null {
const label = request.authorName ?? request.authorUsername ?? request.authorId;
if (!label) return null;
const profileUrl = buildUserProfileUrl(request.authorId);
return {
name: "Code sent by:",
value: profileUrl ? `[${label}](${profileUrl})` : label,
inline: false
};
}
function buildMessageField(request: ClaimRequest): WebhookField | null {
const messageUrl = buildMessageUrl(request);
if (!messageUrl) return null;
return {
name: "Message:",
value: `[Posted here!](${messageUrl})`,
inline: false
};
}
function buildClaimFields(request: ClaimRequest) {
return [
buildAuthorField(request),
buildMessageField(request)
].filter((field): field is WebhookField => field != null);
}
function getResultPresentation(result: WebhookResult) {
switch (result) {
case "claimed":
return {
title: "Yay! Claimed a Nitro!",
color: SUCCESS_COLOR
};
case "failed":
default:
return {
title: "Failed to claim nitro",
color: FAILURE_COLOR
};
}
}
function buildClaimEmbed(finder: FinderProfile, result: WebhookResult, request: ClaimRequest): WebhookEmbed {
const presentation = getResultPresentation(result);
return {
title: presentation.title,
color: presentation.color,
fields: buildClaimFields(request),
timestamp: new Date().toISOString(),
author: {
name: finder.name,
icon_url: finder.iconUrl
},
footer: {
text: WEBHOOK_NAME
}
};
}
function buildTestWebhookPayload(): WebhookPayload {
return createPayload([
{
title: "NitroSniper Webhook Test",
color: TEST_COLOR,
description: "Your NitroSniper webhook is configured correctly.",
timestamp: new Date().toISOString(),
footer: {
text: WEBHOOK_NAME
}
}
]);
}
function buildClaimWebhookPayload(finder: FinderProfile, result: WebhookResult, request: ClaimRequest): WebhookPayload {
return createPayload([
buildClaimEmbed(finder, result, request)
]);
}
function parseWebhookError(data: string, status: number) {
if (!data) {
return `Webhook request failed with status ${status}.`;
}
try {
const body = JSON.parse(data) as { message?: string; errors?: unknown; };
const detail = [
body.message,
body.errors ? JSON.stringify(body.errors) : null
]
.filter(Boolean)
.join(" ");
return detail
? `Webhook request failed with status ${status}: ${detail}`
: `Webhook request failed with status ${status}.`;
} catch {
return `Webhook request failed with status ${status}: ${data}`;
}
}
async function postWebhook(url: URL, payload: WebhookPayload) {
const { status, data } = await getNative().sendWebhook(url.toString(), JSON.stringify(payload));
if (status < 200 || status >= 300) {
throw new Error(parseWebhookError(data, status));
}
}
export async function sendClaimWebhook(
webhookUrl: string,
finder: FinderProfile,
result: WebhookResult,
request: ClaimRequest
) {
const url = parseWebhookUrl(webhookUrl);
if (!url) return;
await postWebhook(url, buildClaimWebhookPayload(finder, result, request));
}
export async function sendTestWebhook(webhookUrl: string) {
const url = parseWebhookUrl(webhookUrl);
if (!url) {
throw new Error("Webhook URL is empty.");
}
await postWebhook(url, buildTestWebhookPayload());
}