- feat(rubgram): add webhook for submission

- fix(rubgram): return to register page after timer expire
- fix(rubgram): hydration warning fix
This commit is contained in:
2026-02-08 16:14:25 +07:00 Unverified
parent 2bd0329c27
commit 3fe84d0c5c
6 changed files with 124 additions and 6 deletions
+1 -1
View File
@@ -187,7 +187,7 @@ export function CharacterChooser({
</span>
</div>
<center>ออกจากเกมเพื่อให้ข้อมูลอัพเดท แล้วกดรีโหลด</center>
<AlertDialogFooter>
<AlertDialogFooter suppressHydrationWarning>
<AlertDialogCancel>ปิดหน้าต่าง</AlertDialogCancel>
<Countdown
time={cacheTtl}
+10 -3
View File
@@ -216,7 +216,10 @@ export async function submitEndgame(formData: EndgameFormData) {
})
.returning({ queue: endgameSubmissions.queue, id: endgameSubmissions.id });
revalidatePath("/rubgram/admin");
ps.publish({}, { topic: "rubgram", event: "update" });
ps.publish(
{ type: "submit", sub: queue.id },
{ topic: "rubgram", event: "update" },
);
return queue;
}
@@ -286,7 +289,11 @@ export async function submitEndgamePayment(formData: EndgamePaymentFormData) {
.where(eq(endgameSubmissions.id, sid));
});
revalidatePath("/rubgram/admin");
ps.publish({}, { topic: "rubgram", event: "update" });
if (Array.isArray(queue))
ps.publish(
{ type: "paid", sub: queue[0].id },
{ topic: "rubgram", event: "update" },
);
return Array.isArray(queue) ? queue[0] : queue;
}
@@ -401,7 +408,7 @@ export async function cancel(sid: string) {
await removeExpiredSubmissions();
ps.publish({}, { topic: "rubgram", event: "update" });
ps.publish({ type: "cancel" }, { topic: "rubgram", event: "update" });
revalidatePath("/rubgram");
}
+12
View File
@@ -1,6 +1,7 @@
"use client";
import { ArrowRight, Check, Download, LogIn, ScrollText } from "lucide-react";
import { useRouter } from "next/navigation";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Blocker } from "@/components/blocker";
@@ -185,11 +186,22 @@ export function DownloadButton() {
export function Countdown({
time,
render = String,
refreshWhenOver = false,
}: {
time: Date;
render?: (time: string | null) => ReactNode;
refreshWhenOver?: boolean;
}) {
const [now, setNow] = useState(new Date());
const router = useRouter();
useEffect(() => {
if (!refreshWhenOver) return;
const wait = time.getTime() - Date.now();
if (wait < 0) return;
const timeout = setTimeout(() => router.refresh(), wait);
return () => clearTimeout(timeout);
}, [time, router, refreshWhenOver]);
useEffect(() => {
const interval = setInterval(() => setNow(new Date()), 1000);
+2 -2
View File
@@ -175,8 +175,8 @@ export default async function EndgamePage() {
<span className="font-bold text-lg">
ยอดชำระ {q.price} บาท{" "}
{q.expires && (
<Kbd>
<Countdown time={q.expires} />
<Kbd suppressHydrationWarning>
<Countdown time={q.expires} refreshWhenOver />
</Kbd>
)}
</span>
+46
View File
@@ -1,4 +1,5 @@
import { env } from "node:process";
import { redis } from "bun";
import { formatDistanceToNow } from "date-fns";
import { and, asc, eq, isNull, lt, not, sql } from "drizzle-orm";
import { removeExpiredSubmissions } from "@/app/(ui)/rubgram/api";
@@ -16,6 +17,7 @@ import {
tierlistTiers,
user,
} from "@/lib/db/schema";
import { rubgramWebhookTemplate } from "./webhook";
function logger(group: string) {
const prefix = `[${group}]`;
@@ -245,9 +247,53 @@ async function cacheCards() {
} else schedule(120, cacheCards);
}
const redisSubscribers: Record<
string,
(payload: { data: unknown; event?: string }) => void
> = {
"sse:rubgram": async ({ data, event }) => {
type RubgramEvent = { type: "submit" | "paid" | "cancel"; sub: string };
if (event !== "update") return;
const { type, sub } = data as RubgramEvent;
if (type === "cancel") return;
if (!process.env.WEBHOOK_RUBGRAM_SUBMIT) return;
const body = rubgramWebhookTemplate(
type,
await db
.select()
.from(endgameSubmissions)
.where(eq(endgameSubmissions.id, sub))
.limit(1)
.then((s) => s[0]),
await db.select().from(endgameTypes),
);
const res = await fetch(
`${process.env.WEBHOOK_RUBGRAM_SUBMIT}?with_components=true`,
{
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
},
);
if (!res.ok)
console.error(
"Webhook request wasn't ok.",
await res.clone().json().catch(res.text.bind(res)),
);
},
};
checkRubgramExpiration();
seedDatabase();
cacheCards();
console.log("Tasks assigned");
for (const [name, handler] of Object.entries(redisSubscribers))
redis.subscribe(name, (p) => handler(JSON.parse(p)));
process
.addListener("uncaughtException", console.error)
.addListener("unhandledRejection", console.error);
process.on(15 as unknown as "SIGTERM", process.exit);
+53
View File
@@ -0,0 +1,53 @@
import type { endgameSubmissions, endgameTypes } from "@/lib/db/schema";
export function rubgramWebhookTemplate(
type: "submit" | "paid",
sub: typeof endgameSubmissions.$inferSelect,
types: (typeof endgameTypes.$inferSelect)[],
) {
return {
allowed_mentions: { parse: [] },
flags: 1 << 15,
components: [
{
type: 17,
components: [
{
type: 9,
components: [
{
type: 10,
content:
type === "submit"
? "## :wave: New Submission"
: "## :money_with_wings: Submission Paid",
},
],
accessory: {
type: 2,
style: 5,
label: "Admin",
url: `${process.env.BASE_URL}/rubgram/admin/${sub.id}`,
},
},
{
type: 14,
divider: true,
spacing: 1,
},
{
type: 10,
content: `**Name**: ${sub.name}\n**Service**:\n${sub.service
.map((s) => types.find((t) => t.id === s)?.display)
.filter(Boolean)
.map((s) => `- ${s}`)
.join(
"\n",
)}\n**Server**: ${{ us: "America", eu: "Europe", as: "Asia", tw: "Taiwan" }[sub.server]}\n-# ${type === "submit" ? "**Not** p" : "P"}aid: **${sub.price} ฿** | <@${sub.user}>`,
},
],
accent_color: type === "paid" ? 0x0ef06d : 15714574,
},
],
};
}