644 lines
17 KiB
TypeScript
Executable File
644 lines
17 KiB
TypeScript
Executable File
"use server";
|
|
|
|
import {
|
|
and,
|
|
desc,
|
|
eq,
|
|
gt,
|
|
inArray,
|
|
lt,
|
|
not,
|
|
sql,
|
|
count as sqlCount,
|
|
} 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,
|
|
settings,
|
|
} from "@/lib/db/schema";
|
|
import { sse } from "@/lib/db/sse-endpoints";
|
|
import { checkSlip } from "@/lib/payment";
|
|
import { getPostHogClient } from "@/lib/posthog-server";
|
|
import {
|
|
addSubmissionNote,
|
|
calculatePrice,
|
|
cancelSubmission,
|
|
compactExpiredSubmissions,
|
|
deleteSubmissionNote,
|
|
isDuplicateSlipError,
|
|
paySubmission,
|
|
type RubgramActor,
|
|
registerSubmission,
|
|
requireAdmin,
|
|
requireOwner,
|
|
runAdminOperation,
|
|
} from "./service";
|
|
|
|
const { DISCORD_WEBHOOK_URL } = process.env as Record<string, string>;
|
|
|
|
export async function wipe() {
|
|
return runAdminOperation(getAdminActor(), async () => {
|
|
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() {
|
|
await requireAdmin(getAdminActor());
|
|
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) {
|
|
await requireAdmin(getAdminActor());
|
|
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() {
|
|
await requireAdmin(getAdminActor());
|
|
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) {
|
|
await requireAdmin(getAdminActor());
|
|
|
|
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) {
|
|
await requireAdmin(getAdminActor());
|
|
|
|
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) {
|
|
await requireAdmin(getAdminActor());
|
|
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) {
|
|
await requireOwner(await getRubgramActor(), uid);
|
|
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 session = await getDiscordSession();
|
|
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 = session?.uid;
|
|
const queue = await registerSubmission(
|
|
{ name, server, service, user },
|
|
{
|
|
getConfig: getEndgameConfig,
|
|
removeExpired: removeExpiredSubmissions,
|
|
findDuplicate: async (userId) => {
|
|
const [existing] = await db
|
|
.select({
|
|
queue: endgameSubmissions.queue,
|
|
id: endgameSubmissions.id,
|
|
})
|
|
.from(endgameSubmissions)
|
|
.where(
|
|
and(
|
|
eq(endgameSubmissions.user, userId),
|
|
not(endgameSubmissions.deleted),
|
|
not(endgameSubmissions.checked),
|
|
not(endgameSubmissions.paid),
|
|
),
|
|
)
|
|
.limit(1);
|
|
return existing;
|
|
},
|
|
insert: async (values) => {
|
|
const [inserted] = await db
|
|
.insert(endgameSubmissions)
|
|
.values(values)
|
|
.returning({
|
|
queue: endgameSubmissions.queue,
|
|
id: endgameSubmissions.id,
|
|
});
|
|
return inserted;
|
|
},
|
|
consumeFreeSlot: async () => {
|
|
await db
|
|
.update(endgameSettings)
|
|
.set({ free: sql`${endgameSettings.free} - 1` })
|
|
.where(gt(endgameSettings.free, 0));
|
|
},
|
|
},
|
|
);
|
|
if (typeof queue === "string") return queue;
|
|
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 paySubmission(
|
|
{ id: sid, slip: buffer, type: slip.type },
|
|
await getRubgramActor(),
|
|
{
|
|
calculatePrice: calcPrice,
|
|
checkSlip,
|
|
isDuplicateSlipError,
|
|
transaction: (run) =>
|
|
db.transaction(async (tx) =>
|
|
run({
|
|
findSubmission: async (id) => {
|
|
const [submission] = await tx
|
|
.select()
|
|
.from(endgameSubmissions)
|
|
.where(
|
|
and(
|
|
eq(endgameSubmissions.id, id),
|
|
not(endgameSubmissions.deleted),
|
|
),
|
|
)
|
|
.limit(1);
|
|
return submission;
|
|
},
|
|
findExpired: async (id) => {
|
|
const [expired] = await tx
|
|
.select()
|
|
.from(endgameExpired)
|
|
.where(eq(endgameExpired.id, id))
|
|
.limit(1);
|
|
return expired;
|
|
},
|
|
restoreExpired: async (expired, price) => {
|
|
await tx.insert(endgameSubmissions).values({ ...expired, price });
|
|
},
|
|
insertSlip: async (input) => {
|
|
const [{ id }] = await tx
|
|
.insert(endgameSlips)
|
|
.values({
|
|
slip: input.slip,
|
|
ref: input.ref,
|
|
amount: String(input.amount ?? 0),
|
|
data: input.data as SlipokResponse,
|
|
})
|
|
.returning({ id: endgameSlips.id });
|
|
return id;
|
|
},
|
|
markPaid: async (submissionId, slipId) => {
|
|
const [paid] = await tx
|
|
.update(endgameSubmissions)
|
|
.set({ slip: slipId })
|
|
.returning({
|
|
queue: endgameSubmissions.queue,
|
|
id: endgameSubmissions.id,
|
|
})
|
|
.where(eq(endgameSubmissions.id, submissionId));
|
|
return paid;
|
|
},
|
|
}),
|
|
),
|
|
},
|
|
);
|
|
revalidatePath("/rubgram/admin");
|
|
if (typeof queue !== "string") {
|
|
sse.rubgram.pub("update", { type: "paid", sub: queue.id });
|
|
getPostHogClient().capture({
|
|
distinctId: sid,
|
|
event: "rubgram_payment_completed",
|
|
properties: {
|
|
submission_id: queue.id,
|
|
queue_position: queue.queue,
|
|
},
|
|
});
|
|
}
|
|
return 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]);
|
|
}
|
|
|
|
async function getRubgramActor(): Promise<RubgramActor> {
|
|
const session = await getDiscordSession();
|
|
return {
|
|
userId: session?.uid,
|
|
isAdmin: async () => Boolean(await adminCheck()),
|
|
};
|
|
}
|
|
|
|
function getAdminActor(): RubgramActor {
|
|
return { isAdmin: async () => Boolean(await adminCheck()) };
|
|
}
|
|
|
|
export async function calcPrice(service: string[]) {
|
|
return calculatePrice(service, await getEndgameConfig());
|
|
}
|
|
|
|
export async function removeExpiredSubmissions() {
|
|
return db.transaction(async (tx) =>
|
|
compactExpiredSubmissions({
|
|
findExpired: async (now) =>
|
|
tx
|
|
.select()
|
|
.from(endgameSubmissions)
|
|
.where(
|
|
and(
|
|
not(endgameSubmissions.paid),
|
|
lt(endgameSubmissions.expires, now),
|
|
),
|
|
)
|
|
.orderBy(desc(endgameSubmissions.queue)),
|
|
archive: async (submissions) => {
|
|
if (submissions.length)
|
|
await tx.insert(endgameExpired).values(submissions);
|
|
},
|
|
remove: async (ids) => {
|
|
if (ids.length)
|
|
await tx
|
|
.delete(endgameSubmissions)
|
|
.where(inArray(endgameSubmissions.id, ids));
|
|
},
|
|
shiftAfter: async (queue) => {
|
|
await tx
|
|
.update(endgameSubmissions)
|
|
.set({ queue: sql`${endgameSubmissions.queue} - 1` })
|
|
.where(gt(endgameSubmissions.queue, queue));
|
|
},
|
|
getMaxQueue: async () => {
|
|
const [result] = await tx
|
|
.select({ max: sql<number>`MAX(${endgameSubmissions.queue})` })
|
|
.from(endgameSubmissions);
|
|
return result?.max ?? null;
|
|
},
|
|
setNextQueue: async (next) => {
|
|
await tx.execute(
|
|
sql`SELECT setval('endgame.submissions_queue_seq', ${next}, false)`,
|
|
);
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function cancel(sid: string) {
|
|
const cancelled = await cancelSubmission(sid, await getRubgramActor(), {
|
|
findSubmission: async (id) => {
|
|
const [submission] = await db
|
|
.select({ user: endgameSubmissions.user })
|
|
.from(endgameSubmissions)
|
|
.where(
|
|
and(eq(endgameSubmissions.id, id), not(endgameSubmissions.deleted)),
|
|
)
|
|
.limit(1);
|
|
return submission;
|
|
},
|
|
cancel: async (id) => {
|
|
await db
|
|
.update(endgameSubmissions)
|
|
.set({ deleted: true })
|
|
.where(eq(endgameSubmissions.id, id));
|
|
},
|
|
});
|
|
if (!cancelled) return;
|
|
|
|
sse.rubgram.pub("update", { type: "cancel" });
|
|
|
|
revalidatePath("/rubgram");
|
|
}
|
|
|
|
export async function bulkDelete(ids: string[]) {
|
|
return runAdminOperation(getAdminActor(), async () => {
|
|
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) {
|
|
const note = await addSubmissionNote(sid, text, getAdminActor(), {
|
|
getNotes: async (id) => {
|
|
const [submission] = await db
|
|
.select({ notes: endgameSubmissions.notes })
|
|
.from(endgameSubmissions)
|
|
.where(eq(endgameSubmissions.id, id))
|
|
.limit(1);
|
|
return submission?.notes ?? [];
|
|
},
|
|
saveNotes: async (id, notes) => {
|
|
await db
|
|
.update(endgameSubmissions)
|
|
.set({ notes })
|
|
.where(eq(endgameSubmissions.id, id));
|
|
},
|
|
createId: Bun.randomUUIDv7,
|
|
now: () => new Date(),
|
|
});
|
|
|
|
revalidatePath(`/rubgram/admin/${sid}`);
|
|
|
|
return note;
|
|
}
|
|
|
|
export async function deleteNote(sid: string, noteId: string) {
|
|
await deleteSubmissionNote(sid, noteId, getAdminActor(), {
|
|
getNotes: async (id) => {
|
|
const [submission] = await db
|
|
.select({ notes: endgameSubmissions.notes })
|
|
.from(endgameSubmissions)
|
|
.where(eq(endgameSubmissions.id, id))
|
|
.limit(1);
|
|
return submission?.notes ?? [];
|
|
},
|
|
saveNotes: async (id, notes) => {
|
|
await db
|
|
.update(endgameSubmissions)
|
|
.set({ notes })
|
|
.where(eq(endgameSubmissions.id, id));
|
|
},
|
|
});
|
|
|
|
revalidatePath(`/rubgram/admin/${sid}`);
|
|
}
|
|
|
|
export async function discordCall(id: string) {
|
|
await requireAdmin(getAdminActor());
|
|
|
|
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) {
|
|
await requireAdmin(getAdminActor());
|
|
|
|
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;
|
|
};
|