import { ValidationResult } from "npm:@atproto/lexicon"; import { parseAtUri } from "./aturi.ts"; import { FINEPDSAndHandleFromDid } from "./identity.ts"; import * as ATPAPI from "npm:@atproto/api" export function validate( record: unknown, validator: (v: unknown) => ValidationResult ): record is R { return validator(record).success; } type NotificationDeclarationRecord = { $type: "app.bsky.notification.declaration"; // its not a string its a enum but we just dont have the lexicons for it allowSubscriptions: string; [k: string]: unknown; }; // declare function validateNotificationDeclarationRecord(v: V): ValidationResult; function validateNotificationDeclaration( obj: unknown ): obj is NotificationDeclarationRecord { return ( typeof obj === "object" && obj !== null && typeof (obj as any).allowSubscriptions === "string" ); } const recordValidators = { "app.bsky.actor.profile": ATPAPI.AppBskyActorProfile.validateRecord, "app.bsky.feed.generator": ATPAPI.AppBskyFeedGenerator.validateRecord, "app.bsky.feed.like": ATPAPI.AppBskyFeedLike.validateRecord, "app.bsky.feed.post": ATPAPI.AppBskyFeedPost.validateRecord, "app.bsky.feed.repost": ATPAPI.AppBskyFeedRepost.validateRecord, "app.bsky.feed.threadgate": ATPAPI.AppBskyFeedThreadgate.validateRecord, "app.bsky.graph.block": ATPAPI.AppBskyGraphBlock.validateRecord, "app.bsky.graph.follow": ATPAPI.AppBskyGraphFollow.validateRecord, "app.bsky.graph.list": ATPAPI.AppBskyGraphList.validateRecord, "app.bsky.graph.listblock": ATPAPI.AppBskyGraphListblock.validateRecord, "app.bsky.graph.listitem": ATPAPI.AppBskyGraphListitem.validateRecord, "app.bsky.notification.declaration": validateNotificationDeclaration, // smh my head } as const; type RecordTypeMap = { "app.bsky.actor.profile": ATPAPI.AppBskyActorProfile.Record; "app.bsky.feed.generator": ATPAPI.AppBskyFeedGenerator.Record; "app.bsky.feed.like": ATPAPI.AppBskyFeedLike.Record; "app.bsky.feed.post": ATPAPI.AppBskyFeedPost.Record; "app.bsky.feed.repost": ATPAPI.AppBskyFeedRepost.Record; "app.bsky.feed.threadgate": ATPAPI.AppBskyFeedThreadgate.Record; "app.bsky.graph.block": ATPAPI.AppBskyGraphBlock.Record; "app.bsky.graph.follow": ATPAPI.AppBskyGraphFollow.Record; "app.bsky.graph.list": ATPAPI.AppBskyGraphList.Record; "app.bsky.graph.listblock": ATPAPI.AppBskyGraphListblock.Record; "app.bsky.graph.listitem": ATPAPI.AppBskyGraphListitem.Record; "app.bsky.notification.declaration": NotificationDeclarationRecord; // smh my smh head smh my head }; // type RecordTypeMap = { // [K in KnownRecordType]: ReturnType extends ValidationResult ? T : never // } type KnownRecordType = keyof typeof recordValidators; export function validateRecord( record: unknown ): RecordTypeMap[T] | undefined { const type = (record as { $type?: string })?.$type; if (!type || !(type in recordValidators)) return undefined; const validator = recordValidators[type as T] as ( v: unknown ) => ValidationResult; const result = validator(record); if (result.success) return result.value; return undefined; } export function assertRecord( record: unknown ): RecordTypeMap[T] | undefined { if (typeof record !== 'object' || record === null) { return undefined; } const type = (record as { $type?: string })?.$type; if (typeof type !== 'string' || !(type in recordValidators)) { return undefined; } return record as RecordTypeMap[T]; } export async function resolveRecordFromURI({ did, uri, }: { did?: string; uri: string; }): Promise { const parsed = parseAtUri(uri); const safeDid = did ?? parsed?.did!; const pdsurl = await FINEPDSAndHandleFromDid(safeDid); const res = await fetch( `${pdsurl}/xrpc/com.atproto.repo.getRecord?repo=${safeDid}&collection=${parsed?.collection}&rkey=${parsed?.rkey}` ); const json = await res.json(); const record = json?.value; return validateRecord(record); } async function _test(uri: string) { const record = await resolveRecordFromURI({ uri }); if (record?.$type === "app.bsky.feed.post") { record.text; // Type-safe access to `AppBskyFeedPost.Record` } }