Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/api/nostr-projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
import {
getNostrSubmissionsSnapshot,
NOSTR_PROJECTS_TAG,
NOSTR_SUBMISSIONS_TAG,
} from "@/lib/nostrCache";

Expand All @@ -11,6 +12,12 @@ export async function GET() {
}

export async function POST() {
revalidateTag(NOSTR_SUBMISSIONS_TAG, "max");
return NextResponse.json({ ok: true, revalidated: NOSTR_SUBMISSIONS_TAG });
revalidateTag(NOSTR_PROJECTS_TAG, { expire: 0 });
revalidateTag(NOSTR_SUBMISSIONS_TAG, { expire: 0 });
const snapshot = await getNostrSubmissionsSnapshot();
return NextResponse.json({
ok: true,
revalidated: [NOSTR_PROJECTS_TAG, NOSTR_SUBMISSIONS_TAG],
snapshot,
});
}
11 changes: 11 additions & 0 deletions app/api/nostr/badges/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { getCachedBadgesSnapshot } from "@/lib/nostrBadgesCache";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const pubkey = (searchParams.get("pubkey") ?? "").trim();
if (!/^[0-9a-f]{64}$/i.test(pubkey)) {
return NextResponse.json({ error: "Falta pubkey." }, { status: 400 });
}
return NextResponse.json(await getCachedBadgesSnapshot(pubkey));
}
26 changes: 26 additions & 0 deletions app/api/nostr/profiles/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { getCachedNostrProfile } from "@/lib/nostrProfileCache";

function pubkeysFromRequest(req: NextRequest) {
const { searchParams } = new URL(req.url);
const raw = searchParams.get("pubkeys") ?? searchParams.get("pubkey") ?? "";
return raw
.split(",")
.map((p) => p.trim())
.filter((p) => /^[0-9a-f]{64}$/i.test(p))
.slice(0, 50);
}

export async function GET(req: NextRequest) {
const pubkeys = pubkeysFromRequest(req);
if (pubkeys.length === 0) {
return NextResponse.json({ error: "Falta pubkey." }, { status: 400 });
}
const entries = await Promise.all(
pubkeys.map(async (pubkey) => [pubkey, await getCachedNostrProfile(pubkey)]),
);
return NextResponse.json({
profiles: Object.fromEntries(entries),
generatedAt: new Date().toISOString(),
});
}
18 changes: 18 additions & 0 deletions app/api/nostr/projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
import { getNostrSubmissionsSnapshot } from "@/lib/nostrCache";
import { projectMatchesIdentifier } from "@/lib/projectIdentity";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const hackathonId = searchParams.get("hackathonId");
const projectId = searchParams.get("projectId");
const author = searchParams.get("author");
const snapshot = await getNostrSubmissionsSnapshot();
const projects = snapshot.projects.filter((project) => {
if (hackathonId && project.hackathon !== hackathonId) return false;
if (author && project.author !== author) return false;
if (projectId && !projectMatchesIdentifier(project, projectId)) return false;
return true;
});
return NextResponse.json({ ...snapshot, projects });
}
104 changes: 104 additions & 0 deletions app/api/nostr/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
import {
getFreshNostrSubmissionsSnapshot,
getNostrSubmissionsSnapshot,
} from "@/lib/nostrCache";
import {
NOSTR_LEGACY_SUBMISSIONS_TAG,
NOSTR_PROJECTS_TAG,
nostrBadgesTag,
nostrProfileTag,
nostrRelayListTag,
nostrReportsTag,
} from "@/lib/nostrCacheTags";
import { projectMatchesIdentifier } from "@/lib/projectIdentity";

type RefreshScope =
| "projects"
| "profile"
| "relay-list"
| "badges"
| "reports"
| "results";

type RefreshBody = {
scopes?: RefreshScope[];
hackathonId?: string;
projectId?: string;
author?: string;
pubkey?: string;
candidateEventId?: string;
candidateCreatedAt?: number;
blocking?: boolean;
};

function expireTag(tag: string) {
revalidateTag(tag, { expire: 0 });
}

export async function POST(req: NextRequest) {
let body: RefreshBody;
try {
body = (await req.json()) as RefreshBody;
} catch {
return NextResponse.json({ error: "JSON invalido." }, { status: 400 });
}

const scopes = Array.isArray(body.scopes) && body.scopes.length > 0
? body.scopes
: (["projects"] satisfies RefreshScope[]);
const refreshed: Record<string, unknown> = {};
const expiredTags: string[] = [];

const expire = (tag: string) => {
expireTag(tag);
expiredTags.push(tag);
};

if (scopes.includes("projects")) {
expire(NOSTR_PROJECTS_TAG);
expire(NOSTR_LEGACY_SUBMISSIONS_TAG);
const snapshot = body.blocking === false
? await getFreshNostrSubmissionsSnapshot()
: await getNostrSubmissionsSnapshot();
refreshed.projects = {
...snapshot,
projects: snapshot.projects.filter((project) => {
if (body.hackathonId && project.hackathon !== body.hackathonId) {
return false;
}
if (body.author && project.author !== body.author) return false;
if (
body.projectId &&
!projectMatchesIdentifier(project, body.projectId)
) {
return false;
}
return true;
}),
};
}

if (body.pubkey) {
if (scopes.includes("profile")) expire(nostrProfileTag(body.pubkey));
if (scopes.includes("relay-list")) expire(nostrRelayListTag(body.pubkey));
if (scopes.includes("badges")) expire(nostrBadgesTag(body.pubkey));
}

if (body.hackathonId) {
if (scopes.includes("reports") || scopes.includes("results")) {
expire(nostrReportsTag(body.hackathonId));
}
}

return NextResponse.json({
ok: true,
expiredTags,
refreshed,
candidate: {
eventId: body.candidateEventId ?? null,
createdAt: body.candidateCreatedAt ?? null,
},
});
}
26 changes: 26 additions & 0 deletions app/api/nostr/relay-lists/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { getCachedRelayList } from "@/lib/nostrRelayListCache";

function pubkeysFromRequest(req: NextRequest) {
const { searchParams } = new URL(req.url);
const raw = searchParams.get("pubkeys") ?? searchParams.get("pubkey") ?? "";
return raw
.split(",")
.map((p) => p.trim())
.filter((p) => /^[0-9a-f]{64}$/i.test(p))
.slice(0, 50);
}

export async function GET(req: NextRequest) {
const pubkeys = pubkeysFromRequest(req);
if (pubkeys.length === 0) {
return NextResponse.json({ error: "Falta pubkey." }, { status: 400 });
}
const entries = await Promise.all(
pubkeys.map(async (pubkey) => [pubkey, await getCachedRelayList(pubkey)]),
);
return NextResponse.json({
relayLists: Object.fromEntries(entries),
generatedAt: new Date().toISOString(),
});
}
22 changes: 22 additions & 0 deletions app/api/nostr/reports/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getCachedHackathonReportsSnapshot } from "@/lib/nostrReportsCache";

export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const hackathonId = (searchParams.get("hackathonId") ?? "").trim();
const projectId = (searchParams.get("projectId") ?? "").trim();
if (!hackathonId) {
return NextResponse.json({ error: "Falta hackathonId." }, { status: 400 });
}
const snapshot = await getCachedHackathonReportsSnapshot(hackathonId);
if (projectId) {
return NextResponse.json({
hackathonId,
projectId,
report: snapshot.reports[projectId] ?? null,
generatedAt: snapshot.generatedAt,
relays: snapshot.relays,
});
}
return NextResponse.json(snapshot);
}
15 changes: 10 additions & 5 deletions app/api/revalidate-nostr/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { revalidateTag } from "next/cache";
import { type NextRequest, NextResponse } from "next/server";
import {
NOSTR_LEGACY_SUBMISSIONS_TAG,
NOSTR_PROJECTS_TAG,
} from "@/lib/nostrCacheTags";

/**
* Secret-gated revalidation hook for Nostr-sourced cached data.
Expand All @@ -9,9 +13,7 @@ import { type NextRequest, NextResponse } from "next/server";
* -H "x-revalidate-secret: $REVALIDATE_SECRET" \
* -d '{"tag":"nostr:hackathon-submissions"}'
*
* Defaults to the global submissions tag when no body is provided.
* Stale-while-revalidate ('max') keeps perceived speed while a fresh
* relay round-trip completes in the background.
* Defaults to the global projects tag when no body is provided.
*/
export async function POST(req: NextRequest) {
const secret = req.headers.get("x-revalidate-secret");
Expand All @@ -20,7 +22,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
}

let tag = "nostr:hackathon-submissions";
let tag = NOSTR_PROJECTS_TAG;
try {
const body = (await req.json()) as { tag?: unknown };
if (typeof body.tag === "string" && body.tag.length > 0) {
Expand All @@ -30,6 +32,9 @@ export async function POST(req: NextRequest) {
/* no body, use default */
}

revalidateTag(tag, "max");
revalidateTag(tag, { expire: 0 });
if (tag === NOSTR_PROJECTS_TAG) {
revalidateTag(NOSTR_LEGACY_SUBMISSIONS_TAG, { expire: 0 });
}
return NextResponse.json({ ok: true, tag });
}
Loading