[READ ONLY MIRROR] Spark Social AppView Server
github.com/sprksocial/server
atproto
deno
hono
lexicon
1import { DataPlane } from "../data-plane/index.ts";
2import * as so from "../lex/so.ts";
3import {
4 HydrationMap,
5 parseRecord,
6 parseString,
7 safeTakedownRef,
8} from "./util.ts";
9
10export type ProfileRecord = so.sprk.actor.profile.Main;
11
12export type Actor = {
13 did: string;
14 handle?: string;
15 profile?: ProfileRecord;
16 profileCid?: string;
17 profileTakedownRef?: string;
18 indexedAt?: Date;
19 createdAt?: Date;
20 sortedAt?: Date;
21 takedownRef?: string;
22 upstreamStatus?: string;
23};
24
25export type Actors = HydrationMap<Actor>;
26
27export type ProfileViewerState = {
28 muted?: boolean;
29 blockedBy?: string;
30 blocking?: string;
31 following?: string;
32 followedBy?: string;
33};
34
35export type ProfileViewerStates = HydrationMap<ProfileViewerState>;
36
37type ActivitySubscriptionState = {
38 post: boolean;
39 reply: boolean;
40};
41
42export type ActivitySubscriptionStates = HydrationMap<
43 ActivitySubscriptionState | undefined
44>;
45
46type KnownFollowersState = {
47 count: number;
48 followers: string[];
49};
50
51export type KnownFollowersStates = HydrationMap<
52 KnownFollowersState | undefined
53>;
54
55export type ProfileAgg = {
56 followers: number;
57 follows: number;
58 posts: number;
59 feeds: number;
60};
61
62export type ProfileAggs = HydrationMap<ProfileAgg>;
63
64export class ActorHydrator {
65 constructor(public dataplane: DataPlane) {}
66
67 async getRepoRevSafe(did: string | null): Promise<string | null> {
68 if (!did) return null;
69 try {
70 const res = await this.dataplane.sync.latestRev(did);
71 return parseString(res.rev) ?? null;
72 } catch {
73 return null;
74 }
75 }
76
77 async getDids(
78 handleOrDids: string[],
79 ): Promise<(string | undefined)[]> {
80 const handles = handleOrDids.filter((actor) => !actor.startsWith("did:"));
81 const res = handles.length
82 ? await this.dataplane.actors.getDidsByHandles(handles)
83 : { dids: [] };
84 const didByHandle = handles.reduce(
85 (acc, cur, i) => {
86 const did = res.dids[i];
87 if (did && did.length > 0) {
88 return acc.set(cur, did);
89 }
90 return acc;
91 },
92 new Map() as Map<string, string>,
93 );
94 return handleOrDids.map((id) =>
95 id.startsWith("did:") ? id : didByHandle.get(id)
96 );
97 }
98
99 async getDidsDefined(handleOrDids: string[]): Promise<string[]> {
100 const res = await this.getDids(handleOrDids);
101 return res.filter((did) => did !== undefined);
102 }
103
104 async getActors(
105 dids: string[],
106 opts: {
107 includeTakedowns?: boolean;
108 } = {},
109 ): Promise<Actors> {
110 const { includeTakedowns = false } = opts;
111 if (!dids.length) return new HydrationMap<Actor>();
112 const res = await this.dataplane.actors.getActors(dids);
113 return dids.reduce((acc, did, i) => {
114 const actor = res.actors[i];
115 const isNoHosted = actor.takenDown ||
116 (actor.upstreamStatus && actor.upstreamStatus !== "active");
117 if (
118 !actor.exists ||
119 (isNoHosted && !includeTakedowns) ||
120 !!actor.tombstonedAt
121 ) {
122 return acc.set(did, null);
123 }
124
125 const profile = actor.profile
126 ? parseRecord<ProfileRecord>(
127 so.sprk.actor.profile.main,
128 actor.profile,
129 includeTakedowns,
130 )
131 : undefined;
132
133 return acc.set(did, {
134 did,
135 handle: parseString(actor.handle),
136 profile: profile?.record,
137 profileCid: profile?.cid,
138 sortedAt: profile?.sortedAt ?? new Date(0),
139 profileTakedownRef: profile?.takedownRef,
140 indexedAt: profile?.indexedAt,
141 takedownRef: safeTakedownRef(actor),
142 upstreamStatus: actor.upstreamStatus || undefined,
143 createdAt: new Date(actor.createdAt ?? 0),
144 });
145 }, new HydrationMap<Actor>());
146 }
147
148 // "naive" because this method does not verify the existence of the list itself
149 // a later check in the main hydrator will remove list uris that have been deleted or
150 // repurposed to "curate lists"
151 async getProfileViewerStatesNaive(
152 dids: string[],
153 viewer: string,
154 ): Promise<ProfileViewerStates> {
155 if (!dids.length) return new HydrationMap<ProfileViewerState>();
156 const res = await this.dataplane.relationships.getRelationships(
157 viewer,
158 dids,
159 );
160
161 return dids.reduce((acc, did, i) => {
162 const rels = res.relationships[i];
163 if (viewer === did) {
164 // ignore self-follows, self-mutes, self-blocks, self-activity-subscriptions
165 return acc.set(did, {});
166 }
167 return acc.set(did, {
168 muted: rels.muted ?? false,
169 blockedBy: parseString(rels.blockedBy),
170 blocking: parseString(rels.blocking),
171 following: parseString(rels.following),
172 followedBy: parseString(rels.followedBy),
173 });
174 }, new HydrationMap<ProfileViewerState>());
175 }
176
177 async getKnownFollowers(
178 dids: string[],
179 viewer: string | null,
180 ): Promise<KnownFollowersStates> {
181 if (!viewer) return new HydrationMap<KnownFollowersState | undefined>();
182 const { results: knownFollowersResults } = await this.dataplane
183 .follows.getFollowsFollowing(viewer, dids);
184 return dids.reduce((acc, did, i) => {
185 const result = knownFollowersResults[i]?.dids;
186 return acc.set(
187 did,
188 result && result.length > 0
189 ? {
190 count: result.length,
191 followers: result.slice(0, 5),
192 }
193 : undefined,
194 );
195 }, new HydrationMap<KnownFollowersState | undefined>());
196 }
197
198 async getProfileAggregates(dids: string[]): Promise<ProfileAggs> {
199 if (!dids.length) return new HydrationMap<ProfileAgg>();
200 const counts = await this.dataplane.interactions.getCountsForUsers(dids);
201 return dids.reduce((acc, did, i) => {
202 return acc.set(did, {
203 followers: counts.followers[i] ?? 0,
204 follows: counts.following[i] ?? 0,
205 posts: counts.posts[i] ?? 0,
206 feeds: counts.feeds[i] ?? 0,
207 });
208 }, new HydrationMap<ProfileAgg>());
209 }
210}