feat: harden webhook error handlin and escape sender labels

This commit is contained in:
neo
2026-04-28 00:25:17 -04:00
parent 734a6cdde3
commit 4e7b4fe662
5 changed files with 43 additions and 34 deletions

View File

@@ -14,7 +14,7 @@ import { findByPropsLazy } from "@webpack";
import { UserStore } from "@webpack/common"; import { UserStore } from "@webpack/common";
import { settings } from "./settings"; import { settings } from "./settings";
import type { ClaimRequest, FinderProfile, WebhookResult } from "./types"; import type { ClaimRequest, WebhookResult } from "./types";
import { sendClaimWebhook } from "./webhook"; import { sendClaimWebhook } from "./webhook";
const GIFT_LINK_REGEX = /(?:discord\.gift\/|discord\.com\/gifts?\/)([a-zA-Z0-9]{16,24})/; const GIFT_LINK_REGEX = /(?:discord\.gift\/|discord\.com\/gifts?\/)([a-zA-Z0-9]{16,24})/;
@@ -32,6 +32,10 @@ function resetState() {
claiming = false; claiming = false;
} }
function toError(error: unknown) {
return error instanceof Error ? error : new Error(String(error));
}
function isOwnMessage(message: Message) { function isOwnMessage(message: Message) {
return message.author?.id === UserStore.getCurrentUser()?.id; return message.author?.id === UserStore.getCurrentUser()?.id;
} }
@@ -52,32 +56,26 @@ function createClaimRequest(message: Message): ClaimRequest | null {
const code = message.content ? extractGiftCode(message.content) : null; const code = message.content ? extractGiftCode(message.content) : null;
if (!code) return null; if (!code) return null;
const authorId = message.author?.id;
const authorAvatar = message.author?.avatar;
return { return {
code, code,
authorId: message.author?.id, authorId,
authorName: message.author?.globalName ?? message.author?.username, authorName: message.author?.globalName ?? message.author?.username,
authorUsername: message.author?.username, authorUsername: message.author?.username,
authorAvatarUrl: authorId && authorAvatar
? `https://cdn.discordapp.com/avatars/${authorId}/${authorAvatar}.png?size=128`
: undefined,
channelId: message.channel_id, channelId: message.channel_id,
guildId: message.guild_id, guildId: message.guild_id,
messageId: message.id messageId: message.id
}; };
} }
function getFinderProfile(): FinderProfile {
const currentUser = UserStore.getCurrentUser();
return {
name: currentUser?.globalName ?? currentUser?.username ?? "NitroSniper",
iconUrl: currentUser?.avatar
? `https://cdn.discordapp.com/avatars/${currentUser.id}/${currentUser.avatar}.png?size=128`
: undefined
};
}
function notifyClaim(result: WebhookResult, request: ClaimRequest) { function notifyClaim(result: WebhookResult, request: ClaimRequest) {
void sendClaimWebhook( void sendClaimWebhook(
settings.store.webhookUrl, settings.store.webhookUrl,
getFinderProfile(),
result, result,
request request
).catch(webhookError => { ).catch(webhookError => {
@@ -112,7 +110,7 @@ function processQueue() {
GiftActions.redeemGiftCode({ GiftActions.redeemGiftCode({
code: request.code, code: request.code,
onRedeemed: () => handleClaimSuccess(request), onRedeemed: () => handleClaimSuccess(request),
onError: (error: Error) => handleClaimFailure(request, error) onError: (error: unknown) => handleClaimFailure(request, toError(error))
}); });
} }

View File

@@ -22,7 +22,7 @@ export async function sendWebhook(_: IpcMainInvokeEvent, webhookUrl: string, pay
} catch (error) { } catch (error) {
return { return {
status: -1, status: -1,
data: String(error) data: error instanceof Error ? error.message : String(error)
}; };
} }
} }

View File

@@ -4,6 +4,12 @@ import { Button, showToast, Toasts } from "@webpack/common";
import { sendTestWebhook } from "./webhook"; import { sendTestWebhook } from "./webhook";
function getToastErrorMessage(error: unknown) {
return error instanceof Error
? error.message
: "Failed to send test webhook.";
}
function TestWebhookButton() { function TestWebhookButton() {
const { webhookUrl } = settings.use(["webhookUrl"]); const { webhookUrl } = settings.use(["webhookUrl"]);
const disabled = webhookUrl.trim().length === 0; const disabled = webhookUrl.trim().length === 0;
@@ -16,8 +22,8 @@ function TestWebhookButton() {
.then(() => { .then(() => {
showToast("Test webhook sent successfully.", Toasts.Type.SUCCESS); showToast("Test webhook sent successfully.", Toasts.Type.SUCCESS);
}) })
.catch((error: Error) => { .catch((error: unknown) => {
showToast(error.message, Toasts.Type.FAILURE); showToast(getToastErrorMessage(error), Toasts.Type.FAILURE);
}); });
}} }}
> >

View File

@@ -3,16 +3,12 @@ export interface ClaimRequest {
authorId?: string; authorId?: string;
authorName?: string; authorName?: string;
authorUsername?: string; authorUsername?: string;
authorAvatarUrl?: string;
channelId?: string; channelId?: string;
guildId?: string; guildId?: string;
messageId?: string; messageId?: string;
} }
export interface FinderProfile {
name: string;
iconUrl?: string;
}
export type WebhookResult = "claimed" | "failed"; export type WebhookResult = "claimed" | "failed";
export interface WebhookField { export interface WebhookField {

View File

@@ -2,7 +2,6 @@ import type { PluginNative } from "@utils/types";
import type { import type {
ClaimRequest, ClaimRequest,
FinderProfile,
WebhookEmbed, WebhookEmbed,
WebhookField, WebhookField,
WebhookPayload, WebhookPayload,
@@ -54,6 +53,10 @@ function buildMessageUrl(request: ClaimRequest) {
return `https://discordapp.com/channels/${request.guildId ?? "@me"}/${request.channelId}/${request.messageId}`; return `https://discordapp.com/channels/${request.guildId ?? "@me"}/${request.channelId}/${request.messageId}`;
} }
function escapeMarkdown(value: string) {
return value.replace(/([\\`*_{}\[\]()#+\-.!|>~])/g, "\\$1");
}
function buildAuthorField(request: ClaimRequest): WebhookField | null { function buildAuthorField(request: ClaimRequest): WebhookField | null {
const label = request.authorName ?? request.authorUsername ?? request.authorId; const label = request.authorName ?? request.authorUsername ?? request.authorId;
if (!label) return null; if (!label) return null;
@@ -61,7 +64,7 @@ function buildAuthorField(request: ClaimRequest): WebhookField | null {
const profileUrl = buildUserProfileUrl(request.authorId); const profileUrl = buildUserProfileUrl(request.authorId);
return { return {
name: "Code sent by:", name: "Code sent by:",
value: profileUrl ? `[${label}](${profileUrl})` : label, value: profileUrl ? `[${escapeMarkdown(label)}](${profileUrl})` : escapeMarkdown(label),
inline: false inline: false
}; };
} }
@@ -100,7 +103,17 @@ function getResultPresentation(result: WebhookResult) {
} }
} }
function buildClaimEmbed(finder: FinderProfile, result: WebhookResult, request: ClaimRequest): WebhookEmbed { function buildEmbedAuthor(request: ClaimRequest) {
const name = request.authorName ?? request.authorUsername;
if (!name) return undefined;
return {
name,
icon_url: request.authorAvatarUrl
};
}
function buildClaimEmbed(result: WebhookResult, request: ClaimRequest): WebhookEmbed {
const presentation = getResultPresentation(result); const presentation = getResultPresentation(result);
return { return {
@@ -108,10 +121,7 @@ function buildClaimEmbed(finder: FinderProfile, result: WebhookResult, request:
color: presentation.color, color: presentation.color,
fields: buildClaimFields(request), fields: buildClaimFields(request),
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
author: { author: buildEmbedAuthor(request),
name: finder.name,
icon_url: finder.iconUrl
},
footer: { footer: {
text: WEBHOOK_NAME text: WEBHOOK_NAME
} }
@@ -132,9 +142,9 @@ function buildTestWebhookPayload(): WebhookPayload {
]); ]);
} }
function buildClaimWebhookPayload(finder: FinderProfile, result: WebhookResult, request: ClaimRequest): WebhookPayload { function buildClaimWebhookPayload(result: WebhookResult, request: ClaimRequest): WebhookPayload {
return createPayload([ return createPayload([
buildClaimEmbed(finder, result, request) buildClaimEmbed(result, request)
]); ]);
} }
@@ -170,14 +180,13 @@ async function postWebhook(url: URL, payload: WebhookPayload) {
export async function sendClaimWebhook( export async function sendClaimWebhook(
webhookUrl: string, webhookUrl: string,
finder: FinderProfile,
result: WebhookResult, result: WebhookResult,
request: ClaimRequest request: ClaimRequest
) { ) {
const url = parseWebhookUrl(webhookUrl); const url = parseWebhookUrl(webhookUrl);
if (!url) return; if (!url) return;
await postWebhook(url, buildClaimWebhookPayload(finder, result, request)); await postWebhook(url, buildClaimWebhookPayload(result, request));
} }
export async function sendTestWebhook(webhookUrl: string) { export async function sendTestWebhook(webhookUrl: string) {