Files
dragonsstash/src/app/(app)/kickstarters/actions.ts
xCyanGrizzly 718007446f
All checks were successful
continuous-integration/drone/push Build is passing
feat: fix multi-part archive forwarding and add kickstarter package linking
Multi-part send fix:
- Add destMessageIds BigInt[] to Package schema with backfill migration
- Worker uploadToChannel now returns all message IDs, stored in DB
- Bot forwards all parts of multi-part archives (not just the first)
- Add retry logic for upload rate limits (429) and download stalls

Kickstarter package linking:
- Add package search/linking queries and API routes
- Add PackageLinkerDialog with search + checkbox selection
- Add "Link Packages" and "Send All" actions to kickstarter table
- Add sendAllKickstarterPackages server action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:11:35 +01:00

229 lines
6.7 KiB
TypeScript

"use server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { kickstarterSchema, kickstarterHostSchema } from "@/schemas/kickstarter.schema";
import { revalidatePath } from "next/cache";
import type { ActionResult } from "@/types/api.types";
const REVALIDATE_PATH = "/kickstarters";
export async function createKickstarter(
input: unknown
): Promise<ActionResult<{ id: string }>> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const parsed = kickstarterSchema.safeParse(input);
if (!parsed.success) return { success: false, error: "Validation failed" };
try {
const ks = await prisma.kickstarter.create({
data: {
name: parsed.data.name,
link: parsed.data.link || null,
filesUrl: parsed.data.filesUrl || null,
deliveryStatus: parsed.data.deliveryStatus,
paymentStatus: parsed.data.paymentStatus,
hostId: parsed.data.hostId || null,
notes: parsed.data.notes || null,
userId: session.user.id,
},
});
revalidatePath(REVALIDATE_PATH);
return { success: true, data: { id: ks.id } };
} catch {
return { success: false, error: "Failed to create kickstarter" };
}
}
export async function updateKickstarter(
id: string,
input: unknown
): Promise<ActionResult> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const parsed = kickstarterSchema.safeParse(input);
if (!parsed.success) return { success: false, error: "Validation failed" };
const existing = await prisma.kickstarter.findFirst({
where: { id, userId: session.user.id },
});
if (!existing) return { success: false, error: "Not found" };
try {
await prisma.kickstarter.update({
where: { id },
data: {
name: parsed.data.name,
link: parsed.data.link || null,
filesUrl: parsed.data.filesUrl || null,
deliveryStatus: parsed.data.deliveryStatus,
paymentStatus: parsed.data.paymentStatus,
hostId: parsed.data.hostId || null,
notes: parsed.data.notes || null,
},
});
revalidatePath(REVALIDATE_PATH);
return { success: true, data: undefined };
} catch {
return { success: false, error: "Failed to update kickstarter" };
}
}
export async function deleteKickstarter(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const existing = await prisma.kickstarter.findFirst({
where: { id, userId: session.user.id },
});
if (!existing) return { success: false, error: "Not found" };
try {
await prisma.kickstarter.delete({ where: { id } });
revalidatePath(REVALIDATE_PATH);
return { success: true, data: undefined };
} catch {
return { success: false, error: "Failed to delete kickstarter" };
}
}
export async function createHost(
input: unknown
): Promise<ActionResult<{ id: string; name: string }>> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const parsed = kickstarterHostSchema.safeParse(input);
if (!parsed.success) return { success: false, error: "Validation failed" };
try {
const host = await prisma.kickstarterHost.create({
data: { name: parsed.data.name },
});
revalidatePath(REVALIDATE_PATH);
return { success: true, data: { id: host.id, name: host.name } };
} catch (err: unknown) {
if (
err instanceof Error &&
err.message.includes("Unique constraint")
) {
return { success: false, error: "A host with that name already exists" };
}
return { success: false, error: "Failed to create host" };
}
}
export async function linkPackages(
kickstarterId: string,
packageIds: string[]
): Promise<ActionResult> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const existing = await prisma.kickstarter.findFirst({
where: { id: kickstarterId, userId: session.user.id },
});
if (!existing) return { success: false, error: "Not found" };
try {
// Replace all linked packages
await prisma.$transaction([
prisma.kickstarterPackage.deleteMany({
where: { kickstarterId },
}),
...packageIds.map((packageId) =>
prisma.kickstarterPackage.create({
data: { kickstarterId, packageId },
})
),
]);
revalidatePath(REVALIDATE_PATH);
return { success: true, data: undefined };
} catch {
return { success: false, error: "Failed to link packages" };
}
}
export async function sendAllKickstarterPackages(
kickstarterId: string
): Promise<ActionResult<{ queued: number }>> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
try {
const telegramLink = await prisma.telegramLink.findUnique({
where: { userId: session.user.id },
});
if (!telegramLink) {
return { success: false, error: "No linked Telegram account. Link one in Settings." };
}
const kickstarter = await prisma.kickstarter.findFirst({
where: { id: kickstarterId, userId: session.user.id },
select: {
packages: {
select: {
package: {
select: { id: true, destChannelId: true, destMessageId: true, fileName: true },
},
},
},
},
});
if (!kickstarter) {
return { success: false, error: "Kickstarter not found" };
}
const sendablePackages = kickstarter.packages
.map((lnk) => lnk.package)
.filter((p) => p.destChannelId && p.destMessageId);
if (sendablePackages.length === 0) {
return { success: false, error: "No linked packages are available for sending" };
}
let queued = 0;
for (const pkg of sendablePackages) {
const existing = await prisma.botSendRequest.findFirst({
where: {
packageId: pkg.id,
telegramLinkId: telegramLink.id,
status: { in: ["PENDING", "SENDING"] },
},
});
if (!existing) {
const sendRequest = await prisma.botSendRequest.create({
data: {
packageId: pkg.id,
telegramLinkId: telegramLink.id,
requestedByUserId: session.user.id,
status: "PENDING",
},
});
try {
await prisma.$queryRawUnsafe(
`SELECT pg_notify('bot_send', $1)`,
sendRequest.id
);
} catch {
// Best-effort
}
queued++;
}
}
revalidatePath(REVALIDATE_PATH);
return { success: true, data: { queued } };
} catch {
return { success: false, error: "Failed to send packages" };
}
}