0
2
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<GrainActorProfile>("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<string, Label[]>;
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 });
});