608 lines
17 KiB
TypeScript
Executable File
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;
|
|
};
|