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<PipelineSummary, "sha" | "createdAt" | "trigger">,
workflow: Pick<WorkflowSummary, "name" | "status" | "duration">,
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
);
}