import { atUriOf, nsidOf, recordKeyOf, type AtUri, type Cid, type Did, type LabelKey, type Rkey } from "$lib/api/syntax"; import { now as tidNow } from "@atcute/tid"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { deleteRecord, putRecord } from "$lib/api/write"; import { awaitReadable, awaitRemoved } from "$lib/api/awaitRecord"; import { writeTargetFor } from "$lib/api/knot"; import { eraseCob, withRkey, type Written } from "$lib/api/cob"; import { createTicket, editTicket as restateOnKnot, TICKET_COLLECTION, type TicketProse } from "$lib/api/ticket"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; import type { RecordList, RecordView, TicketRecord, TicketState } from "$lib/api/records"; import type * as ShTangledRepoIssue from "$lib/api/lexicons/types/sh/tangled/repo/issue"; import type * as ShTangledRepoIssueState from "$lib/api/lexicons/types/sh/tangled/repo/issue/state"; export type IssueRecord = ShTangledRepoIssue.Main; export type IssueStateRecord = ShTangledRepoIssueState.Main; const ISSUE_COLLECTION = nsidOf("sh.tangled.repo.issue"); export interface TicketInput { title: string; body?: string; targetBranch?: string; tangledLabels?: LabelKey[]; } const legacyIssue = (repoDid: Did, { title, body }: TicketInput): IssueRecord => ({ $type: "sh.tangled.repo.issue" as IssueRecord["$type"], repo: repoDid as IssueRecord["repo"], title, body: body ?? "", createdAt: new Date().toISOString() }) as IssueRecord; const proseOf = ({ title, body, targetBranch, tangledLabels }: TicketInput): TicketProse => ({ title, body: body ?? "", targetBranch, tangledLabels }); export const openTicket = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: TicketInput ): Promise => { const target = await writeTargetFor(bobbinUrl, repoDid); if (target.kind === "knot") return withRkey(await createTicket(agent, target.knot, repoDid, proseOf(input))); const saved = await putRecord( agent, ISSUE_COLLECTION, recordKeyOf(tidNow()), legacyIssue(repoDid, input) ); await awaitReadable(bobbinUrl, saved); return withRkey(saved); }; export const editTicket = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: TicketInput & { rkey: Rkey; swapRecord: Cid } ): Promise => { const target = await writeTargetFor(bobbinUrl, repoDid); if (target.kind === "knot") { const uri = atUriOf(`at://${repoDid}/${TICKET_COLLECTION}/${input.rkey}`); return withRkey(await restateOnKnot(agent, target.knot, uri, proseOf(input))); } const saved = await putRecord( agent, ISSUE_COLLECTION, input.rkey, legacyIssue(repoDid, input), input.swapRecord ); await awaitReadable(bobbinUrl, saved); return withRkey(saved); }; export const removeTicket = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: { rkey: Rkey; swapRecord: Cid } ): Promise<{ uri: AtUri }> => { const target = await writeTargetFor(bobbinUrl, repoDid); const uri = atUriOf(`at://${repoDid}/${TICKET_COLLECTION}/${input.rkey}`); if (target.kind === "knot") await eraseCob(agent, target.knot, { uri, cid: input.swapRecord }); else { await deleteRecord(agent, ISSUE_COLLECTION, input.rkey, input.swapRecord); await awaitRemoved(bobbinUrl, uri); } return { uri }; }; export const getIssue = (ctx: BobbinContext, issue: AtUri, init?: XrpcRequestInit) => jsonGet>(ctx, "sh.tangled.repo.getIssue", { issue }, init); export const getIssues = (ctx: BobbinContext, issues: readonly AtUri[], init?: XrpcRequestInit) => jsonGet>(ctx, "sh.tangled.repo.getIssues", { issues }, init); export type IssueState = "open" | "closed"; // a listIssues row: the embedded issue record plus bobbin-derived state/counts. export interface IssueListItem { cid?: string; commentCount: number; state: TicketState; stateUpdatedAt?: string; uri: string; value: TicketRecord; } export interface IssueListPage { cursor?: string | null; total?: number; items: IssueListItem[]; } export interface ListIssuesFilter { state?: TicketState; author?: string; limit?: number; cursor?: string; order?: "asc" | "desc"; } export const listIssues = ( ctx: BobbinContext, subject: Did, filter: ListIssuesFilter = {}, init?: XrpcRequestInit ) => jsonGet(ctx, "sh.tangled.repo.listIssues", { subject, ...filter }, init); export interface IssueStateList { cursor?: string | null; items: RecordView[]; } export const listIssueStates = ( ctx: BobbinContext, subject: AtUri, filter: { limit?: number; cursor?: string; order?: "asc" | "desc" } = {}, init?: XrpcRequestInit ) => jsonGet(ctx, "sh.tangled.repo.issue.listStates", { subject, ...filter }, init);