[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 175 lines 4.9 kB view raw
1import { AtUri } from "@atp/syntax"; 2import { DataPlane } from "../data-plane/index.ts"; 3import * as com from "../lex/com.ts"; 4import * as so from "../lex/so.ts"; 5import { ParsedLabelers } from "../util.ts"; 6import { 7 HydrationMap, 8 Merges, 9 parseRecord, 10 parseString, 11 RecordInfo, 12} from "./util.ts"; 13 14export type Label = com.atproto.label.defs.Label; 15export type LabelerRecord = so.sprk.labeler.service.Main; 16 17export type SubjectLabels = { 18 isImpersonation: boolean; 19 isTakendown: boolean; 20 needsReview: boolean; 21 labels: HydrationMap<Label>; // src + val -> label 22}; 23 24export class Labels extends HydrationMap<SubjectLabels> implements Merges { 25 static key(label: Label) { 26 return `${label.src}::${label.val}`; 27 } 28 override merge(map: Labels): this { 29 map.forEach((theirs, key) => { 30 if (!theirs) return; 31 const mine = this.get(key); 32 if (mine) { 33 mine.isTakendown = mine.isTakendown || theirs.isTakendown; 34 mine.labels = mine.labels.merge(theirs.labels); 35 } else { 36 this.set(key, theirs); 37 } 38 }); 39 return this; 40 } 41 getBySubject(sub: string): Label[] { 42 const it = this.get(sub)?.labels.values(); 43 if (!it) return []; 44 const labels: Label[] = []; 45 for (const label of it) { 46 if (label) labels.push(label); 47 } 48 return labels; 49 } 50} 51 52export type LabelerAgg = { 53 likes: number; 54}; 55 56export type LabelerAggs = HydrationMap<LabelerAgg>; 57 58export type Labeler = RecordInfo<LabelerRecord>; 59export type Labelers = HydrationMap<Labeler>; 60 61export type LabelerViewerState = { 62 like?: string; 63}; 64 65export type LabelerViewerStates = HydrationMap<LabelerViewerState>; 66 67export class LabelHydrator { 68 constructor(public dataplane: DataPlane) {} 69 70 async getLabelsForSubjects( 71 subjects: string[], 72 labelers: ParsedLabelers, 73 ): Promise<Labels> { 74 if (!subjects.length || !labelers.dids.length) return new Labels(); 75 const res = await this.dataplane.labels.getLabels( 76 subjects, 77 labelers.dids, 78 ); 79 80 return res.labels.reduce((acc, cur) => { 81 const label = cur as unknown as Label; 82 if (!label || label.neg) return acc; 83 const { sig: _, ...labelWithoutSig } = label; 84 let entry = acc.get(label.uri); 85 if (!entry) { 86 entry = { 87 isImpersonation: false, 88 isTakendown: false, 89 needsReview: false, 90 labels: new HydrationMap(), 91 }; 92 acc.set(label.uri, entry); 93 } 94 95 const isActionableNeedsReview = label.val === NEEDS_REVIEW_LABEL && 96 !label.neg && 97 labelers.redact.has(label.src); 98 99 // we action needs review labels on backend for now so don't send to client until client has proper logic for them 100 if (!isActionableNeedsReview) { 101 entry.labels.set(Labels.key(labelWithoutSig), labelWithoutSig); 102 } 103 104 if ( 105 TAKEDOWN_LABELS.includes(label.val) && 106 !label.neg && 107 labelers.redact.has(label.src) 108 ) { 109 entry.isTakendown = true; 110 } 111 if (isActionableNeedsReview) { 112 entry.needsReview = true; 113 } 114 if ( 115 label.val === IMPERSONATION_LABEL && 116 !label.neg && 117 labelers.redact.has(label.src) 118 ) { 119 entry.isImpersonation = true; 120 } 121 122 return acc; 123 }, new Labels()); 124 } 125 126 async getLabelers( 127 dids: string[], 128 includeTakedowns = false, 129 ): Promise<Labelers> { 130 const uris = dids.map(labelerDidToUri); 131 const res = await this.dataplane.records.getRecords(uris); 132 return dids.reduce((acc, did, i) => { 133 const record = parseRecord<LabelerRecord>( 134 so.sprk.labeler.service.main, 135 res.records[i], 136 includeTakedowns, 137 ); 138 return acc.set(did, record ?? null); 139 }, new HydrationMap<Labeler>()); 140 } 141 142 async getLabelerViewerStates( 143 dids: string[], 144 viewer: string, 145 ): Promise<LabelerViewerStates> { 146 const refs = dids.map((did) => ({ uri: labelerDidToUri(did) })); 147 const likes = await this.dataplane.likes.byActorAndSubjects(viewer, refs); 148 return dids.reduce((acc, did, i) => { 149 return acc.set(did, { 150 like: parseString(likes.uris[i]), 151 }); 152 }, new HydrationMap<LabelerViewerState>()); 153 } 154 155 async getLabelerAggregates( 156 dids: string[], 157 _viewer: string | null, 158 ): Promise<LabelerAggs> { 159 const refs = dids.map((did) => ({ uri: labelerDidToUri(did) })); 160 const counts = await this.dataplane.interactions.getInteractionCounts(refs); 161 return dids.reduce((acc, did, i) => { 162 return acc.set(did, { 163 likes: counts.likes[i] ?? 0, 164 }); 165 }, new HydrationMap<LabelerAgg>()); 166 } 167} 168 169const labelerDidToUri = (did: string): string => { 170 return AtUri.make(did, so.sprk.labeler.service.$type, "self").toString(); 171}; 172 173const IMPERSONATION_LABEL = "impersonation"; 174const TAKEDOWN_LABELS = ["!takedown", "!suspend"]; 175const NEEDS_REVIEW_LABEL = "needs-review";