import { ClientResponseError, ok } from "@atcute/client"; import { mainSchema as getRecordSchema } from "@atcute/atproto/types/repo/getRecord"; import { mainSchema as uploadBlobSchema } from "@atcute/atproto/types/repo/uploadBlob"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { createClient } from "#lib/auth/agent.ts"; import { FOLLOW_VIEWER, STAR_VIEWER } from "#lib/api/descriptors.ts"; import { enrich, target } from "#lib/api/enrich.ts"; import { jsonGet } from "#lib/api/_request.ts"; import type { BobbinContext, XrpcRequestInit } from "#lib/api/client.ts"; import { nsidOf, recordKeyOf } from "#lib/api/syntax.ts"; import { putRecord, type WrittenRecord } from "#lib/api/write.ts"; import type * as OrgTangledActorGetTrending from "#lib/api/lexicons/types/org/tangled/actor/getTrending.ts"; import type { ProfileRecord } from "#lib/api/records.ts"; export const PROFILE_COLLECTION = "org.tangled.actor.profile"; export const PROFILE_LIMITS = { description: 256, pronouns: 20, location: 40 } as const; const AVATAR_TYPES: Record = { "image/png": true, "image/jpeg": true }; const MAX_AVATAR_BYTES = 1_000_000; export const getTrending = (ctx: BobbinContext, init?: XrpcRequestInit) => jsonGet(ctx, "org.tangled.actor.getTrending", {}, init); export const getTrendingWithViewerState = ( ctx: BobbinContext, viewer: string, init?: XrpcRequestInit ) => enrich( ctx, { xrpc: "org.tangled.actor.getTrending", viewer, enrich: [target(FOLLOW_VIEWER, ["people[].did"]), target(STAR_VIEWER, ["repos[].did"])] }, init ); export const avatarProblem = (image: Blob): string | undefined => { if (!AVATAR_TYPES[image.type]) return "Choose a PNG or JPEG image."; if (image.size > MAX_AVATAR_BYTES) return "Profile pictures must be 1 MB or smaller."; return undefined; }; export const uploadProfileAvatar = async ( agent: OAuthUserAgent, image: Blob ): Promise> => { const problem = avatarProblem(image); if (problem) throw new Error(problem); const rpc = createClient(agent); const { blob } = await ok(rpc.call(uploadBlobSchema, { input: image })); return blob; }; // the profile record lives at the fixed rkey `self`; put upserts it. export const putProfile = async ( agent: OAuthUserAgent, record: ProfileRecord ): Promise => { return putRecord(agent, nsidOf(PROFILE_COLLECTION), recordKeyOf("self"), record); }; // asks the account's own pds rather than bobbin: a fresh account is not indexed // yet, and a stale index answer here would overwrite a profile the user edited export const hasProfile = async (agent: OAuthUserAgent): Promise => { try { await ok( createClient(agent).call(getRecordSchema, { params: { repo: agent.sub, collection: PROFILE_COLLECTION, rkey: "self" } }) ); return true; } catch (cause) { if (cause instanceof ClientResponseError && cause.error === "RecordNotFound") return false; throw cause; } }; // the counterpart of the home page's getProfile check that sends new users to // /welcome; accounts nobody logs into as themselves get their profile written here export const ensureProfile = async ( agent: OAuthUserAgent, record: ProfileRecord ): Promise => { if (await hasProfile(agent)) return; await putProfile(agent, record); };