import {
isCanonicalResourceUri,
isDid,
type CanonicalResourceUri,
type Did,
type Nsid
} from "@atcute/lexicons/syntax";
import type { BobbinContext } from "#lib/api/client.ts";
import {
enrich,
labelsOf,
target,
TYPE_LABELS,
type Enriched,
type Label,
type LabelsDescriptor,
type RecordPath,
type Sidecar
} from "#lib/api/enrich.ts";
import { PROFILE_COLLECTION } from "#lib/api/profile.ts";
import type { ProfileRecord, RecordView } from "#lib/api/records.ts";
export type LabeledProfile = Enriched<RecordView<ProfileRecord>>;
const SEVERITY = ["hide", "warn"] as const;
export type ModerationLevel = (typeof SEVERITY)[number];
export type ModerationTarget = Did | CanonicalResourceUri;
export interface Moderation {
level: ModerationLevel;
target: ModerationTarget;
}
export const isAccountModeration = (
moderation: Moderation
): moderation is Moderation & { target: Did } => isDid(moderation.target);
export const ACCOUNT_MODERATION: LabelsDescriptor = {
source: `${PROFILE_COLLECTION}:.repo`,
type: TYPE_LABELS
};
export const recordModeration = (collection: Nsid): LabelsDescriptor => ({
source: `${collection}:.`,
type: TYPE_LABELS
});
export const PROFILE_MODERATION = recordModeration(PROFILE_COLLECTION);
export const moderationTargets = (
collection: Nsid,
paths: { records: RecordPath[]; authors: RecordPath[] }
): LabelsDescriptor[] => [
target(ACCOUNT_MODERATION, paths.authors),
target(recordModeration(collection), paths.records)
];
const labeledAs = (labels: Label[], level: ModerationLevel): Moderation | undefined => {
const uri = labels.find(({ val }) => val === `!${level}`)?.uri;
return uri && (isDid(uri) || isCanonicalResourceUri(uri)) ? { level, target: uri } : undefined;
};
const severityOf = (moderation: Moderation): number => SEVERITY.indexOf(moderation.level);
export const strongestModeration = (
...candidates: (Moderation | undefined)[]
): Moderation | undefined =>
candidates
.filter((candidate): candidate is Moderation => candidate !== undefined)
.sort((a, b) => severityOf(a) - severityOf(b))[0];
export const moderationOf = (labels: Label[]): Moderation | undefined =>
strongestModeration(...SEVERITY.map((level) => labeledAs(labels, level)));
export const accountModerationOf = (data: Sidecar, did: string): Moderation | undefined =>
moderationOf(labelsOf(data, did, ACCOUNT_MODERATION));
export const recordModerationOf = (
data: Sidecar,
collection: Nsid,
uri: string
): Moderation | undefined => moderationOf(labelsOf(data, uri, recordModeration(collection)));
export const subjectModeration = (
data: Sidecar,
collection: Nsid,
subject: { uri: string; author: string }
): Moderation | undefined =>
strongestModeration(
accountModerationOf(data, subject.author),
recordModerationOf(data, collection, subject.uri)
);
export const getLabeledProfile = (ctx: BobbinContext, did: string): Promise<LabeledProfile> =>
enrich<RecordView<ProfileRecord>>(ctx, {
xrpc: "org.tangled.actor.getProfile",
params: { actor: `at://${did}/${PROFILE_COLLECTION}/self` },
enrich: [target(PROFILE_MODERATION, ["uri"])]
});
export const profileModeration = (
account?: Moderation,
profile?: LabeledProfile
): Moderation | undefined =>
strongestModeration(
account,
profile && recordModerationOf(profile.data, PROFILE_COLLECTION, profile.output.uri)
);