import { ok } from '@atcute/client';
import type { Did } from '@atcute/lexicons/syntax';
import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
import { ShTangledFeedStar } from '@atcute/tangled';
import {
appviewFallback,
getDistinctAppviewRecordCount,
listAllAppviewRecords,
type AppviewRecordView,
} from './appview';
import {
findBacklinkRefByDid,
getBacklinkDids,
} from './backlinks';
import { STAR_COLLECTION } from './constants';
import { createAuthRpc } from './identity';
import {
parseAtUri,
type RecordCreationResult,
} from './records';
import type { RepoContext } from './repos';
export interface RepoStarSummary {
count: number;
isStarred: boolean;
currentUserStarRkey?: string;
}
const STAR_BACKLINK_SOURCES = [
'sh.tangled.feed.star:subject.did',
'sh.tangled.feed.star:subjectDid',
];
const getRepoStarSummaryFromBacklinks = async (repo: RepoContext, viewerDid?: Did | null): Promise<RepoStarSummary> => {
const starrers = new Set<Did>();
const backlinkDidGroups = await Promise.all(STAR_BACKLINK_SOURCES.map((source) => getBacklinkDids(repo.repoDid, source)));
for (const did of backlinkDidGroups.flat()) {
starrers.add(did);
}
const currentUserStar =
viewerDid && starrers.has(viewerDid)
? await findBacklinkRefByDid(repo.repoDid, STAR_BACKLINK_SOURCES, viewerDid)
: undefined;
return {
count: starrers.size,
isStarred: !!currentUserStar,
currentUserStarRkey: currentUserStar?.rkey,
};
};
const getRepoStarCountFromBacklinks = async (repo: RepoContext): Promise<number> => {
const summary = await getRepoStarSummaryFromBacklinks(repo);
return summary.count;
};
const appviewStarSubjectDid = (star: ShTangledFeedStar.Main): Did | null => {
const subject = star.subject as { did?: unknown; subjectDid?: unknown };
const did = subject.did ?? subject.subjectDid;
return typeof did === 'string' && did.startsWith('did:') ? (did as Did) : null;
};
const getRepoStarCountFromAppview = async (repo: RepoContext): Promise<number> =>
getDistinctAppviewRecordCount('sh.tangled.feed.countStars', repo.repoDid);
const findCurrentUserStarFromAppview = async (
repo: RepoContext,
viewerDid: Did,
): Promise<AppviewRecordView<ShTangledFeedStar.Main> | undefined> => {
const stars = await listAllAppviewRecords<ShTangledFeedStar.Main>('sh.tangled.feed.listStarsBy', viewerDid);
return stars.find((star) => appviewStarSubjectDid(star.value) === repo.repoDid);
};
const getRepoStarSummaryFromAppview = async (
repo: RepoContext,
viewerDid?: Did | null,
): Promise<RepoStarSummary> => {
const [count, currentUserStar] = await Promise.all([
getRepoStarCountFromAppview(repo),
viewerDid ? findCurrentUserStarFromAppview(repo, viewerDid) : undefined,
]);
return {
count,
isStarred: !!currentUserStar,
currentUserStarRkey: currentUserStar ? parseAtUri(currentUserStar.uri).rkey : undefined,
};
};
export const getRepoStarSummary = async (repo: RepoContext, viewerDid?: Did | null): Promise<RepoStarSummary> =>
appviewFallback(
() => getRepoStarSummaryFromAppview(repo, viewerDid),
() => getRepoStarSummaryFromBacklinks(repo, viewerDid),
);
export const getRepoStarCount = async (repo: RepoContext): Promise<number> =>
appviewFallback(
() => getRepoStarCountFromAppview(repo),
() => getRepoStarCountFromBacklinks(repo),
);
export const createRepoStar = async (agent: OAuthUserAgent, repo: RepoContext): Promise<RecordCreationResult> => {
const rpc = createAuthRpc(agent);
const record: ShTangledFeedStar.Main = {
$type: 'sh.tangled.feed.star',
subject: {
$type: 'sh.tangled.feed.star#repo',
did: repo.repoDid,
},
createdAt: new Date().toISOString(),
};
const result = await ok(
rpc.post('com.atproto.repo.createRecord', {
input: {
repo: agent.sub,
collection: STAR_COLLECTION,
record,
},
}),
);
return {
uri: result.uri,
rkey: parseAtUri(result.uri).rkey,
};
};
export const deleteRepoStar = async (agent: OAuthUserAgent, rkey: string): Promise<void> => {
const rpc = createAuthRpc(agent);
await ok(
rpc.post('com.atproto.repo.deleteRecord', {
input: {
repo: agent.sub,
collection: STAR_COLLECTION,
rkey,
},
}),
);
};