Files
buzz/app/(ui)/rubgram/api.ts
T
dmgnr e3133d0cd3
Build and Push Image / build-backend (push) Successful in 2m54s
Build and Push Image / build-frontend (push) Successful in 4m11s
Build and Push Image / push (push) Successful in 1m1s
fix(rg): redirect url fix
2026-07-25 14:17:23 +07:00

608 lines
17 KiB
TypeScript
Executable File

"use server";
import {
and,
count as sqlCount,
desc,
eq,
gt,
inArray,
lt,
not,
sql,
} from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import {
endgameDiscord,
endgameExpired,
endgameSettings,
endgameSlips,
endgameSubmissions,
endgameTypes,
type Note,
settings,
} from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints";
import { checkSlip } from "@/lib/payment";
import { getPostHogClient } from "@/lib/posthog-server";
const { DISCORD_WEBHOOK_URL, DISCORD_CLIENT_ID, BASE_URL } =
process.env as Record<string, string>;
export async function wipe() {
if (!(await adminCheck())) throw "Unauthorized";
await db.update(endgameSubmissions).set({ deleted: true });
revalidatePath("/rubgram/admin");
revalidatePath("/rubgram");
sse.rubgram.pub("update", { type: "wipe" });
await actionLog(`Deleted rubgram submissions`);
}
export async function random() {
if (!(await adminCheck())) throw "Unauthorized";
const [sub] = await db
.select()
.from(endgameSubmissions)
.where(endgameSubmissions.paid.getSQL())
.orderBy(sql`${endgameSubmissions.checked} ASC, RANDOM()`)
.limit(1);
if (sub) redirect(`/rubgram/admin/${sub.id}`);
else throw "ไม่พบผู้ลงทะเบียนที่ยังไม่ตรวจสอบ";
}
export async function toggleCheck(submissionId: string) {
if (!(await adminCheck())) throw "Unauthorized";
await db
.update(endgameSubmissions)
.set({
checked: not(endgameSubmissions.checked),
})
.where(eq(endgameSubmissions.id, submissionId));
revalidatePath("/rubgram/admin");
revalidatePath("/rubgram");
sse.rubgram.pub("update", { type: "toggleCheck" });
await actionLog(`Toggled an rubgram submission check mark`);
}
export async function toggleLock() {
if (!(await adminCheck())) throw "Unauthorized";
const existing = await db
.update(endgameSettings)
.set({
locked: not(endgameSettings.locked),
})
.returning({ locked: endgameSettings.locked });
if (existing.length === 0)
await db.insert(endgameSettings).values({ locked: true });
revalidatePath("/rubgram/admin");
revalidatePath("/rubgram");
sse.rubgram.pub("update", { type: "toggleLock" });
await actionLog(
`${(existing.length ? existing[0].locked : true) ? "Locked" : "Unlocked"} rubgram submission`,
);
}
export async function setLimit(limit: number) {
if (!(await adminCheck())) throw "Unauthorized";
if (
(
await db
.update(endgameSettings)
.set({
limit,
})
.returning({ id: endgameSettings.id })
).length === 0
)
await db.insert(endgameSettings).values({ limit });
revalidatePath("/rubgram/admin");
revalidatePath("/rubgram");
sse.rubgram.pub("update", { type: "setLimit" });
await actionLog(
`Set rubgram submit limit to ${limit < 0 ? "unlimited" : limit}`,
);
}
export async function setFree(free: number) {
if (!(await adminCheck())) throw "Unauthorized";
if (
(
await db
.update(endgameSettings)
.set({
free,
})
.returning({ id: endgameSettings.id })
).length === 0
)
await db.insert(endgameSettings).values({ free });
revalidatePath("/rubgram/admin");
revalidatePath("/rubgram");
sse.rubgram.pub("update", { type: "setFree" });
await actionLog(`Set rubgram free submission amount to ${free}`);
}
export async function toggleMonth(month: string) {
if (!(await adminCheck())) throw "Unauthorized";
const [s] = await db
.select({ monthly: endgameSettings.monthly })
.from(endgameSettings)
.limit(1);
const monthly = {
...(s?.monthly ?? {}),
[month]: !(s?.monthly?.[month] ?? false),
};
await db
.update(endgameSettings)
.set({ monthly })
.where(eq(endgameSettings.id, true));
revalidatePath("/rubgram/admin/calendar");
}
export async function getEndgameConfig() {
const [ngm]: (typeof endgameSettings.$inferSelect | undefined)[] = await db
.select()
.from(endgameSettings)
.limit(1);
const [glob]: (typeof settings.$inferSelect | undefined)[] = await db
.select()
.from(settings)
.limit(1);
const [{ count }] = await db
.select({ count: sqlCount() })
.from(endgameSubmissions)
.where(not(endgameSubmissions.deleted));
const types = await db
.select()
.from(endgameTypes)
.orderBy(endgameTypes.order);
const full = ngm && ngm.limit !== -1 && count >= (ngm.limit || 0);
return {
// @ts-expect-error
locked: false,
// @ts-expect-error
limit: -1,
// @ts-expect-error
enka: false,
// @ts-expect-error
free: 0,
count,
full,
types,
...ngm,
...glob,
};
}
export async function getUserSubmissions(uid: string) {
const types = await db.select().from(endgameTypes);
const typeNames = Object.fromEntries(types.map((t) => [t.id, t.display]));
const subs = await db
.select({
id: endgameSubmissions.id,
queue: endgameSubmissions.queue,
name: endgameSubmissions.name,
server: endgameSubmissions.server,
price: endgameSubmissions.price,
paid: endgameSubmissions.paid,
checked: endgameSubmissions.checked,
deleted: endgameSubmissions.deleted,
expires: endgameSubmissions.expires,
service: endgameSubmissions.service,
submitDay: endgameSubmissions.submit_day,
})
.from(endgameSubmissions)
.where(
and(eq(endgameSubmissions.user, uid), not(endgameSubmissions.deleted)),
)
.orderBy(desc(endgameSubmissions.submit_day))
.limit(20);
return subs.map((s) => ({
...s,
services: s.service.map((id) => typeNames[id] || id),
}));
}
export async function submitEndgame(formData: FormData) {
const { full, locked, limit, types, free } = await getEndgameConfig();
const name = formData.get("name") as string;
const server = formData.get("server") as "as" | "eu" | "us" | "tw";
const service = formData.getAll("service") as string[];
const user = formData.get("user") as string;
if (!name || !server || !service.length || !user)
return "กรุณากรอกข้อมูลให้ครบถ้วน";
// ไม่ควรเกิดขึ้นแน่ๆ แต่กันไว้ก่อน
if (!["as", "eu", "us", "tw"].includes(server)) return "เซิร์ฟไม่ถูกต้อง";
if (service.some((s) => !types.map((t) => t.id).includes(s)))
return "บริการไม่ถูกต้อง";
if (name.length > 32) return "ชื่อยาวเกินไป ต้องไม่เกิน 32 ตัวอักษร";
if (locked) return "ปิดรับลงทะเบียนชั่วคราว เนื่องจากมีผู้ลงจำนวนมาก";
if (full) return `คิวลงทะเบียนเต็มแล้ว (${limit} ครั้ง)`;
await removeExpiredSubmissions();
const [existing] = await db
.select({ queue: endgameSubmissions.queue, id: endgameSubmissions.id })
.from(endgameSubmissions)
.where(
and(
eq(endgameSubmissions.user, user),
not(endgameSubmissions.deleted),
not(endgameSubmissions.checked),
not(endgameSubmissions.paid),
),
)
.limit(1);
if (existing) return existing;
const [queue] = await db
.insert(endgameSubmissions)
.values({
user,
name,
server,
service,
price: await calcPrice(service),
})
.returning({ queue: endgameSubmissions.queue, id: endgameSubmissions.id });
if (free > 0)
await db.update(endgameSettings).set({
free: sql`${endgameSettings.free} - 1`,
});
revalidatePath("/rubgram/admin");
sse.rubgram.pub("update", { type: "submit", sub: queue.id });
getPostHogClient().capture({
distinctId: user,
event: "rubgram_registration_completed",
properties: { queue_position: queue.queue, server, services: service },
});
return queue;
}
export async function submitEndgamePayment(formData: FormData) {
const sid = formData.get("sid") as string;
const slip = formData.get("slip") as File;
if (!sid || !slip) return "กรุณาอัพโหลดสลิปให้ครบถ้วน";
const arrayBuffer = await slip.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const queue = await db.transaction(async (tx) => {
const [s] = await tx
.select()
.from(endgameSubmissions)
.where(
and(eq(endgameSubmissions.id, sid), not(endgameSubmissions.deleted)),
)
.limit(1);
const [a] = await tx
.select()
.from(endgameExpired)
.where(eq(endgameExpired.id, sid))
.limit(1);
if (!s) {
if (a)
await tx.insert(endgameSubmissions).values({
...a,
price: await calcPrice(a.service),
});
else return "คุณยังไม่ได้ลงทะเบียน";
}
// const processed = {
// success: true,
// data: { transRef: "test-abc", amount: 12345 },
// } as const;
const processed = await checkSlip(buffer, slip.type, (s || a).price);
if (!processed.success) return `${processed.code}: ${processed.message}`;
const [{ id: slipId }] = await tx
.insert(endgameSlips)
.values({
slip: buffer,
ref: processed.data.transRef,
amount: processed.data.amount?.toString(),
data: processed,
})
.returning({ id: endgameSlips.id })
.catch((e) => {
console.log(e);
return [{ id: "conflict" }];
});
if (slipId === "conflict") return "สลิปนี้ถูกใช้ไปแล้ว";
return await tx
.update(endgameSubmissions)
.set({
slip: slipId,
})
.returning({ queue: endgameSubmissions.queue, id: endgameSubmissions.id })
.where(eq(endgameSubmissions.id, sid));
});
revalidatePath("/rubgram/admin");
if (Array.isArray(queue)) {
sse.rubgram.pub("update", { type: "paid", sub: queue[0].id });
getPostHogClient().capture({
distinctId: sid,
event: "rubgram_payment_completed",
properties: {
submission_id: queue[0].id,
queue_position: queue[0].queue,
},
});
}
return Array.isArray(queue) ? queue[0] : queue;
}
// discord auth
export async function getDiscordSession() {
const c = await cookies();
const token = c.get("discord");
if (!token) return undefined;
return await db
.select()
.from(endgameDiscord)
.where(eq(endgameDiscord.token, token.value))
.then((v) => v[0]);
}
export async function loginDiscord() {
redirect(
`https://discord.com/oauth2/authorize?client_id=${encodeURIComponent(DISCORD_CLIENT_ID)}&response_type=code&redirect_uri=${encodeURIComponent(`${BASE_URL}/rubgram/callback`)}&scope=identify+guilds.join+guilds`,
);
}
export async function calcPrice(service: string[]) {
const { free, types, allDiscount } = await getEndgameConfig();
return free > 0
? 0
: service.reduce(
(p, s) => p + (types.find((t) => t.id === s)?.price || 0),
0,
) - (types.every((s) => service.includes(s.id)) ? allDiscount : 0);
}
export async function removeExpiredSubmissions() {
// First, get count of items to be removed
const expiredCond = and(
not(endgameSubmissions.paid),
lt(endgameSubmissions.expires, new Date()),
);
const toRemove = await db
.select({ count: sql<number>`COUNT(*)` })
.from(endgameSubmissions)
.where(expiredCond);
// Delete expired submissions and update queue numbers in one transaction
await db.transaction(async (tx) => {
// Store the queue numbers before deletion
const expiredQueues = await tx
.select()
.from(endgameSubmissions)
.where(expiredCond)
.orderBy(desc(endgameSubmissions.queue));
// Delete expired submissions
if (expiredQueues.length)
await tx.insert(endgameExpired).values(expiredQueues);
await tx.delete(endgameSubmissions).where(expiredCond);
// Update queue numbers for remaining submissions
for (const { queue } of expiredQueues) {
await tx
.update(endgameSubmissions)
.set({
queue: sql`${endgameSubmissions.queue} - 1`,
})
.where(gt(endgameSubmissions.queue, queue));
}
// Reset the serial sequence to the max queue number
const maxQueue = await tx
.select({ max: sql<number>`MAX(${endgameSubmissions.queue})` })
.from(endgameSubmissions);
const nextSerial = (maxQueue[0]?.max || 0) + 1;
// Update the sequence - adjust schema name if needed
await tx.execute(
sql`SELECT setval('endgame.submissions_queue_seq', ${nextSerial}, false)`,
);
});
return {
removed: Number(toRemove[0]?.count || 0),
};
}
export async function cancel(sid: string) {
await db
.update(endgameSubmissions)
.set({ deleted: true })
.where(eq(endgameSubmissions.id, sid));
sse.rubgram.pub("update", { type: "cancel" });
revalidatePath("/rubgram");
}
export async function bulkDelete(ids: string[]) {
if (!(await adminCheck())) throw "Unauthorized";
await db
.update(endgameSubmissions)
.set({ deleted: true })
.where(inArray(endgameSubmissions.id, ids));
revalidatePath("/rubgram/admin");
sse.rubgram.pub("update", { type: "wipe" });
await actionLog(`Bulk deleted ${ids.length} rubgram submissions`);
}
export async function addNote(sid: string, text: string) {
if (!(await adminCheck())) throw "Unauthorized";
const note: Note = {
id: Bun.randomUUIDv7(),
text,
createdAt: new Date().toISOString(),
};
const [sub] = await db
.select({ notes: endgameSubmissions.notes })
.from(endgameSubmissions)
.where(eq(endgameSubmissions.id, sid))
.limit(1);
await db
.update(endgameSubmissions)
.set({ notes: [...(sub?.notes || []), note] })
.where(eq(endgameSubmissions.id, sid));
revalidatePath(`/rubgram/admin/${sid}`);
return note;
}
export async function deleteNote(sid: string, noteId: string) {
if (!(await adminCheck())) throw "Unauthorized";
const [sub] = await db
.select({ notes: endgameSubmissions.notes })
.from(endgameSubmissions)
.where(eq(endgameSubmissions.id, sid))
.limit(1);
await db
.update(endgameSubmissions)
.set({ notes: (sub?.notes || []).filter((n) => n.id !== noteId) })
.where(eq(endgameSubmissions.id, sid));
revalidatePath(`/rubgram/admin/${sid}`);
}
export async function discordCall(id: string) {
if (!(await adminCheck())) throw "Unauthorized";
return await fetch(DISCORD_WEBHOOK_URL, {
method: "POST",
body: JSON.stringify({
content: `<@${id}> ถึงคิวแล้ว ทักหาบุส <@681741453382123521> ได้เลย`,
allowed_mentions: { users: [id] },
}),
headers: {
"Content-Type": "application/json",
},
}).then(async (r) => {
console.log(`called`, await r.text());
return r.ok;
});
}
export async function debugUploadSlip(sid: string, image: File) {
if (!(await adminCheck())) throw "Unauthorized";
const arrayBuffer = await image.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
await db.transaction(async (tx) => {
const [{ price }] = await tx
.select({ price: endgameSubmissions.price })
.from(endgameSubmissions)
.where(eq(endgameSubmissions.id, sid));
const [{ id }] = await tx
.insert(endgameSlips)
.values({
slip: buffer,
ref: `MANUAL-${Bun.randomUUIDv7()}`,
amount: price.toString(),
data: {} as SlipokResponse,
})
.returning({ id: endgameSlips.id });
await tx
.update(endgameSubmissions)
.set({ slip: id })
.where(eq(endgameSubmissions.id, sid));
});
sse.rubgram.pub("update", { type: "uploadSlip" });
revalidatePath("/rubgram/admin");
}
export type SlipokResponse =
| {
success: true;
data: {
success: true;
message: string;
rqUID: string;
language: string;
transRef: string;
sendingBank: string;
receivingBank: string;
transDate: string;
transTime: string;
sender: {
displayName: string;
name: string;
proxy: {
type: null;
value: null;
};
account: {
type: string;
value: string;
};
};
receiver: {
displayName: string;
name: string;
proxy: {
type: string;
value: string;
};
account: {
type: string;
value: string;
};
};
amount: number;
paidLocalAmount: number;
paidLocalCurrency: string;
countryCode: string;
transFeeAmount: number;
ref1: string;
ref2: string;
ref3: string;
toMerchantId: string;
};
}
| {
success: false;
code: number;
message: string;
};