import { page } from "$app/state"; import { avatarUrl } from "#lib/avatar.ts"; import type { Did, Handle } from "@atcute/lexicons/syntax"; import type { DiffStat } from "#lib/api/diff.ts"; import type { ProfileActivityData } from "#lib/components/profile/types.ts"; import type { PipelineStatus, PipelineSummary, WorkflowSummary } from "#lib/components/repo/types.ts"; export function resolveOgreUrl(baseUrl?: string): string { if (baseUrl) return baseUrl; try { return page.data.publicConfig?.ogreUrl ?? ""; } catch { return ""; } } export function resolveAppUrl(appUrl?: string): string { if (appUrl) return appUrl.replace(/\/+$/, ""); try { return page.url.origin; } catch { return "https://tangled.org"; } } const ogAvatarUrl = (did: string | undefined, appUrl?: string): string | undefined => did ? `${resolveAppUrl(appUrl)}${avatarUrl(did, 256, "jpeg")}` : undefined; export interface OgRepoPayload { type: "repository"; repoName: string; ownerHandle: string; stars?: number; pulls?: number; issues?: number; createdAt?: string; avatarUrl?: string; languages?: { color: string; percentage: number }[]; } export interface OgIssuePayload { type: "issue"; repoName: string; ownerHandle: string; authorHandle: string; title: string; issueNumber: number | string; status: "open" | "closed"; avatarUrl?: string; authorAvatarUrl?: string; commentCount?: number; createdAt: string; } export interface OgPullPayload { type: "pullRequest"; repoName: string; ownerHandle: string; authorHandle: string; title: string; pullRequestNumber: number | string; status: "open" | "closed" | "merged"; avatarUrl?: string; authorAvatarUrl?: string; commentCount?: number; filesChanged?: number; additions?: number; deletions?: number; rounds?: number; createdAt: string; } export interface OgProfilePayload { type: "profile"; handle: string; avatarUrl?: string; description?: string; followers?: number; repos?: number; stars?: number; activity?: number[]; } export interface OgStringPayload { type: "string"; ownerHandle: string; avatarUrl?: string; filename: string; description?: string; preview?: string[]; lineCount?: number; createdAt?: string; } export interface OgCommitPayload { type: "commit"; repoName: string; ownerHandle: string; avatarUrl?: string; authorHandle: string; authorAvatarUrl?: string; subject: string; shortHash: string; filesChanged?: number; additions?: number; deletions?: number; createdAt?: string; } export interface OgFilePayload { type: "file"; repoName: string; ownerHandle: string; avatarUrl?: string; path: string; ref: string; lineCount?: number; sizeLabel?: string; } export interface OgTagPayload { type: "tag"; repoName: string; ownerHandle: string; avatarUrl?: string; name: string; message?: string; taggerName?: string; createdAt?: string; } export type OgPipelineTrigger = | { kind: "push"; ref: string } | { kind: "pull_request"; ref: string; source: string } | { kind: "manual" }; export interface OgPipelinePayload { type: "pipeline"; repoName: string; ownerHandle: string; avatarUrl?: string; workflow: string; status: PipelineStatus; durationMs?: number; trigger: OgPipelineTrigger; shortHash: string; createdAt?: string; } export type OgPayload = | OgRepoPayload | OgIssuePayload | OgPullPayload | OgProfilePayload | OgStringPayload | OgCommitPayload | OgFilePayload | OgTagPayload | OgPipelinePayload; export function encodeOgPayload(payload: OgPayload): string { const entries = Object.entries(payload).filter( ([_, v]) => v != null && v !== "" && !(Array.isArray(v) && v.length === 0) ); const json = JSON.stringify(Object.fromEntries(entries)); if (typeof Buffer !== "undefined") { return Buffer.from(json, "utf-8").toString("base64url"); } const bytes = new TextEncoder().encode(json); let bin = ""; for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } export function ogCardUrl(payload: OgPayload, baseUrl?: string): string { return `${resolveOgreUrl(baseUrl)}/card?payload=${encodeOgPayload(payload)}`; } export function repoCard( repo: { name: string; ownerHandle: string; ownerDid?: string; createdAt?: string }, counts?: { stars?: number; pulls?: number; issues?: number } | null, baseUrl?: string, languages?: { color: string; percentage: number }[] | null, appUrl?: string ): string { return ogCardUrl( { type: "repository", repoName: repo.name, ownerHandle: repo.ownerHandle, stars: counts?.stars, pulls: counts?.pulls, issues: counts?.issues, createdAt: repo.createdAt, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), languages: languages ?? undefined }, baseUrl ); } export function issueCard( repo: { name: string; ownerHandle: string; ownerDid?: string }, issue: { title: string; issueNumber?: number | string; rkey?: string; state?: string; createdAt: string; author?: { handle?: string; avatar?: string; did?: string }; authorHandle?: string; authorDid?: string; commentCount?: number; }, baseUrl?: string, appUrl?: string ): string { const authorHandle = issue.author?.handle || issue.authorHandle || ""; const authorDid = issue.author?.did || issue.authorDid; return ogCardUrl( { type: "issue", repoName: repo.name, ownerHandle: repo.ownerHandle, authorHandle, title: issue.title || "Issue", issueNumber: issue.issueNumber ?? issue.rkey ?? "", status: issue.state === "closed" ? "closed" : "open", createdAt: issue.createdAt, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), authorAvatarUrl: issue.author?.avatar || ogAvatarUrl(authorDid, appUrl), commentCount: issue.commentCount }, baseUrl ); } export function pullCard( repo: { name: string; ownerHandle: string; ownerDid?: string }, pull: { title?: string; rkey: string; state?: "open" | "closed" | "completed"; createdAt: string; author?: { handle?: string; did?: string }; versions: readonly unknown[]; }, commentCount?: number, stat?: DiffStat, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "pullRequest", repoName: repo.name, ownerHandle: repo.ownerHandle, authorHandle: pull.author?.handle || "", title: pull.title || "Pull Request", pullRequestNumber: pull.rkey, status: pull.state === "completed" ? "merged" : (pull.state ?? "open"), createdAt: pull.createdAt, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), authorAvatarUrl: ogAvatarUrl(pull.author?.did, appUrl), commentCount, filesChanged: stat?.files_changed, additions: stat?.insertions, deletions: stat?.deletions, rounds: pull.versions.length || undefined }, baseUrl ); } type OgRepo = { name: string; ownerHandle: string; ownerDid?: string }; const ACTIVITY_MONTHS = 6; const PREVIEW_LINES = 9; const PREVIEW_LINE_LENGTH = 120; export const stringPreview = (contents: string): string => contents .split("\n", PREVIEW_LINES) .map((line) => line.replace(/\t/g, " ").slice(0, PREVIEW_LINE_LENGTH)) .join("\n"); const MESSAGE_LENGTH = 500; export function profileCard( identity: { did: Did; handle: Handle }, profile: { description?: string } | null, counts: { followers: number; repos: number; stars: number } | null, activity?: ProfileActivityData, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "profile", handle: identity.handle, avatarUrl: ogAvatarUrl(identity.did, appUrl), description: profile?.description?.trim().slice(0, MESSAGE_LENGTH), followers: counts?.followers, repos: counts?.repos, stars: counts?.stars, activity: activity ? Array.from({ length: ACTIVITY_MONTHS }, (_, index) => { const month = activity.months.find( (each) => each.monthsAgo === ACTIVITY_MONTHS - 1 - index ); return month ? month.commits + month.repos.length + month.issues.length + month.pulls.length : 0; }) : undefined }, baseUrl ); } export function stringCard( snippet: { ownerHandle: string; ownerDid?: string; filename: string; description?: string; contents: string; lineCount: number | null; createdAt: string; }, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "string", ownerHandle: snippet.ownerHandle, avatarUrl: ogAvatarUrl(snippet.ownerDid, appUrl), filename: snippet.filename, description: snippet.description?.trim().slice(0, MESSAGE_LENGTH), preview: stringPreview(snippet.contents).split("\n"), lineCount: snippet.lineCount ?? undefined, createdAt: snippet.createdAt }, baseUrl ); } export function commitCard( repo: OgRepo, commit: { subject: string; shortHash: string; authorName: string; authorWhen: string; author?: { handle?: string; did?: string }; }, stat?: { insertions: number; deletions: number; files_changed: number }, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "commit", repoName: repo.name, ownerHandle: repo.ownerHandle, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), authorHandle: commit.author?.handle || commit.authorName, authorAvatarUrl: ogAvatarUrl(commit.author?.did, appUrl), subject: commit.subject || commit.shortHash, shortHash: commit.shortHash, filesChanged: stat?.files_changed, additions: stat?.insertions, deletions: stat?.deletions, createdAt: commit.authorWhen }, baseUrl ); } export function fileCard( repo: OgRepo, file: { path: string; ref: string; lines: number | null; sizeLabel: string | null }, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "file", repoName: repo.name, ownerHandle: repo.ownerHandle, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), path: file.path, ref: file.ref, lineCount: file.lines ?? undefined, sizeLabel: file.sizeLabel ?? undefined }, baseUrl ); } export function tagCard( repo: OgRepo, tag: { name: string; message?: string; taggerName?: string; when?: string }, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "tag", repoName: repo.name, ownerHandle: repo.ownerHandle, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), name: tag.name, message: tag.message?.trim().slice(0, MESSAGE_LENGTH), taggerName: tag.taggerName, createdAt: tag.when }, baseUrl ); } const ogTrigger = (trigger: PipelineSummary["trigger"]): OgPipelineTrigger => { switch (trigger.kind) { case "push": return { kind: "push", ref: trigger.targetRef }; case "pull_request": return { kind: "pull_request", ref: trigger.targetRef, source: trigger.sourceLabel }; case "manual": return { kind: "manual" }; } }; export function pipelineCard( repo: OgRepo, pipeline: Pick, workflow: Pick, baseUrl?: string, appUrl?: string ): string { return ogCardUrl( { type: "pipeline", repoName: repo.name, ownerHandle: repo.ownerHandle, avatarUrl: ogAvatarUrl(repo.ownerDid, appUrl), workflow: workflow.name, status: workflow.status, durationMs: workflow.duration, trigger: ogTrigger(pipeline.trigger), shortHash: pipeline.sha.slice(0, 8), createdAt: pipeline.createdAt }, baseUrl ); }