import { Hono } from "hono"; import { z } from "zod"; import { getDb } from "@exosphere/core/db"; import { eq, and, sql } from "@exosphere/core/db/drizzle"; import { requireAuth, type AuthEnv } from "@exosphere/core/auth"; import { getActiveMemberRole } from "@exosphere/core/sphere"; import { requirePermission, checkPermission } from "@exosphere/core/permissions"; import { putPdsRecord, generateRkey, tidToDate } from "@exosphere/core/pds"; import { resolveDidHandles } from "@exosphere/core/identity"; import type { SphereEnv } from "@exosphere/core/types"; import { featureRequests, featureRequestStatuses } from "../db/schema.ts"; import { updateStatusSchema, markAsDuplicateSchema } from "../schemas/feature-request.ts"; import { insertStatusAndUpdateFR } from "../db/operations.ts"; const STATUS_COLLECTION = "site.exosphere.featureRequest.status"; const app = new Hono(); // ---- Duplicates ---- // Mark a feature request as duplicate of another app.post("/:id/duplicate", requireAuth, async (c) => { const id = c.req.param("id"); const body = await c.req.json(); const result = markAsDuplicateSchema.safeParse(body); if (!result.success) { return c.json({ error: z.flattenError(result.error) }, 400); } const { duplicateOfId } = result.data; const db = getDb(); const did = c.var.did; const sphereId = c.var.sphereId; const sphereVisibility = c.var.sphereVisibility; // Check FR exists, belongs to this sphere, is "requested", and not hidden const fr = db .select({ id: featureRequests.id, authorDid: featureRequests.authorDid, status: featureRequests.status, pdsUri: featureRequests.pdsUri, }) .from(featureRequests) .where( and( eq(featureRequests.id, id), eq(featureRequests.sphereId, sphereId), sql`${featureRequests.hiddenAt} is null`, ), ) .get(); if (!fr) { return c.json({ error: "Feature request not found" }, 404); } if (fr.status !== "requested") { return c.json({ error: "Only requested feature requests can be marked as duplicate" }, 400); } // Permission: admin/owner OR author const role = getActiveMemberRole(sphereId, did); const canMark = checkPermission(sphereId, "feature-requests", "markDuplicate", role) || fr.authorDid === did; if (!canMark) { return c.json({ error: "Forbidden" }, 403); } // Check target exists and is not the same FR, not hidden if (duplicateOfId === id) { return c.json({ error: "Cannot mark as duplicate of itself" }, 400); } const target = db .select({ id: featureRequests.id }) .from(featureRequests) .where( and( eq(featureRequests.id, duplicateOfId), eq(featureRequests.sphereId, sphereId), sql`${featureRequests.hiddenAt} is null`, ), ) .get(); if (!target) { return c.json({ error: "Target feature request not found" }, 404); } // Write to PDS for public spheres let pdsUri: string | null = null; if (sphereVisibility === "public" && fr.pdsUri) { const session = c.var.session; pdsUri = await putPdsRecord(session, STATUS_COLLECTION, id, { subject: fr.pdsUri, status: "duplicate", }); } const statusId = generateRkey(); insertStatusAndUpdateFR({ id: statusId, requestId: id, authorDid: did, status: "duplicate", pdsUri, duplicateOfId, }); return c.json({ ok: true }); }); // Get FRs that are duplicates of a given FR app.get("/:id/duplicates", (c) => { const id = c.req.param("id"); const db = getDb(); const rows = db .select({ id: featureRequests.id, number: featureRequests.number, title: featureRequests.title, }) .from(featureRequests) .where(and(eq(featureRequests.duplicateOfId, id), sql`${featureRequests.hiddenAt} is null`)) .all(); return c.json({ duplicates: rows }); }); // ---- Statuses ---- // Admin/owner-only: set status on a feature request app.post( "/:id/status", requireAuth, requirePermission("feature-requests", "changeStatus"), async (c) => { const id = c.req.param("id"); const body = await c.req.json(); const result = updateStatusSchema.safeParse(body); if (!result.success) { return c.json({ error: z.flattenError(result.error) }, 400); } const { status } = result.data; const db = getDb(); const did = c.var.did; const sphereId = c.var.sphereId; const sphereVisibility = c.var.sphereVisibility; const existing = db .select({ id: featureRequests.id, status: featureRequests.status, pdsUri: featureRequests.pdsUri, }) .from(featureRequests) .where(and(eq(featureRequests.id, id), eq(featureRequests.sphereId, sphereId))) .get(); if (!existing) { return c.json({ error: "Feature request not found" }, 404); } let pdsUri: string | null = null; // Write to PDS for public spheres if (sphereVisibility === "public" && existing.pdsUri) { const session = c.var.session; pdsUri = await putPdsRecord(session, STATUS_COLLECTION, id, { subject: existing.pdsUri, status, }); } const statusId = generateRkey(); insertStatusAndUpdateFR({ id: statusId, requestId: id, authorDid: did, status, pdsUri, clearDuplicateOfId: existing.status === "duplicate", }); return c.json({ status }); }, ); // Get status change history for a feature request app.get("/:id/statuses", async (c) => { const id = c.req.param("id"); const db = getDb(); const rows = db .select({ id: featureRequestStatuses.id, authorDid: featureRequestStatuses.authorDid, status: featureRequestStatuses.status, }) .from(featureRequestStatuses) .where(eq(featureRequestStatuses.requestId, id)) .orderBy(sql`${featureRequestStatuses.id} desc`) .all(); const handleMap = await resolveDidHandles(rows.map((r) => r.authorDid)); const statuses = rows.map((r) => ({ ...r, createdAt: tidToDate(r.id), authorHandle: handleMap.get(r.authorDid) ?? null, })); return c.json({ statuses }); }); export { app as statusesApi };