import { defineQuery } from "$hatk"; import { views } from "$hatk"; import type { GrainActorProfile, Story, Label } from "$hatk"; import { lookupCrossPosts } from "../hydrate/galleries.ts"; import { lookupHandles } from "../helpers/lookupHandles.ts"; import { resolveAtUri } from "../helpers/resolveHandle.ts"; import { lookupViewedStories, storyViewerState } from "../helpers/storyViews.ts"; export default defineQuery("social.grain.unspecced.getStory", async (ctx) => { const { db, ok } = ctx; let storyUri = ctx.params.story; if (!storyUri) return ok({}); // Resolve handle in AT URI if needed const resolved = await resolveAtUri(db, storyUri); if (resolved) storyUri = resolved; const rows = (await db.query( `SELECT s.uri, s.cid, s.did, s.media, s.aspect_ratio, s.location, s.address, s.created_at FROM "social.grain.story" s LEFT JOIN _repos r ON r.did = s.did WHERE s.uri = $1 AND (r.status IS NULL OR r.status NOT IN ('takendown', 'deactivated', 'deleted'))`, [storyUri], )) as { uri: string; cid: string; did: string; media: string; aspect_ratio: string; location: string | null; address: string | null; created_at: string; }[]; const row = rows[0]; if (!row) return ok({}); // Resolve author profile const [profiles, handleMap] = await Promise.all([ ctx.lookup("social.grain.actor.profile", "did", [row.did]), lookupHandles(db, [row.did]), ]); const author = profiles.get(row.did); const profileView = author ? views.grainActorDefsProfileView({ cid: author.cid, did: author.did, handle: author.handle ?? handleMap.get(author.did) ?? author.did, displayName: author.value.displayName, avatar: ctx.blobUrl(author.did, author.value.avatar) ?? undefined, }) : views.grainActorDefsProfileView({ cid: "", did: row.did, handle: handleMap.get(row.did) ?? row.did, }); let blobRef: any; try { blobRef = typeof row.media === "string" ? JSON.parse(row.media) : row.media; } catch { blobRef = row.media; } let aspectRatio: { width: number; height: number }; try { aspectRatio = typeof row.aspect_ratio === "string" ? JSON.parse(row.aspect_ratio) : row.aspect_ratio; } catch { aspectRatio = { width: 4, height: 3 }; } let location: Story["location"] | null = null; if (row.location) { try { location = typeof row.location === "string" ? JSON.parse(row.location) : row.location; } catch { location = null; } } let address: Story["address"] | null = null; if (row.address) { try { address = typeof row.address === "string" ? JSON.parse(row.address) : row.address; } catch { address = null; } } // Labels: merge external + self-labels const externalLabels = (await ctx.labels([row.uri])) as Map; const selfLabelRows = (await db.query( `SELECT parent_uri, val FROM "social.grain.story__labels_self_labels" WHERE parent_uri = $1`, [row.uri], )) as { parent_uri: string; val: string }[]; const labels = externalLabels.get(row.uri) ?? []; for (const sl of selfLabelRows) { labels.push({ src: row.did, uri: row.uri, val: sl.val, cts: new Date().toISOString() }); } // Cross-post lookup, and whether the viewer has already watched it const [crossPostMap, viewed] = await Promise.all([ lookupCrossPosts(db, [row], "story"), lookupViewedStories(db, ctx.viewer?.did, [row.uri]), ]); const crossPostUrl = crossPostMap.get(row.uri); const crossPost = crossPostUrl ? { url: crossPostUrl } : undefined; const story = views.storyView({ uri: row.uri, cid: row.cid, creator: profileView, thumb: ctx.blobUrl(row.did, blobRef, "feed_thumbnail") ?? "", fullsize: ctx.blobUrl(row.did, blobRef, "feed_fullsize") ?? "", aspectRatio, ...(location ? { location: { name: location.name, value: location.value }, ...(address ? { address } : {}), } : {}), ...(crossPost ? { crossPost } : {}), ...(labels.length > 0 ? { labels } : {}), createdAt: row.created_at, ...storyViewerState(undefined, viewed.has(row.uri)), }); return ok({ story }); });