import { createServerFn } from "@tanstack/react-start"; import { isTakenDown } from "~/lib/account-disable"; import { z } from "zod"; import { viewerMiddleware } from "./middleware"; import { first } from "~/server/db"; import { repoDb } from "~/server/scoped-db"; import { limitBrowse } from "~/server/limits"; import { loadRepo, repoPermission } from "~/server/repos"; import { withStore } from "~/server/git/store"; import { accountName } from "~/lib/validation"; const ISSUE_REF = /^([^/]+)\/([^#]+)#(\d+)$/; const COMMIT_REF = /^([^/]+)\/([^@]+)@([0-9a-f]{7,40})$/; const FULL_SHA = 40; const SHORT_SHA = 7; const MAX_TEXT = 200; async function readableRepo(owner: string, name: string, viewer: Parameters[1]) { const repo = await loadRepo(owner, name); if (!repo || (await repoPermission(repo, viewer)) === "none") return null; return repo; } /** Preview data for `data-hovercard` links rendered by the markdown pipeline. */ export const getHovercard = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(z.object({ kind: z.enum(["issue", "user", "team", "commit"]), ref: z.string().min(1).max(300) })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); if (data.kind === "user" || data.kind === "team") { const name = accountName.safeParse(data.ref.split("/")[0]); if (!name.success) return null; const account = await first<{ id: string; name: string; display_name: string | null; bio: string | null; kind: string; website: string | null; avatar_key: string | null; created_at: number; disabled_at: number | null; disabled_reason: string | null; followers: number; repos: number; }>( `SELECT a.id, a.name, a.display_name, a.bio, a.kind, a.website, a.avatar_key, a.created_at, a.disabled_at, a.disabled_reason, (SELECT COUNT(*) FROM follows f WHERE f.followee_id = a.id) AS followers, (SELECT COUNT(*) FROM repos r WHERE r.owner_id = a.id AND r.visibility = 'public') AS repos FROM accounts a WHERE a.id = COALESCE( (SELECT id FROM accounts WHERE name = ?1), (SELECT account_id FROM account_redirects WHERE old_name = ?1))`, name.data, ); if (!account || (isTakenDown(account) && !context.viewer?.isAdmin)) return null; const team = data.kind === "team" ? data.ref.split("/").slice(1).join("/") : null; return { kind: data.kind, title: team ? `@${account.name}/${team}` : (account.display_name ?? account.name), subtitle: team ? `Team in ${account.name}` : `@${account.name} · ${account.kind}`, body: (account.bio ?? "").slice(0, MAX_TEXT), state: null, profile: team ? null : { name: account.name, avatarUrl: account.avatar_key ? `/avatars/${account.avatar_key}` : null, website: account.website, followers: account.followers, repos: account.repos, joinedAt: account.created_at, }, }; } if (data.kind === "issue") { const match = ISSUE_REF.exec(data.ref); if (!match) return null; const repo = await readableRepo(match[1], match[2], context.viewer); if (!repo) return null; const issue = await repoDb(repo.id).first<{ title: string; state: string; is_pull: number; body: string; comments_count: number; }>( "SELECT title, state, is_pull, body, comments_count FROM issues WHERE repo_id = ? AND number = ? AND hidden_at IS NULL", repo.id, Number(match[3]), ); if (!issue) return null; return { kind: "issue" as const, title: issue.title, subtitle: `${repo.owner_name}/${repo.name} ${issue.is_pull ? "pull request" : "issue"} #${match[3]} · ${issue.comments_count} comments`, body: issue.body.slice(0, MAX_TEXT), state: issue.state, profile: null, }; } const match = COMMIT_REF.exec(data.ref); if (!match) return null; const repo = await readableRepo(match[1], match[2], context.viewer); if (!repo) return null; const sha = match[3]; const commit = sha.length === FULL_SHA ? await withStore(repo.artifacts_name, (store) => store.commit(sha)) : null; return { kind: "commit" as const, title: commit ? commit.subject.slice(0, MAX_TEXT) : `Commit ${sha.slice(0, SHORT_SHA)}`, subtitle: `${repo.owner_name}/${repo.name}@${sha.slice(0, SHORT_SHA)}${commit ? ` · ${commit.authorName}` : ""}`, body: "", state: null, profile: null, }; });