Post stream fix & add debug menu for rubgram

This commit is contained in:
Dreamgineer
2025-12-12 09:21:26 +07:00 Unverified
parent 7393bd1db5
commit 42ee896468
7 changed files with 131 additions and 37 deletions
+37 -3
View File
@@ -4,14 +4,15 @@ import {
Check,
Copy,
CopyCheck,
ImageOff,
Loader2,
MessageSquareWarning,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
import { discordCall } from "../../api";
import { debugUploadSlip, discordCall } from "../../api";
export function EnkaBrowser({ uid }: { uid: string }) {
const [id, setId] = useState(uid);
@@ -47,7 +48,12 @@ export function CopyButton({ text }: { text: string }) {
setTimeout(() => setCopied(false), 2000);
}
return (
<Button variant="link" onClick={copy} disabled={copied} className="text-white">
<Button
variant="link"
onClick={copy}
disabled={copied}
className="text-white"
>
{copied ? <CopyCheck /> : <Copy />} คัดลอก username
</Button>
);
@@ -83,3 +89,31 @@ export function CallButton({ user }: { user: string }) {
</Button>
);
}
export function DebugSlipUpload({ sid }: { sid: string }) {
const ref = useRef<HTMLInputElement | null>(null);
return (
<>
<button
className="flex flex-col justify-center items-center border rounded-md w-40 bg-card cursor-pointer"
onClick={() => ref.current?.click()}
type="button"
>
<ImageOff />
ยังไม่จ่าย
<span className="text-xs">กดเพื่ออัพสลิป</span>
</button>
<input
ref={ref}
hidden
type="file"
accept="image/*"
onChange={(ev) => {
if (ev.target.files?.length)
debugUploadSlip(sid, ev.target.files.item(0)!);
}}
/>
</>
);
}
+8 -18
View File
@@ -2,8 +2,7 @@ import { eq } from "drizzle-orm";
import {
BadgeDollarSign,
Download,
ExternalLink,
ImageOff,
ExternalLink
} from "lucide-react";
import Image from "next/image";
import { notFound } from "next/navigation";
@@ -29,7 +28,7 @@ import {
endgameSubmissions,
endgameTypes,
} from "@/lib/db/schema";
import { CallButton, CopyButton } from "./client";
import { CallButton, CopyButton, DebugSlipUpload } from "./client";
export default async function AdminSubmissionView({
params,
@@ -50,9 +49,7 @@ export default async function AdminSubmissionView({
.limit(1);
const types = await db.select().from(endgameTypes);
const typesMap = Object.fromEntries(
types.map((v) => [v.id, v.display]),
);
const typesMap = Object.fromEntries(types.map((v) => [v.id, v.display]));
return (
<div className="p-2 h-full">
@@ -126,20 +123,13 @@ export default async function AdminSubmissionView({
</DialogFooter>
</DialogContent>
</Dialog>
) : (
) : sub.price <= 0 ? (
<div className="flex flex-col justify-center items-center border rounded-md w-40 bg-card">
{sub.price <= 0 ? (
<>
<BadgeDollarSign />
ฟรี
</>
) : (
<>
<ImageOff />
ยังไม่จ่าย
</>
)}
<BadgeDollarSign />
ฟรี
</div>
) : (
<DebugSlipUpload sid={sub.id} />
)}
</div>
<div className="border rounded-md h-full overflow-hidden"></div>
+20 -3
View File
@@ -34,6 +34,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { comms } from "@/lib/comms";
import { cn } from "@/lib/utils";
import { setFree, setLimit, toggleCheck, toggleLock } from "../api";
@@ -234,9 +235,17 @@ export function LimitManager({
export function SubmissionList({
subs,
}: {
subs: { id: string; name: string; checked: boolean; queue: number }[];
subs: {
id: string;
name: string;
checked: boolean;
queue: number;
paid: boolean;
}[];
}) {
const [query, setQuery] = useState("");
const [debug] = comms.var("debug");
return (
<>
<Input
@@ -245,9 +254,17 @@ export function SubmissionList({
onChange={(ev) => setQuery(ev.target.value.toLowerCase())}
/>
{subs
.filter((s) => (s.queue + s.name).toLowerCase().includes(query))
.filter(
(s) =>
(debug || s.paid) &&
(s.queue + s.name).toLowerCase().includes(query),
)
.map((s) => (
<SidebarMenuButton key={s.id} asChild>
<SidebarMenuButton
key={s.id}
asChild
className={cn(debug && !s.paid && "opacity-50")}
>
<SidebarLink submission={s} />
</SidebarMenuButton>
))}
+9 -5
View File
@@ -1,4 +1,4 @@
import { eq, isNotNull, or } from "drizzle-orm";
import { eq, isNotNull, or, type SQL } from "drizzle-orm";
import { Dice3, Trash2 } from "lucide-react";
import { redirect } from "next/navigation";
import {
@@ -42,11 +42,12 @@ export default async function AdminLayout({
name: endgameSubmissions.name,
checked: endgameSubmissions.checked,
queue: endgameSubmissions.queue,
paid: or(
eq(endgameSubmissions.price, 0),
isNotNull(endgameSubmissions.slip),
) as SQL<boolean>,
})
.from(endgameSubmissions)
.where(
or(eq(endgameSubmissions.price, 0), isNotNull(endgameSubmissions.slip)),
)
.orderBy(endgameSubmissions.id);
const config = await getEndgameConfig();
return (
@@ -101,7 +102,10 @@ export default async function AdminLayout({
<SidebarFooter>
<SidebarGroup>
<SidebarGroupLabel>การตั้งค่า</SidebarGroupLabel>
<LimitManager config={config} length={subs.length} />
<LimitManager
config={config}
length={subs.reduce((c, s) => c + (s.paid ? 1 : 0), 0)}
/>
</SidebarGroup>
</SidebarFooter>
</Sidebar>
+45 -4
View File
@@ -31,8 +31,13 @@ import {
} from "@/lib/db/schema";
import type { TypedFormData } from "./type";
const { SLIPOK_API_URL, SLIPOK_API_KEY, DISCORD_WEBHOOK_URL } =
process.env as Record<string, string>;
const {
SLIPOK_API_URL,
SLIPOK_API_KEY,
DISCORD_WEBHOOK_URL,
DISCORD_CLIENT_ID,
BASE_URL,
} = process.env as Record<string, string>;
export async function wipe() {
if (!(await adminCheck())) throw "Unauthorized";
@@ -325,7 +330,9 @@ export async function getDiscordSession() {
}
export async function loginDiscord() {
redirect(process.env.DISCORD_OAUTH_URL!);
redirect(
`https://discord.com/oauth2/authorize?client_id=${encodeURIComponent(DISCORD_CLIENT_ID)}&response_type=code&redirect_uri=${encodeURIComponent(new URL("/rubgram/callback", BASE_URL).href)}&scope=identify+guilds.join+guilds`,
);
}
export async function calcPrice(service: string[]) {
@@ -397,6 +404,8 @@ export async function cancel(sid: string) {
await removeExpiredSubmissions();
ps.publish({}, { topic: "rubgram", event: "update" });
revalidatePath("/rubgram");
}
@@ -404,7 +413,8 @@ export async function discordCall(id: string) {
return await fetch(DISCORD_WEBHOOK_URL, {
method: "POST",
body: JSON.stringify({
content: `<@${id}> ถึงคิวแล้ว ทักหาบุสได้เลย`,
content: `<@${id}> ถึงคิวแล้ว ทักหาบุส <@681741453382123521> ได้เลย`,
allowed_mentions: { users: [id] },
}),
headers: {
"Content-Type": "application/json",
@@ -415,6 +425,37 @@ export async function discordCall(id: string) {
});
}
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));
});
ps.publish({}, { topic: "rubgram", event: "update" });
revalidatePath("/rubgram/admin");
}
export type SlipokResponse =
| {
success: true;
+7 -3
View File
@@ -220,8 +220,12 @@ export function CancelButton({ sid }: { sid: string }) {
}
export function ClearCookie() {
// biome-ignore lint/suspicious/noTsIgnore: typescript issue
// @ts-ignore
cookieStore.delete("rsid");
try {
// biome-ignore lint/suspicious/noTsIgnore: typescript issue
// @ts-ignore
cookieStore.delete("rsid");
} catch {
console.warn("Cannot clear rubgram cookie");
}
return "";
}
+5 -1
View File
@@ -123,7 +123,7 @@ export default async function EndgamePage() {
return (
<PageTransition>
<div className="flex flex-col justify-around items-center h-svh">
<div className="flex flex-col justify-center gap-2 items-center h-svh">
<Card className="w-full max-w-md">
{q ? (
!q.paid ? (
@@ -330,6 +330,10 @@ export default async function EndgamePage() {
</div>
</CardFooter>
</Card>
<span className="backdrop-blur-md p-1 text-xs m-1 border border-dashed rounded-sm">
หากติดปัญหา โปรดแจ้งผ่านทาง
<a href="https://discord.gg/HQwDXNhxuK" className="underline">ช่องดิสคอร์ด</a>
</span>
{sid && !q && <ClearCookie />}
</div>
</PageTransition>