import { ok } from '@atcute/client';
import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
import {
type ActorResolver,
type ResolveActorOptions,
type ResolvedActor,
} from '@atcute/identity-resolver';
import type {
ActorIdentifier,
Did,
Handle,
} from '@atcute/lexicons/syntax';
import { ShTangledActorProfile } from '@atcute/tangled';
import {
APP_BSKY_PROFILE_COLLECTION,
ACTOR_PROFILE_COLLECTION,
SLINGSHOT_SERVICE,
} from './constants';
import { appviewJson, AppviewUnavailableError, type AppviewRecordView } from './appview';
import { clearAppviewStatefulCursorCache } from './appview-cache';
import {
createAuthRpc as createClientAuthRpc,
getRpc,
normalizeServiceUrl,
} from './client';
import {
extractBlobCid,
getOptionalRecord,
} from './records';
interface MiniDocResponse {
did: Did;
handle: Handle;
pds: string;
}
interface ProfileWithAvatar {
avatar?: unknown;
}
const actorCache = new Map<string, Promise<ResolvedActor>>();
class AppviewFirstActorResolver implements ActorResolver {
async resolve(actor: ActorIdentifier, options?: ResolveActorOptions): Promise<ResolvedActor> {
try {
return normalizeResolvedActor(
await appviewJson<MiniDocResponse>(
'com.bad-example.identity.resolveMiniDoc',
{ identifier: actor },
options?.signal,
),
);
} catch (cause) {
if (!(cause instanceof AppviewUnavailableError)) {
throw cause;
}
const resolved = await ok(
getRpc(SLINGSHOT_SERVICE).get('blue.microcosm.identity.resolveMiniDoc', {
params: {
identifier: actor,
},
signal: options?.signal,
}),
);
return normalizeResolvedActor(resolved);
}
}
}
export const identityResolver = new AppviewFirstActorResolver();
export const clearApiCaches = () => {
actorCache.clear();
clearAppviewStatefulCursorCache();
};
export const createAuthRpc = createClientAuthRpc;
export const resolveActor = async (identifier: string): Promise<ResolvedActor> => {
const key = identifier.toLowerCase();
const cached = actorCache.get(key);
if (cached) {
return cached;
}
const promise = identityResolver.resolve(asActorIdentifier(identifier));
actorCache.set(key, promise);
return promise;
};
export const resolveActorForDisplay = async (did: Did): Promise<ResolvedActor> => {
try {
return await resolveActor(did);
} catch {
return unresolvedActorFromDid(did);
}
};
const normalizeResolvedActor = (resolved: MiniDocResponse): ResolvedActor => ({
did: resolved.did,
handle: resolved.handle,
pds: normalizeServiceUrl(resolved.pds),
});
const unresolvedActorFromDid = (did: Did): ResolvedActor => ({
did,
handle: did as unknown as Handle,
pds: '',
});
const getActorProfileFromAppview = async (actor: ResolvedActor): Promise<ShTangledActorProfile.Main | null> => {
const profile = await appviewJson<AppviewRecordView<ShTangledActorProfile.Main>>(
'sh.tangled.actor.getProfile',
{
actor: `at://${actor.did}/${ACTOR_PROFILE_COLLECTION}/self`,
},
);
return profile.value;
};
export const getActorProfile = async (actor: ResolvedActor): Promise<ShTangledActorProfile.Main | null> => {
try {
return await getActorProfileFromAppview(actor);
} catch (cause) {
if (!(cause instanceof AppviewUnavailableError)) {
throw cause;
}
return await getOptionalRecord<ShTangledActorProfile.Main>(
actor,
ACTOR_PROFILE_COLLECTION,
'self',
);
}
};
export const resolveAvatarUrl = async (identifier: string): Promise<string | null> => {
const actor = await resolveActor(identifier);
let tangledProfile: ShTangledActorProfile.Main | null;
try {
tangledProfile = await getActorProfileFromAppview(actor);
} catch (cause) {
if (!(cause instanceof AppviewUnavailableError)) {
throw cause;
}
tangledProfile = await getOptionalRecord<ShTangledActorProfile.Main>(
actor,
ACTOR_PROFILE_COLLECTION,
'self',
);
}
const tangledCid = extractBlobCid(tangledProfile?.avatar);
if (tangledCid) {
return buildPdsBlobUrl(actor.pds, actor.did, tangledCid);
}
const bskyProfile = await getOptionalRecord<ProfileWithAvatar>(
actor,
APP_BSKY_PROFILE_COLLECTION,
'self',
);
const bskyCid = extractBlobCid(bskyProfile?.avatar);
if (bskyCid) {
return buildPdsBlobUrl(actor.pds, actor.did, bskyCid);
}
return null;
};
const asActorIdentifier = (value: string): ActorIdentifier => value as ActorIdentifier;
const buildPdsBlobUrl = (pds: string, did: Did, cid: string): string => {
const url = new URL('/xrpc/com.atproto.sync.getBlob', normalizeServiceUrl(pds));
url.searchParams.set('did', did);
url.searchParams.set('cid', cid);
return url.toString();
};
export const putActorProfile = async (
agent: OAuthUserAgent,
profile: ShTangledActorProfile.Main
): Promise<void> => {
const rpc = createAuthRpc(agent);
await ok(
rpc.post('com.atproto.repo.putRecord', {
input: {
repo: agent.sub,
collection: ACTOR_PROFILE_COLLECTION,
rkey: 'self',
record: profile,
},
})
);
};