diff --git a/showcase.json b/showcase.json index 3f5f7602..4474a107 100644 --- a/showcase.json +++ b/showcase.json @@ -64,5 +64,7 @@ "jowno", "walt", "adazz", - "ark" + "ark", + "haoz0x", + "hans" ] diff --git a/src/lib/redis.server.ts b/src/lib/redis.server.ts index 9d3348b3..4fb3baa0 100644 --- a/src/lib/redis.server.ts +++ b/src/lib/redis.server.ts @@ -19,6 +19,7 @@ if (REDIS_URL) { export async function cacheGet( key: string | (Record | string | undefined)[], getValueFun: () => Promise, + noUpdate?: boolean, ) { if (redis && redis.status === "ready") { let redisKey: string @@ -31,11 +32,13 @@ export async function cacheGet( } const cacheValue = await redis.get(redisKey) if (cacheValue) { - setTimeout(() => { - getValueFun().then((value) => { - redis.set(redisKey, JSON.stringify(value), "EX", REDIS_EXPIRE) - }) - }, Math.random() * REDIS_REFRESH) + if (!noUpdate) { + setTimeout(() => { + getValueFun().then((value) => { + redis.set(redisKey, JSON.stringify(value), "EX", REDIS_EXPIRE) + }) + }, Math.random() * REDIS_REFRESH) + } return JSON.parse(cacheValue) } else { const value = await getValueFun() @@ -47,3 +50,19 @@ export async function cacheGet( return await getValueFun() } } + +export function cacheDelete( + key: string | (Record | string | undefined)[], +) { + if (redis && redis.status === "ready") { + let redisKey: string + if (Array.isArray(key)) { + redisKey = key + .map((k) => (typeof k === "string" ? k : JSON.stringify(k))) + .join(":") + } else { + redisKey = key + } + redis.del(redisKey) + } +} diff --git a/src/models/page.model.ts b/src/models/page.model.ts index f3a4280b..dca8ed0d 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -304,7 +304,22 @@ export async function getPage( customUnidata?: Unidata, ) { if (!input.site || !(input.page || input.pageId)) { - return undefined + return null + } + + if (!input.pageId) { + const slug2Id = ( + await axios.get("/api/slug2id", { + params: { + handle: input.site, + slug: input.page, + }, + }) + ).data + input.pageId = `${slug2Id.characterId}-${slug2Id.noteId}` + if (!input.pageId) { + return null + } } const local = getLocalPages({ @@ -322,27 +337,12 @@ export async function getPage( source: "Crossbell Note", identity: input.site, platform: "Crossbell", - limit: 1000, - ...(input.pageId && { - filter: { - id: input.pageId, - }, - }), + filter: { + id: input.pageId, + }, }) - let page - if (input.page) { - page = pages?.list.find((item) => { - item.slug = - item.attributes?.find((a) => a.trait_type === "xlog_slug")?.value || - item.metadata?.raw?._xlog_slug || - item.metadata?.raw?._crosslog_slug || - item.id - return item.slug === input.page || item.id === input.page - }) - } else { - page = pages?.list[0] - } + let page = pages?.list[0] || null if (localPage) { if (page) { @@ -361,8 +361,6 @@ export async function getPage( if (page) { expandPage(page, input.render || false) - } else { - page = null } return page diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 31f88583..73cc68f2 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,12 +1,9 @@ -import { useRouter } from "next/router" import { SitePage } from "~/components/site/SitePage" import { SITE_URL } from "~/lib/env" import { SiteLayout } from "~/components/site/SiteLayout" import { useState } from "react" export default function Custom404() { - const router = useRouter() - const [siteId, setSiteId] = useState("") try { diff --git a/src/pages/api/slug2id.ts b/src/pages/api/slug2id.ts new file mode 100644 index 00000000..8fefa2a9 --- /dev/null +++ b/src/pages/api/slug2id.ts @@ -0,0 +1,85 @@ +import { NextApiRequest, NextApiResponse } from "next" +import { cacheGet, cacheDelete } from "~/lib/redis.server" + +const getSlug = (note: any) => { + return ( + note.metadata?.content?.attributes?.find( + (a: any) => a?.trait_type === "xlog_slug", + )?.value || + note.metadata?.content?._xlog_slug || + note.metadata?.content?._crosslog_slug + )?.toLowerCase?.() +} + +export async function getIdBySlug(slug: string, handle: string) { + slug = (slug as string)?.toLowerCase?.() + + const result = await cacheGet( + ["slug2id", handle, slug], + async () => { + let note + let cursor = "" + + const characterRes = await ( + await fetch( + `https://indexer.crossbell.io/v1/handles/${handle}/character`, + ) + ).json() + const cid = characterRes?.characterId + + do { + const response = await ( + await fetch( + `https://indexer.crossbell.io/v1/notes?characterId=${cid}&sources=xlog&cursor=${cursor}&limit=100`, + ) + ).json() + cursor = response.cursor + note = response?.list?.find( + (item: any) => slug === (getSlug(item) || `${cid}-${item.noteId}`), + ) + } while (!note && cursor) + + if (note?.noteId) { + return { + noteId: note?.noteId, + characterId: cid, + } + } + }, + true, + ) + + if (result) { + fetch( + `https://indexer.crossbell.io/v1/characters/${result.characterId}/notes/${result.noteId}`, + ) + .then((res) => res.json()) + .then((note) => { + if (note) { + const currentSlug = + getSlug(note) || `${result.characterId}-${note.noteId}` + if (currentSlug !== slug) { + cacheDelete(["slug2id", handle, slug]) + } + } else { + cacheDelete(["slug2id", handle, slug]) + } + }) + } + + return result +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + let { handle, slug } = req.query + + if (!slug || !handle) { + res.status(400).send("Bad Request") + return + } + + res.status(200).send(await getIdBySlug(slug as string, handle as string)) +} diff --git a/src/queries/page.server.ts b/src/queries/page.server.ts index c97a57b7..17879a1b 100644 --- a/src/queries/page.server.ts +++ b/src/queries/page.server.ts @@ -1,6 +1,7 @@ import * as pageModel from "~/models/page.model" import { QueryClient } from "@tanstack/react-query" import { cacheGet } from "~/lib/redis.server" +import { getIdBySlug } from "~/pages/api/slug2id" export const fetchGetPage = async ( input: Parameters[0], @@ -8,6 +9,17 @@ export const fetchGetPage = async ( ) => { const key = ["getPage", input.page, input] return await queryClient.fetchQuery(key, async () => { + if (!input.pageId) { + if (!input.page || !input.site) { + return null + } + const slug2Id = await getIdBySlug(input.page, input.site) + input.pageId = `${slug2Id.characterId}-${slug2Id.noteId}` + if (!input.pageId) { + return null + } + } + delete input.page return cacheGet(key, () => pageModel.getPage(input)) }) }