~james/core

core/web/src/lib/api/moderationLabels.ts -rw-r--r-- 3.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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)
	);