[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.

fix(feed): getActorReposts nuclear block

+62 -5
+62 -5
api/so/sprk/feed/getActorReposts.ts
··· 7 7 HydrateCtx, 8 8 HydrationState, 9 9 Hydrator, 10 + mergeManyStates, 10 11 } from "../../../../hydration/index.ts"; 11 12 import { parseString } from "../../../../hydration/util.ts"; 12 13 import { Server } from "../../../../lex/index.ts"; ··· 52 53 }): Promise<Skeleton> => { 53 54 const { ctx, params } = inputs; 54 55 const { actor, limit, cursor } = params; 55 - const viewer = params.hydrateCtx.viewer; 56 56 if (clearlyBadCursor(cursor)) { 57 - return { items: [] }; 57 + return { actorDid: "", items: [] }; 58 58 } 59 59 const [actorDid] = await ctx.hydrator.actor.getDids([actor]); 60 - if (!actorDid || !viewer || viewer !== actorDid) { 60 + if (!actorDid) { 61 61 throw new InvalidRequestError("Profile not found"); 62 62 } 63 63 ··· 70 70 const items = repostsRes.reposts.map((r) => ({ post: { uri: r.uri } })); 71 71 72 72 return { 73 + actorDid, 73 74 items, 74 75 cursor: parseString(repostsRes.cursor), 75 76 }; ··· 81 82 skeleton: Skeleton; 82 83 }) => { 83 84 const { ctx, params, skeleton } = inputs; 84 - return await ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx); 85 + 86 + // Build map for bidirectional block checking between actor (reposter) and post authors 87 + const postAuthorDids = skeleton.items.map((item) => 88 + creatorFromUri(item.post.uri) 89 + ); 90 + const actorToAuthorsMap = new Map<string, string[]>(); 91 + if (skeleton.actorDid && postAuthorDids.length > 0) { 92 + // Filter out self-reposts (actor reposting their own posts) 93 + const otherAuthorDids = postAuthorDids.filter((did) => 94 + did !== skeleton.actorDid 95 + ); 96 + if (otherAuthorDids.length > 0) { 97 + actorToAuthorsMap.set(skeleton.actorDid, otherAuthorDids); 98 + } 99 + } 100 + 101 + const [feedItemsState, actorViewerState, actorAuthorBlocks] = await Promise 102 + .all([ 103 + ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx), 104 + ctx.hydrator.hydrateProfileViewers( 105 + [skeleton.actorDid], 106 + params.hydrateCtx, 107 + ), 108 + ctx.hydrator.hydrateBidirectionalBlocks(actorToAuthorsMap), 109 + ]); 110 + return mergeManyStates(feedItemsState, actorViewerState, { 111 + bidirectionalBlocks: actorAuthorBlocks, 112 + }); 85 113 }; 86 114 87 115 const noPostBlocks = (inputs: { ··· 90 118 hydration: HydrationState; 91 119 }) => { 92 120 const { ctx, skeleton, hydration } = inputs; 121 + 122 + // Check if viewer is blocking or blocked by the actor (reposter) 123 + const actorRelationship = hydration.profileViewers?.get(skeleton.actorDid); 124 + if (actorRelationship?.blocking) { 125 + throw new InvalidRequestError( 126 + `Requester has blocked actor: ${skeleton.actorDid}`, 127 + "BlockedActor", 128 + ); 129 + } 130 + if (actorRelationship?.blockedBy) { 131 + throw new InvalidRequestError( 132 + `Requester is blocked by actor: ${skeleton.actorDid}`, 133 + "BlockedByActor", 134 + ); 135 + } 136 + 137 + // Filter out posts where: 138 + // - viewer is blocking or blocked by the post author, OR 139 + // - actor (reposter) is blocking or blocked by the post author 140 + const actorBlocks = hydration.bidirectionalBlocks?.get(skeleton.actorDid); 93 141 skeleton.items = skeleton.items.filter((item) => { 94 142 const creator = creatorFromUri(item.post.uri); 95 - return !ctx.views.viewerBlockExists(creator, hydration); 143 + // Check viewer <-> post author blocks 144 + if (ctx.views.viewerBlockExists(creator, hydration)) { 145 + return false; 146 + } 147 + // Check actor (reposter) <-> post author blocks 148 + if (actorBlocks?.get(creator)) { 149 + return false; 150 + } 151 + return true; 96 152 }); 97 153 return skeleton; 98 154 }; ··· 122 178 type Params = QueryParams & { hydrateCtx: HydrateCtx }; 123 179 124 180 type Skeleton = { 181 + actorDid: string; 125 182 items: FeedItem[]; 126 183 cursor?: string; 127 184 };