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

Configure Feed

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

at main 210 lines 5.9 kB view raw
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}