// skinny atproto types file for supporting constellation.ts const DEFAULT_SERVICE = "https://api.bsky.app"; /** Type used to imply that a parameter will be run through {@link ValidateNSID} */ export type NSID = string; const NSIDExpression = /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$/; export function ValidateNSID(nsid: string): string | null { return NSIDExpression.test(nsid) ? nsid : null; } export type Handle = string; export function StripHandle(handle: Handle) { return handle.replace("@", ""); } export type DID = `did:${"web"|"plc"}:${string}`; export function ValidateDID(did: string): string | null { const parts = did.split(":"); const isValid = parts.length == 3 && parts[0] == "did" && (parts[1] == "plc" || parts[1] == "web") && parts[2].length > 0; return isValid ? did : null; } export type AtURIString = string; //`at://${string}/${string}/${string}`; export class AtURI { readonly authority: string | null; readonly collection: string | null; readonly rkey: string | null; static fromString(uri: AtURIString): AtURI { const parts = uri.split("/").slice(2); return new AtURI(ValidateDID(parts[0]), ValidateNSID(parts[1]), parts[2]); } constructor(authority: string | null, collection: string | null = null, rkey: string | null = null) { this.authority = authority; this.collection = collection; this.rkey = rkey; } /** * Converts URI to at:// URI. * @returns The string form of this URI, unless if any parts are specified without any preceding elements. * @example ``` * // Invalid collection NSID, returns null. * new AtURI("at://did:web:example.com/cheese/abc123").toString() * // Invalid 'authority' DID, returns null. * new AtURI("at://not-a-did/com.example.nsid").toString() * // All good and happy, returns the string fed in. * new AtURI("at://did:web:example.com/com.example.nsid/abc123").toString() * ``` */ toString(): string | null { const ret: (string|null)[] = ["at://"]; // using `?? ""` to have a "bad" value to find if (this.authority) { ret.push(this.authority ?? ""); } else ret.push(null); if (this.collection) { if (ret.indexOf(null) != -1) {return null;} ret.push("/"); ret.push(this.collection ?? ""); } else ret.push(null); if (this.rkey) { if (ret.indexOf(null) != -1) {return null;} ret.push("/"); ret.push(this.rkey ?? ""); } return ret.join(""); } } export type RecordResponse = { cid: string; uri: AtURIString; value: T; } // technically you can cast it to whatever you want but i feel like using a generic(?) makes it cleaner /** Calls an XRPC "query" method (HTTP GET) * @param service Defaults to the {@link https://api.bsky.app/ Bluesky (PBC) API} service. */ export async function XQuery(method: string, params: Record | null = null, service: string = DEFAULT_SERVICE) { let QueryURL = `${service}/xrpc/${method}`; if (params) { const usp = new URLSearchParams(); for (const key in params) { if (params[key]) { usp.append(key, params[key].toString()); } } QueryURL += "?" + usp.toString(); } return (await (await fetch(QueryURL)).json()) as T; } /** Calls com.atproto.repo.getRecord with XQuery */ export async function GetRecord(uri: AtURI, service: string = "https://slingshot.microcosm.blue") { return await XQuery>("com.atproto.repo.getRecord", { repo: uri.authority!, collection: uri.collection!, rkey: uri.rkey! }, service); } type ListRecordsResponse = { cursor: string; records: RecordResponse[]; }; export async function ListRecords(repo: string, collection: NSID, service: string, limit: number = 50) { return await XQuery>("com.atproto.repo.listRecords", { repo: repo, collection: collection, limit: limit }, service); }