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

refactor: pipeline and createHydrateCtx simplification

+575 -551
+11 -11
api/so/sprk/actor/getProfile.ts
··· 7 7 } from "../../../../hydration/index.ts"; 8 8 import { Server } from "../../../../lex/index.ts"; 9 9 import { QueryParams } from "../../../../lex/types/so/sprk/actor/getProfile.ts"; 10 - import { createPipeline, noRules } from "../../../../pipeline.ts"; 10 + import { createPipeline } from "../../../../pipeline.ts"; 11 11 import { Views } from "../../../../views/index.ts"; 12 - import { resHeaders } from "../../../util.ts"; 12 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 13 13 14 14 export default function (server: Server, ctx: AppContext) { 15 - const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 15 + const getProfile = createPipeline({ 16 + skeleton, 17 + hydration, 18 + presentation, 19 + }); 16 20 server.so.sprk.actor.getProfile({ 17 21 auth: ctx.authVerifier.optionalStandardOrRole, 18 22 handler: async ({ auth, params, req }) => { 19 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 - const labelers = ctx.reqLabelers(req); 21 - const hydrateCtx = await ctx.hydrator.createContext({ 22 - labelers, 23 - viewer, 24 - includeTakedowns, 25 - }); 23 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 26 24 const result = await getProfile({ ...params, hydrateCtx }, ctx); 27 - const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 25 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe( 26 + hydrateCtx.viewer, 27 + ); 28 28 29 29 return { 30 30 encoding: "application/json",
+26 -29
api/so/sprk/actor/getProfiles.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { AppContext } from "../../../../context.ts"; 3 - import { 4 - HydrateCtx, 5 - HydrationState, 6 - Hydrator, 7 - } from "../../../../hydration/index.ts"; 2 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 8 3 import { Server } from "../../../../lex/index.ts"; 9 4 import { QueryParams } from "../../../../lex/types/so/sprk/actor/getProfiles.ts"; 10 - import { createPipeline, noRules } from "../../../../pipeline.ts"; 5 + import { 6 + createPipeline, 7 + mapSkeletonList, 8 + type PresentationFnInput, 9 + type SkeletonFnInput, 10 + } from "../../../../pipeline.ts"; 11 11 import { Views } from "../../../../views/index.ts"; 12 - import { resHeaders } from "../../../util.ts"; 12 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 13 13 14 14 export default function (server: Server, ctx: AppContext) { 15 - const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 15 + const getProfile = createPipeline({ 16 + skeleton, 17 + hydration, 18 + presentation, 19 + }); 16 20 server.so.sprk.actor.getProfiles({ 17 21 auth: ctx.authVerifier.standardOptional, 18 22 handler: async ({ auth, params, req }) => { 19 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 - const labelers = ctx.reqLabelers(req); 21 - const hydrateCtx = await ctx.hydrator.createContext({ 22 - viewer, 23 - labelers, 24 - includeTakedowns, 25 - }); 23 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 26 24 27 25 const result = await getProfile({ ...params, hydrateCtx }, ctx); 28 26 29 - const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 27 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe( 28 + hydrateCtx.viewer, 29 + ); 30 30 31 31 return { 32 32 encoding: "application/json", ··· 40 40 }); 41 41 } 42 42 43 - const skeleton = async (input: { 44 - ctx: Context; 45 - params: Params; 46 - }): Promise<SkeletonState> => { 43 + const skeleton = async ( 44 + input: SkeletonFnInput<Context, Params>, 45 + ): Promise<SkeletonState> => { 47 46 const { ctx, params } = input; 48 47 const dids = await ctx.hydrator.actor.getDidsDefined(params.actors); 49 48 return { dids }; ··· 58 57 return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.hydrateCtx); 59 58 }; 60 59 61 - const presentation = (input: { 62 - ctx: Context; 63 - params: Params; 64 - skeleton: SkeletonState; 65 - hydration: HydrationState; 66 - }) => { 60 + const presentation = ( 61 + input: PresentationFnInput<Context, Params, SkeletonState>, 62 + ) => { 67 63 const { ctx, skeleton, hydration } = input; 68 - const profiles = mapDefined( 69 - skeleton.dids, 64 + const profiles = mapSkeletonList( 65 + skeleton, 66 + "dids", 70 67 (did) => ctx.views.profileDetailed(did, hydration), 71 68 ); 72 69 return { profiles };
+13 -15
api/so/sprk/actor/searchActors.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { AppContext } from "../../../../context.ts"; 3 2 import { DataPlane } from "../../../../data-plane/index.ts"; 4 3 import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; ··· 7 6 import { QueryParams } from "../../../../lex/types/so/sprk/actor/searchActors.ts"; 8 7 import { 9 8 createPipeline, 9 + filterSkeletonList, 10 10 HydrationFnInput, 11 + mapSkeletonList, 11 12 PresentationFnInput, 12 13 RulesFnInput, 13 14 SkeletonFnInput, 14 15 } from "../../../../pipeline.ts"; 15 16 import { Views } from "../../../../views/index.ts"; 16 - import { resHeaders } from "../../../util.ts"; 17 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 17 18 18 19 export default function (server: Server, ctx: AppContext) { 19 - const searchActors = createPipeline( 20 + const searchActors = createPipeline({ 20 21 skeleton, 21 22 hydration, 22 - noBlocks, 23 + rules: noBlocks, 23 24 presentation, 24 - ); 25 + }); 25 26 server.so.sprk.actor.searchActors({ 26 27 auth: ctx.authVerifier.standardOptional, 27 28 handler: async ({ auth, params, req }) => { ··· 37 38 }; 38 39 } 39 40 40 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 41 - const hydrateCtx = await ctx.hydrator.createContext({ 42 - viewer, 43 - labelers, 44 - includeTakedowns, 45 - }); 41 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 46 42 const results = await searchActors({ 47 43 ...params, 48 44 q: cleanedQuery, ··· 88 84 89 85 const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 90 86 const { ctx, skeleton, hydration } = inputs; 91 - skeleton.dids = skeleton.dids.filter( 87 + return filterSkeletonList( 88 + skeleton, 89 + "dids", 92 90 (did) => !ctx.views.viewerBlockExists(did, hydration), 93 91 ); 94 - return skeleton; 95 92 }; 96 93 97 94 const presentation = ( 98 95 inputs: PresentationFnInput<Context, Params, Skeleton>, 99 96 ) => { 100 97 const { ctx, skeleton, hydration } = inputs; 101 - const actors = mapDefined( 102 - skeleton.dids, 98 + const actors = mapSkeletonList( 99 + skeleton, 100 + "dids", 103 101 (did) => ctx.views.profile(did, hydration), 104 102 ); 105 103 return {
+13 -15
api/so/sprk/actor/searchActorsTypeahead.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { AppContext } from "../../../../context.ts"; 3 2 import { DataPlane } from "../../../../data-plane/index.ts"; 4 3 import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; ··· 6 5 import { QueryParams } from "../../../../lex/types/so/sprk/actor/searchActorsTypeahead.ts"; 7 6 import { 8 7 createPipeline, 8 + filterSkeletonList, 9 9 HydrationFnInput, 10 + mapSkeletonList, 10 11 PresentationFnInput, 11 12 RulesFnInput, 12 13 SkeletonFnInput, 13 14 } from "../../../../pipeline.ts"; 14 15 import { Views } from "../../../../views/index.ts"; 15 - import { resHeaders } from "../../../util.ts"; 16 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 16 17 17 18 export default function (server: Server, ctx: AppContext) { 18 - const searchActorsTypeahead = createPipeline( 19 + const searchActorsTypeahead = createPipeline({ 19 20 skeleton, 20 21 hydration, 21 - noBlocks, 22 + rules: noBlocks, 22 23 presentation, 23 - ); 24 + }); 24 25 25 26 server.so.sprk.actor.searchActorsTypeahead({ 26 27 auth: ctx.authVerifier.standardOptional, ··· 37 38 }; 38 39 } 39 40 40 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 41 - const hydrateCtx = await ctx.hydrator.createContext({ 42 - viewer, 43 - labelers, 44 - includeTakedowns, 45 - }); 41 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 46 42 47 43 const results = await searchActorsTypeahead({ 48 44 ...params, ··· 88 84 89 85 const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 90 86 const { ctx, skeleton, hydration } = inputs; 91 - skeleton.dids = skeleton.dids.filter( 87 + return filterSkeletonList( 88 + skeleton, 89 + "dids", 92 90 (did) => !ctx.views.viewerBlockExists(did, hydration), 93 91 ); 94 - return skeleton; 95 92 }; 96 93 97 94 const presentation = ( 98 95 inputs: PresentationFnInput<Context, Params, Skeleton>, 99 96 ) => { 100 97 const { ctx, skeleton, hydration } = inputs; 101 - const actors = mapDefined( 102 - skeleton.dids, 98 + const actors = mapSkeletonList( 99 + skeleton, 100 + "dids", 103 101 (did) => ctx.views.profileBasic(did, hydration), 104 102 ); 105 103 return {
+33 -32
api/so/sprk/feed/getActorLikes.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; 5 4 import { FeedItem } from "../../../../hydration/feed.ts"; 6 - import { 7 - HydrateCtx, 8 - HydrationState, 9 - Hydrator, 10 - } from "../../../../hydration/index.ts"; 5 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 11 6 import { parseString } from "../../../../hydration/util.ts"; 12 7 import { Server } from "../../../../lex/index.ts"; 13 8 import { QueryParams } from "../../../../lex/types/so/sprk/feed/getActorLikes.ts"; 14 - import { createPipeline } from "../../../../pipeline.ts"; 9 + import { 10 + createPipeline, 11 + filterSkeletonList, 12 + mapSkeletonList, 13 + type PresentationFnInput, 14 + type RulesFnInput, 15 + } from "../../../../pipeline.ts"; 15 16 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 16 17 import { Views } from "../../../../views/index.ts"; 17 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 18 + import { 19 + clearlyBadCursor, 20 + createHydrateCtxFromAuth, 21 + resHeaders, 22 + } from "../../../util.ts"; 18 23 19 24 export default function (server: Server, ctx: AppContext) { 20 - const getActorLikes = createPipeline( 25 + const getActorLikes = createPipeline({ 21 26 skeleton, 22 27 hydration, 23 - noPostBlocks, 28 + rules: noPostBlocks, 24 29 presentation, 25 - ); 30 + }); 26 31 server.so.sprk.feed.getActorLikes({ 27 32 auth: ctx.authVerifier.standardOptional, 28 33 handler: async ({ params, auth, req }) => { 29 - const viewer = auth.credentials.iss; 30 - const labelers = ctx.reqLabelers(req); 31 - const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer }); 34 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 32 35 33 36 const result = await getActorLikes({ ...params, hydrateCtx }, ctx); 34 37 35 - const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 38 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe( 39 + hydrateCtx.viewer, 40 + ); 36 41 37 42 return { 38 43 encoding: "application/json", ··· 80 85 return await ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx); 81 86 }; 82 87 83 - const noPostBlocks = (inputs: { 84 - ctx: Context; 85 - skeleton: Skeleton; 86 - hydration: HydrationState; 87 - }) => { 88 + const noPostBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 88 89 const { ctx, skeleton, hydration } = inputs; 89 - skeleton.items = skeleton.items.filter((item) => { 90 - const creator = creatorFromUri(item.post.uri); 91 - return !ctx.views.viewerBlockExists(creator, hydration); 92 - }); 93 - return skeleton; 90 + return filterSkeletonList( 91 + skeleton, 92 + "items", 93 + (item) => 94 + !ctx.views.viewerBlockExists(creatorFromUri(item.post.uri), hydration), 95 + ); 94 96 }; 95 97 96 - const presentation = (inputs: { 97 - ctx: Context; 98 - skeleton: Skeleton; 99 - hydration: HydrationState; 100 - }) => { 98 + const presentation = ( 99 + inputs: PresentationFnInput<Context, Params, Skeleton>, 100 + ) => { 101 101 const { ctx, skeleton, hydration } = inputs; 102 - const feed = mapDefined( 103 - skeleton.items, 102 + const feed = mapSkeletonList( 103 + skeleton, 104 + "items", 104 105 (item) => ctx.views.feedViewPost(item, hydration), 105 106 ); 106 107 return {
+27 -27
api/so/sprk/feed/getActorReposts.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; 5 4 import { FeedItem } from "../../../../hydration/feed.ts"; 6 5 import { 7 6 HydrateCtx, 8 - HydrationState, 9 7 Hydrator, 10 8 mergeManyStates, 11 9 } from "../../../../hydration/index.ts"; 12 10 import { parseString } from "../../../../hydration/util.ts"; 13 11 import { Server } from "../../../../lex/index.ts"; 14 12 import { QueryParams } from "../../../../lex/types/so/sprk/feed/getActorLikes.ts"; 15 - import { createPipeline } from "../../../../pipeline.ts"; 13 + import { 14 + createPipeline, 15 + filterSkeletonList, 16 + mapSkeletonList, 17 + type PresentationFnInput, 18 + type RulesFnInput, 19 + } from "../../../../pipeline.ts"; 16 20 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 17 21 import { Views } from "../../../../views/index.ts"; 18 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 22 + import { 23 + clearlyBadCursor, 24 + createHydrateCtxFromAuth, 25 + resHeaders, 26 + } from "../../../util.ts"; 19 27 20 28 export default function (server: Server, ctx: AppContext) { 21 - const getActorReposts = createPipeline( 29 + const getActorReposts = createPipeline({ 22 30 skeleton, 23 31 hydration, 24 - noPostBlocks, 32 + rules: noPostBlocks, 25 33 presentation, 26 - ); 34 + }); 27 35 server.so.sprk.feed.getActorReposts({ 28 36 auth: ctx.authVerifier.standardOptional, 29 37 handler: async ({ params, auth, req }) => { 30 - const viewer = auth.credentials.iss; 31 - const labelers = ctx.reqLabelers(req); 32 - const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer }); 38 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 33 39 34 40 const result = await getActorReposts({ ...params, hydrateCtx }, ctx); 35 41 36 - const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 42 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe( 43 + hydrateCtx.viewer, 44 + ); 37 45 38 46 return { 39 47 encoding: "application/json", ··· 112 120 }); 113 121 }; 114 122 115 - const noPostBlocks = (inputs: { 116 - ctx: Context; 117 - skeleton: Skeleton; 118 - hydration: HydrationState; 119 - }) => { 123 + const noPostBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 120 124 const { ctx, skeleton, hydration } = inputs; 121 125 122 126 // Check if viewer is blocking or blocked by the actor (reposter) ··· 138 142 // - viewer is blocking or blocked by the post author, OR 139 143 // - actor (reposter) is blocking or blocked by the post author 140 144 const actorBlocks = hydration.bidirectionalBlocks?.get(skeleton.actorDid); 141 - skeleton.items = skeleton.items.filter((item) => { 145 + return filterSkeletonList(skeleton, "items", (item) => { 142 146 const creator = creatorFromUri(item.post.uri); 143 - // Check viewer <-> post author blocks 144 147 if (ctx.views.viewerBlockExists(creator, hydration)) { 145 148 return false; 146 149 } 147 - // Check actor (reposter) <-> post author blocks 148 150 if (actorBlocks?.get(creator)) { 149 151 return false; 150 152 } 151 153 return true; 152 154 }); 153 - return skeleton; 154 155 }; 155 156 156 - const presentation = (inputs: { 157 - ctx: Context; 158 - skeleton: Skeleton; 159 - hydration: HydrationState; 160 - }) => { 157 + const presentation = ( 158 + inputs: PresentationFnInput<Context, Params, Skeleton>, 159 + ) => { 161 160 const { ctx, skeleton, hydration } = inputs; 162 - const feed = mapDefined( 163 - skeleton.items, 161 + const feed = mapSkeletonList( 162 + skeleton, 163 + "items", 164 164 (item) => ctx.views.feedViewPost(item, hydration), 165 165 ); 166 166 return {
+19 -19
api/so/sprk/feed/getAuthorFeed.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; ··· 13 12 import { parseString } from "../../../../hydration/util.ts"; 14 13 import { Server } from "../../../../lex/index.ts"; 15 14 import { QueryParams } from "../../../../lex/types/so/sprk/feed/getAuthorFeed.ts"; 16 - import { createPipeline } from "../../../../pipeline.ts"; 15 + import { 16 + createPipeline, 17 + filterSkeletonList, 18 + mapSkeletonList, 19 + } from "../../../../pipeline.ts"; 17 20 import { safePinnedPost, uriToDid } from "../../../../utils/uris.ts"; 18 21 import { Views } from "../../../../views/index.ts"; 19 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 22 + import { 23 + clearlyBadCursor, 24 + createHydrateCtxFromAuth, 25 + resHeaders, 26 + } from "../../../util.ts"; 20 27 21 28 export default function (server: Server, ctx: AppContext) { 22 - const getAuthorFeed = createPipeline( 29 + const getAuthorFeed = createPipeline({ 23 30 skeleton, 24 31 hydration, 25 - noBlocksOrMutes, 32 + rules: noBlocksOrMutes, 26 33 presentation, 27 - ); 34 + }); 28 35 server.so.sprk.feed.getAuthorFeed({ 29 36 auth: ctx.authVerifier.optionalStandardOrRole, 30 37 handler: async ({ params, auth, req }) => { 31 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 32 - const labelers = ctx.reqLabelers(req); 33 - const hydrateCtx = await ctx.hydrator.createContext({ 34 - labelers, 35 - viewer, 36 - includeTakedowns, 37 - }); 38 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 38 39 39 40 // Parallelize pipeline execution with repoRev fetch 40 41 const [result, repoRev] = await Promise.all([ 41 42 getAuthorFeed({ ...params, hydrateCtx }, ctx), 42 - ctx.hydrator.actor.getRepoRevSafe(viewer), 43 + ctx.hydrator.actor.getRepoRevSafe(hydrateCtx.viewer), 43 44 ]); 44 45 45 46 return { ··· 157 158 ); 158 159 }; 159 160 160 - skeleton.items = skeleton.items.filter(checkBlocksAndMutes); 161 - 162 - return skeleton; 161 + return filterSkeletonList(skeleton, "items", checkBlocksAndMutes); 163 162 }; 164 163 165 164 const presentation = (inputs: { ··· 168 167 hydration: HydrationState; 169 168 }) => { 170 169 const { ctx, skeleton, hydration } = inputs; 171 - const feed = mapDefined( 172 - skeleton.items, 170 + const feed = mapSkeletonList( 171 + skeleton, 172 + "items", 173 173 (item) => ctx.views.feedViewPost(item, hydration), 174 174 ); 175 175 return { feed, cursor: skeleton.cursor };
+11 -18
api/so/sprk/feed/getCrosspostThread.ts
··· 20 20 import { createPipeline } from "../../../../pipeline.ts"; 21 21 import { uriToDid } from "../../../../utils/uris.ts"; 22 22 import { Views } from "../../../../views/index.ts"; 23 - import { ATPROTO_REPO_REV, resHeaders } from "../../../util.ts"; 23 + import { 24 + ATPROTO_REPO_REV, 25 + createHydrateCtxFromAuth, 26 + resHeaders, 27 + } from "../../../util.ts"; 24 28 25 29 export default function (server: Server, ctx: AppContext) { 26 - const getCrosspostThread = createPipeline( 30 + const getCrosspostThread = createPipeline({ 27 31 skeleton, 28 32 hydration, 29 - noRules, 30 33 presentation, 31 - ); 34 + }); 32 35 33 36 server.so.sprk.feed.getCrosspostThread({ 34 37 auth: ctx.authVerifier.optionalStandardOrRole, 35 38 handler: async ({ params, auth, req, res }) => { 36 - const { viewer, includeTakedowns, include3pBlocks } = ctx.authVerifier 37 - .parseCreds(auth); 38 - const labelers = ctx.reqLabelers(req); 39 - const hydrateCtx = await ctx.hydrator.createContext({ 40 - labelers, 41 - viewer, 42 - includeTakedowns, 43 - include3pBlocks, 44 - }); 39 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 45 40 46 - const repoRevPromise = ctx.hydrator.actor.getRepoRevSafe(viewer); 41 + const repoRevPromise = ctx.hydrator.actor.getRepoRevSafe( 42 + hydrateCtx.viewer, 43 + ); 47 44 let result: OutputSchema; 48 45 try { 49 46 result = await getCrosspostThread({ ...params, hydrateCtx }, ctx); ··· 128 125 ...profileState, 129 126 postBlocks, 130 127 }; 131 - }; 132 - 133 - const noRules = (inputs: { skeleton: Skeleton }) => { 134 - return inputs.skeleton; 135 128 }; 136 129 137 130 const presentation = (
+15 -13
api/so/sprk/feed/getFeed.ts
··· 1 - import { mapDefined, noUndefinedVals } from "@atp/common"; 1 + import { noUndefinedVals } from "@atp/common"; 2 2 import { ResponseType, XrpcClient, XRPCError } from "@atp/xrpc"; 3 3 import { 4 4 InvalidRequestError, ··· 20 20 import { OutputSchema as SkeletonOutput } from "../../../../lex/types/so/sprk/feed/getFeedSkeleton.ts"; 21 21 import { 22 22 createPipeline, 23 + filterSkeletonList, 23 24 HydrationFnInput, 25 + mapSkeletonList, 24 26 PresentationFnInput, 25 27 RulesFnInput, 26 28 SkeletonFnInput, 27 29 } from "../../../../pipeline.ts"; 28 - import { resHeaders, SPRK_USER_AGENT } from "../../../util.ts"; 30 + import { 31 + createHydrateCtxFromAuth, 32 + resHeaders, 33 + SPRK_USER_AGENT, 34 + } from "../../../util.ts"; 29 35 30 36 type GetIdentityByDidResponse = { 31 37 did: string; ··· 36 42 }; 37 43 38 44 export default function (server: Server, ctx: AppContext) { 39 - const getFeed = createPipeline( 45 + const getFeed = createPipeline({ 40 46 skeleton, 41 47 hydration, 42 - noBlocksOrMutes, 48 + rules: noBlocksOrMutes, 43 49 presentation, 44 - ); 50 + }); 45 51 server.so.sprk.feed.getFeed({ 46 52 auth: ctx.authVerifier.standardOptionalParameterized({ 47 53 lxmCheck: (method) => { ··· 53 59 skipAudCheck: true, 54 60 }), 55 61 handler: async ({ params, auth, req }) => { 56 - const viewer = auth.credentials.iss; 57 - const labelers = ctx.reqLabelers(req); 58 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 62 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 59 63 const headers = noUndefinedVals({ 60 64 "user-agent": SPRK_USER_AGENT, 61 65 authorization: req.headers.get("authorization") as string, ··· 75 79 body: result, 76 80 headers: { 77 81 ...(feedResHeaders ?? {}), 78 - ...resHeaders({}), 82 + ...resHeaders({ labelers: hydrateCtx.labelers }), 79 83 "server-timing": serverTimingHeader([timerSkele, timerHydr]), 80 84 }, 81 85 }; ··· 122 126 123 127 const noBlocksOrMutes = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 124 128 const { ctx, skeleton, hydration } = inputs; 125 - skeleton.items = skeleton.items.filter((item) => { 129 + return filterSkeletonList(skeleton, "items", (item) => { 126 130 const bam = ctx.views.feedItemBlocksAndMutes(item, hydration); 127 131 return ( 128 132 !bam.authorBlocked && 129 133 !bam.authorMuted 130 134 ); 131 135 }); 132 - 133 - return skeleton; 134 136 }; 135 137 136 138 const presentation = ( 137 139 inputs: PresentationFnInput<Context, Params, Skeleton>, 138 140 ) => { 139 141 const { ctx, skeleton, hydration } = inputs; 140 - const feed = mapDefined(skeleton.items, (item) => { 142 + const feed = mapSkeletonList(skeleton, "items", (item) => { 141 143 const post = ctx.views.feedViewPost(item, hydration); 142 144 if (!post) return; 143 145 return {
+4 -9
api/so/sprk/feed/getFeedGenerator.ts
··· 1 1 import { AppContext } from "../../../../context.ts"; 2 2 import { Server } from "../../../../lex/index.ts"; 3 - import { resHeaders } from "../../../util.ts"; 3 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 4 4 5 5 export default function (server: Server, ctx: AppContext) { 6 6 server.so.sprk.feed.getFeedGenerator({ 7 7 auth: ctx.authVerifier.optionalStandardOrRole, 8 8 handler: async ({ params, auth, req }) => { 9 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 - const labelers = ctx.reqLabelers(req); 11 - const hydrateCtx = await ctx.hydrator.createContext({ 12 - labelers, 13 - viewer, 14 - includeTakedowns, 15 - }); 9 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 16 10 17 11 // Parallelize hydration with repoRev fetch 18 12 const [hydrationState, repoRev] = await Promise.all([ 19 13 ctx.hydrator.hydrateFeedGens([params.feed], hydrateCtx), 20 - ctx.hydrator.actor.getRepoRevSafe(viewer), 14 + ctx.hydrator.actor.getRepoRevSafe(hydrateCtx.viewer), 21 15 ]); 22 16 23 17 // Create generator view ··· 41 35 }, 42 36 headers: resHeaders({ 43 37 repoRev, 38 + labelers: hydrateCtx.labelers, 44 39 }), 45 40 }; 46 41 },
+4 -9
api/so/sprk/feed/getFeedGenerators.ts
··· 1 1 import { AppContext } from "../../../../context.ts"; 2 2 import { Server } from "../../../../lex/index.ts"; 3 - import { resHeaders } from "../../../util.ts"; 3 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 4 4 5 5 export default function (server: Server, ctx: AppContext) { 6 6 server.so.sprk.feed.getFeedGenerators({ 7 7 auth: ctx.authVerifier.optionalStandardOrRole, 8 8 handler: async ({ params, auth, req }) => { 9 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 10 - const labelers = ctx.reqLabelers(req); 11 - const hydrateCtx = await ctx.hydrator.createContext({ 12 - labelers, 13 - viewer, 14 - includeTakedowns, 15 - }); 9 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 16 10 17 11 // Parallelize hydration with repoRev fetch 18 12 const [hydrationState, repoRev] = await Promise.all([ 19 13 ctx.hydrator.hydrateFeedGens(params.feeds, hydrateCtx), 20 - ctx.hydrator.actor.getRepoRevSafe(viewer), 14 + ctx.hydrator.actor.getRepoRevSafe(hydrateCtx.viewer), 21 15 ]); 22 16 23 17 // Create generator views ··· 30 24 body: { feeds }, 31 25 headers: resHeaders({ 32 26 repoRev, 27 + labelers: hydrateCtx.labelers, 33 28 }), 34 29 }; 35 30 },
+11 -15
api/so/sprk/feed/getPostThread.ts
··· 13 13 import { 14 14 createPipeline, 15 15 HydrationFnInput, 16 - noRules, 17 16 PresentationFnInput, 18 17 SkeletonFnInput, 19 18 } from "../../../../pipeline.ts"; 20 19 import { Views } from "../../../../views/index.ts"; 21 - import { ATPROTO_REPO_REV, resHeaders } from "../../../util.ts"; 20 + import { 21 + ATPROTO_REPO_REV, 22 + createHydrateCtxFromAuth, 23 + resHeaders, 24 + } from "../../../util.ts"; 22 25 23 26 export default function (server: Server, ctx: AppContext) { 24 - const getPostThread = createPipeline( 27 + const getPostThread = createPipeline({ 25 28 skeleton, 26 29 hydration, 27 - noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost. 28 30 presentation, 29 - ); 31 + }); 30 32 server.so.sprk.feed.getPostThread({ 31 33 auth: ctx.authVerifier.optionalStandardOrRole, 32 34 handler: async ({ params, auth, req, res }) => { 33 - const { viewer, includeTakedowns, include3pBlocks } = ctx.authVerifier 34 - .parseCreds(auth); 35 - const labelers = ctx.reqLabelers(req); 36 - const hydrateCtx = await ctx.hydrator.createContext({ 37 - labelers, 38 - viewer, 39 - includeTakedowns, 40 - include3pBlocks, 41 - }); 35 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 42 36 43 37 // Start repoRev fetch early so it runs in parallel with the pipeline 44 - const repoRevPromise = ctx.hydrator.actor.getRepoRevSafe(viewer); 38 + const repoRevPromise = ctx.hydrator.actor.getRepoRevSafe( 39 + hydrateCtx.viewer, 40 + ); 45 41 46 42 let result: OutputSchema; 47 43 try {
+29 -30
api/so/sprk/feed/getPosts.ts
··· 1 - import { dedupeStrs, mapDefined } from "@atp/common"; 1 + import { dedupeStrs } from "@atp/common"; 2 2 import { AppContext } from "../../../../context.ts"; 3 - import { 4 - HydrateCtx, 5 - HydrationState, 6 - Hydrator, 7 - } from "../../../../hydration/index.ts"; 3 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 8 4 import { Server } from "../../../../lex/index.ts"; 9 5 import { QueryParams } from "../../../../lex/types/so/sprk/feed/getPosts.ts"; 10 - import { createPipeline } from "../../../../pipeline.ts"; 6 + import { 7 + createPipeline, 8 + filterSkeletonList, 9 + mapSkeletonList, 10 + type PresentationFnInput, 11 + type RulesFnInput, 12 + } from "../../../../pipeline.ts"; 11 13 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 12 14 import { Views } from "../../../../views/index.ts"; 13 - import { resHeaders } from "../../../util.ts"; 15 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 14 16 15 17 export default function (server: Server, ctx: AppContext) { 16 - const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation); 18 + const getPosts = createPipeline({ 19 + skeleton, 20 + hydration, 21 + rules: noBlocks, 22 + presentation, 23 + }); 17 24 server.so.sprk.feed.getPosts({ 18 25 auth: ctx.authVerifier.standardOptional, 19 26 handler: async ({ params, auth, req }) => { 20 - const viewer = auth.credentials.iss; 21 - const labelers = ctx.reqLabelers(req); 22 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 27 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 23 28 24 29 const results = await getPosts({ ...params, hydrateCtx }, ctx); 25 30 ··· 50 55 ); 51 56 }; 52 57 53 - const noBlocks = (inputs: { 54 - ctx: Context; 55 - skeleton: Skeleton; 56 - hydration: HydrationState; 57 - }) => { 58 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 58 59 const { ctx, skeleton, hydration } = inputs; 59 - skeleton.posts = skeleton.posts.filter((uri) => { 60 - const creator = creatorFromUri(uri); 61 - return !ctx.views.viewerBlockExists(creator, hydration); 62 - }); 63 - return skeleton; 60 + return filterSkeletonList( 61 + skeleton, 62 + "posts", 63 + (uri) => !ctx.views.viewerBlockExists(creatorFromUri(uri), hydration), 64 + ); 64 65 }; 65 66 66 - const presentation = (inputs: { 67 - ctx: Context; 68 - params: Params; 69 - skeleton: Skeleton; 70 - hydration: HydrationState; 71 - }) => { 67 + const presentation = ( 68 + inputs: PresentationFnInput<Context, Params, Skeleton>, 69 + ) => { 72 70 const { ctx, skeleton, hydration } = inputs; 73 - const posts = mapDefined( 74 - skeleton.posts, 71 + const posts = mapSkeletonList( 72 + skeleton, 73 + "posts", 75 74 (uri) => ctx.views.post(uri, hydration), 76 75 ); 77 76 return { posts };
+3 -6
api/so/sprk/feed/getSuggestedFeeds.ts
··· 2 2 import { AppContext } from "../../../../context.ts"; 3 3 import { parseString } from "../../../../hydration/util.ts"; 4 4 import { Server } from "../../../../lex/index.ts"; 5 - import { resHeaders } from "../../../util.ts"; 5 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 6 6 7 7 export default function (server: Server, ctx: AppContext) { 8 8 server.so.sprk.feed.getSuggestedFeeds({ 9 9 auth: ctx.authVerifier.standardOptional, 10 10 handler: async ({ auth, params, req }) => { 11 - const viewer = auth.credentials.iss; 12 - 13 11 // @NOTE no need to coordinate the cursor for appview swap, as v1 doesn't use the cursor 14 12 const suggestedRes = await ctx.dataplane.feedGens.getSuggestedFeeds( 15 13 params.limit, 16 14 params.cursor, 17 15 ); 18 16 const uris = suggestedRes.uris; 19 - const labelers = ctx.reqLabelers(req); 20 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 17 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 21 18 const hydration = await ctx.hydrator.hydrateFeedGens(uris, hydrateCtx); 22 19 const feedViews = mapDefined( 23 20 uris, ··· 30 27 feeds: feedViews, 31 28 cursor: parseString(suggestedRes.cursor), 32 29 }, 33 - headers: resHeaders({}), 30 + headers: resHeaders({ labelers: hydrateCtx.labelers }), 34 31 }; 35 32 }, 36 33 });
+23 -12
api/so/sprk/feed/getTimeline.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { AppContext } from "../../../../context.ts"; 3 2 import { DataPlane } from "../../../../data-plane/index.ts"; 4 3 import { FeedItem } from "../../../../hydration/feed.ts"; ··· 10 9 import { parseString } from "../../../../hydration/util.ts"; 11 10 import { Server } from "../../../../lex/index.ts"; 12 11 import { QueryParams } from "../../../../lex/types/so/sprk/feed/getTimeline.ts"; 13 - import { createPipeline } from "../../../../pipeline.ts"; 12 + import { 13 + createPipeline, 14 + filterSkeletonList, 15 + mapSkeletonList, 16 + } from "../../../../pipeline.ts"; 14 17 import { Views } from "../../../../views/index.ts"; 15 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 18 + import { 19 + clearlyBadCursor, 20 + createHydrateCtxFromAuth, 21 + resHeaders, 22 + } from "../../../util.ts"; 16 23 17 24 export default function (server: Server, ctx: AppContext) { 18 - const getTimeline = createPipeline( 25 + const getTimeline = createPipeline({ 19 26 skeleton, 20 27 hydration, 21 - noBlocksOrMutes, 28 + rules: noBlocksOrMutes, 22 29 presentation, 23 - ); 30 + }); 24 31 server.so.sprk.feed.getTimeline({ 25 32 auth: ctx.authVerifier.standard, 26 33 handler: async ({ params, auth, req }) => { 27 34 const viewer = auth.credentials.iss; 28 - const labelers = ctx.reqLabelers(req); 29 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 35 + const hydrateCtx = await createHydrateCtxFromAuth( 36 + ctx, 37 + req, 38 + auth, 39 + { viewer }, 40 + ); 30 41 31 42 // Parallelize pipeline execution with repoRev fetch 32 43 const [result, repoRev] = await Promise.all([ ··· 86 97 hydration: HydrationState; 87 98 }): Skeleton => { 88 99 const { ctx, skeleton, hydration } = inputs; 89 - skeleton.items = skeleton.items.filter((item) => { 100 + return filterSkeletonList(skeleton, "items", (item) => { 90 101 const bam = ctx.views.feedItemBlocksAndMutes(item, hydration); 91 102 return !bam.authorBlocked && 92 103 !bam.authorMuted; 93 104 }); 94 - return skeleton; 95 105 }; 96 106 97 107 const presentation = (inputs: { ··· 100 110 hydration: HydrationState; 101 111 }) => { 102 112 const { ctx, skeleton, hydration } = inputs; 103 - const feed = mapDefined( 104 - skeleton.items, 113 + const feed = mapSkeletonList( 114 + skeleton, 115 + "items", 105 116 (item) => ctx.views.feedViewPost(item, hydration), 106 117 ); 107 118 return { feed, cursor: skeleton.cursor };
+10 -20
api/so/sprk/feed/searchPosts.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { ServerConfig } from "../../../../config.ts"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; 5 - import { 6 - parsePostSearchQuery, 7 - PostSearchQuery, 8 - } from "../../../../data-plane/util.ts"; 9 4 import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 10 5 import { parseString } from "../../../../hydration/util.ts"; 11 6 import { Server } from "../../../../lex/index.ts"; 12 7 import { QueryParams } from "../../../../lex/types/so/sprk/feed/searchPosts.ts"; 13 8 import { 14 9 createPipeline, 10 + filterSkeletonList, 15 11 HydrationFnInput, 12 + mapSkeletonList, 16 13 PresentationFnInput, 17 14 RulesFnInput, 18 15 SkeletonFnInput, 19 16 } from "../../../../pipeline.ts"; 20 17 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 21 18 import { Views } from "../../../../views/index.ts"; 22 - import { resHeaders } from "../../../util.ts"; 19 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 23 20 24 21 export default function (server: Server, ctx: AppContext) { 25 - const searchPosts = createPipeline( 22 + const searchPosts = createPipeline({ 26 23 skeleton, 27 24 hydration, 28 - noBlocksOrTagged, 25 + rules: noBlocksOrTagged, 29 26 presentation, 30 - ); 27 + }); 31 28 server.so.sprk.feed.searchPosts({ 32 29 auth: ctx.authVerifier.standardOptional, 33 30 handler: async ({ auth, params, req }) => { 34 - const { viewer } = ctx.authVerifier.parseCreds(auth); 35 - const labelers = ctx.reqLabelers(req); 36 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 31 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 37 32 const results = await searchPosts( 38 33 { ...params, hydrateCtx }, 39 34 ctx, ··· 51 46 52 47 const skeleton = async (inputs: SkeletonFnInput<Context, Params>) => { 53 48 const { ctx, params } = inputs; 54 - const parsedQuery = parsePostSearchQuery(params.q); 55 - 56 49 const res = await ctx.dataplane.search.posts( 57 50 params.q, 58 51 params.limit, ··· 61 54 return { 62 55 posts: res.uris, 63 56 cursor: parseString(res.cursor), 64 - parsedQuery, 65 57 }; 66 58 }; 67 59 ··· 79 71 const noBlocksOrTagged = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 80 72 const { ctx, params, skeleton, hydration } = inputs; 81 73 82 - skeleton.posts = skeleton.posts.filter((uri) => { 74 + return filterSkeletonList(skeleton, "posts", (uri) => { 83 75 const post = hydration.posts?.get(uri); 84 - if (!post) return; 76 + if (!post) return false; 85 77 86 78 const creator = creatorFromUri(uri); 87 79 const isPostByViewer = creator === params.hydrateCtx.viewer; ··· 93 85 if (ctx.views.viewerBlockExists(creator, hydration)) return false; 94 86 return true; 95 87 }); 96 - return skeleton; 97 88 }; 98 89 99 90 const presentation = ( 100 91 inputs: PresentationFnInput<Context, Params, Skeleton>, 101 92 ) => { 102 93 const { ctx, skeleton, hydration } = inputs; 103 - const posts = mapDefined(skeleton.posts, (uri) => { 94 + const posts = mapSkeletonList(skeleton, "posts", (uri) => { 104 95 const post = hydration.posts?.get(uri); 105 96 if (!post) return; 106 97 ··· 128 119 posts: string[]; 129 120 hitsTotal?: number; 130 121 cursor?: string; 131 - parsedQuery: PostSearchQuery; 132 122 };
+23 -9
api/so/sprk/graph/getBlocks.ts
··· 6 6 import { 7 7 createPipeline, 8 8 HydrationFnInput, 9 - noRules, 9 + mapSkeletonList, 10 10 PresentationFnInput, 11 11 SkeletonFnInput, 12 12 } from "../../../../pipeline.ts"; 13 13 import { Views } from "../../../../views/index.ts"; 14 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 14 + import { 15 + clearlyBadCursor, 16 + createHydrateCtxFromAuth, 17 + resHeaders, 18 + } from "../../../util.ts"; 15 19 16 20 export default function (server: Server, ctx: AppContext) { 17 - const getBlocks = createPipeline(skeleton, hydration, noRules, presentation); 21 + const getBlocks = createPipeline({ 22 + skeleton, 23 + hydration, 24 + presentation, 25 + }); 18 26 server.so.sprk.graph.getBlocks({ 19 27 auth: ctx.authVerifier.standard, 20 28 handler: async ({ params, auth, req }) => { 21 29 const viewer = auth.credentials.iss; 22 - const labelers = ctx.reqLabelers(req); 23 - const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer }); 30 + const hydrateCtx = await createHydrateCtxFromAuth( 31 + ctx, 32 + req, 33 + auth, 34 + { viewer }, 35 + ); 24 36 const result = await getBlocks( 25 37 { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) }, 26 38 ctx, ··· 66 78 input: PresentationFnInput<Context, Params, SkeletonState>, 67 79 ) => { 68 80 const { ctx, hydration, skeleton } = input; 69 - const { blockedDids, cursor } = skeleton; 70 - const blocks = mapDefined(blockedDids, (did) => { 71 - return ctx.views.profile(did, hydration); 72 - }); 81 + const { cursor } = skeleton; 82 + const blocks = mapSkeletonList( 83 + skeleton, 84 + "blockedDids", 85 + (did) => ctx.views.profile(did, hydration), 86 + ); 73 87 return { blocks, cursor }; 74 88 }; 75 89
+14 -16
api/so/sprk/graph/getFollowers.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { ··· 10 9 import { QueryParams } from "../../../../lex/types/so/sprk/graph/getFollowers.ts"; 11 10 import { 12 11 createPipeline, 12 + filterSkeletonList, 13 13 HydrationFnInput, 14 + mapSkeletonList, 14 15 PresentationFnInput, 15 16 RulesFnInput, 16 17 SkeletonFnInput, 17 18 } from "../../../../pipeline.ts"; 18 19 import { uriToDid as didFromUri } from "../../../../utils/uris.ts"; 19 20 import { Views } from "../../../../views/index.ts"; 20 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 21 + import { 22 + clearlyBadCursor, 23 + createHydrateCtxFromAuth, 24 + resHeaders, 25 + } from "../../../util.ts"; 21 26 22 27 export default function (server: Server, ctx: AppContext) { 23 - const getFollowers = createPipeline( 28 + const getFollowers = createPipeline({ 24 29 skeleton, 25 30 hydration, 26 - noBlocks, 31 + rules: noBlocks, 27 32 presentation, 28 - ); 33 + }); 29 34 server.so.sprk.graph.getFollowers({ 30 35 auth: ctx.authVerifier.optionalStandardOrRole, 31 36 handler: async ({ params, auth, req }) => { 32 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 33 - const labelers = ctx.reqLabelers(req); 34 - const hydrateCtx = await ctx.hydrator.createContext({ 35 - labelers, 36 - viewer, 37 - includeTakedowns, 38 - }); 37 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 39 38 40 39 const result = await getFollowers({ ...params, hydrateCtx }, ctx); 41 40 ··· 89 88 const noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => { 90 89 const { skeleton, params, hydration, ctx } = input; 91 90 const viewer = params.hydrateCtx.viewer; 92 - skeleton.followUris = skeleton.followUris.filter((followUri) => { 91 + return filterSkeletonList(skeleton, "followUris", (followUri) => { 93 92 const followerDid = didFromUri(followUri); 94 93 return ( 95 94 !hydration.followBlocks?.get(followUri) && 96 95 (!viewer || !ctx.views.viewerBlockExists(followerDid, hydration)) 97 96 ); 98 97 }); 99 - return skeleton; 100 98 }; 101 99 102 100 const presentation = ( 103 101 input: PresentationFnInput<Context, Params, SkeletonState>, 104 102 ) => { 105 103 const { ctx, hydration, skeleton, params } = input; 106 - const { subjectDid, followUris, cursor } = skeleton; 104 + const { subjectDid, cursor } = skeleton; 107 105 const isNoHosted = (did: string) => ctx.views.actorIsNoHosted(did, hydration); 108 106 109 107 const subject = ctx.views.profile(subjectDid, hydration); ··· 114 112 throw new InvalidRequestError(`Actor not found: ${params.actor}`); 115 113 } 116 114 117 - const followers = mapDefined(followUris, (followUri) => { 115 + const followers = mapSkeletonList(skeleton, "followUris", (followUri) => { 118 116 const followerDid = didFromUri(followUri); 119 117 if (!params.hydrateCtx.includeTakedowns && isNoHosted(followerDid)) { 120 118 return;
+14 -16
api/so/sprk/graph/getFollows.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { ··· 10 9 import { QueryParams } from "../../../../lex/types/so/sprk/graph/getFollowers.ts"; 11 10 import { 12 11 createPipeline, 12 + filterSkeletonList, 13 13 HydrationFnInput, 14 + mapSkeletonList, 14 15 PresentationFnInput, 15 16 RulesFnInput, 16 17 SkeletonFnInput, 17 18 } from "../../../../pipeline.ts"; 18 19 import { Views } from "../../../../views/index.ts"; 19 - import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 20 + import { 21 + clearlyBadCursor, 22 + createHydrateCtxFromAuth, 23 + resHeaders, 24 + } from "../../../util.ts"; 20 25 21 26 export default function (server: Server, ctx: AppContext) { 22 - const getFollows = createPipeline( 27 + const getFollows = createPipeline({ 23 28 skeleton, 24 29 hydration, 25 - noBlocks, 30 + rules: noBlocks, 26 31 presentation, 27 - ); 32 + }); 28 33 server.so.sprk.graph.getFollows({ 29 34 auth: ctx.authVerifier.optionalStandardOrRole, 30 35 handler: async ({ params, auth, req }) => { 31 - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 32 - const labelers = ctx.reqLabelers(req); 33 - const hydrateCtx = await ctx.hydrator.createContext({ 34 - labelers, 35 - viewer, 36 - includeTakedowns, 37 - }); 36 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 38 37 39 38 // @TODO ensure canViewTakedowns gets threaded through and applied properly 40 39 const result = await getFollows({ ...params, hydrateCtx }, ctx); ··· 96 95 const noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => { 97 96 const { skeleton, params, hydration, ctx } = input; 98 97 const viewer = params.hydrateCtx.viewer; 99 - skeleton.followUris = skeleton.followUris.filter((followUri) => { 98 + return filterSkeletonList(skeleton, "followUris", (followUri) => { 100 99 const follow = hydration.follows?.get(followUri); 101 100 if (!follow) return false; 102 101 return ( ··· 105 104 !ctx.views.viewerBlockExists(follow.record.subject, hydration)) 106 105 ); 107 106 }); 108 - return skeleton; 109 107 }; 110 108 111 109 const presentation = ( 112 110 input: PresentationFnInput<Context, Params, SkeletonState>, 113 111 ) => { 114 112 const { ctx, hydration, skeleton, params } = input; 115 - const { subjectDid, followUris, cursor } = skeleton; 113 + const { subjectDid, cursor } = skeleton; 116 114 const isNoHosted = (did: string) => ctx.views.actorIsNoHosted(did, hydration); 117 115 118 116 const subject = ctx.views.profile(subjectDid, hydration); ··· 123 121 throw new InvalidRequestError(`Actor not found: ${params.actor}`); 124 122 } 125 123 126 - const follows = mapDefined(followUris, (followUri) => { 124 + const follows = mapSkeletonList(skeleton, "followUris", (followUri) => { 127 125 const followDid = hydration.follows?.get(followUri)?.record.subject; 128 126 if (!followDid) return; 129 127 if (!params.hydrateCtx.includeTakedowns && isNoHosted(followDid)) {
+2 -7
api/so/sprk/labeler/getServices.ts
··· 1 1 import { mapDefined } from "@atp/common"; 2 2 import { AppContext } from "../../../../context.ts"; 3 3 import { Server } from "../../../../lex/index.ts"; 4 - import { resHeaders } from "../../../util.ts"; 4 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 5 5 6 6 export default function (server: Server, ctx: AppContext) { 7 7 server.so.sprk.labeler.getServices({ 8 8 auth: ctx.authVerifier.standardOptional, 9 9 handler: async ({ params, auth, req }) => { 10 10 const { dids, detailed } = params; 11 - const viewer = auth.credentials.iss; 12 - const labelers = ctx.reqLabelers(req); 13 - const hydrateCtx = await ctx.hydrator.createContext({ 14 - viewer, 15 - labelers, 16 - }); 11 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 17 12 const hydration = await ctx.hydrator.hydrateLabelers(dids, hydrateCtx); 18 13 19 14 const views = mapDefined(dids, (did) => {
+2 -6
api/so/sprk/notification/getUnreadCount.ts
··· 6 6 import { 7 7 createPipeline, 8 8 HydrationFnInput, 9 - noRules, 10 9 PresentationFnInput, 11 10 SkeletonFnInput, 12 11 } from "../../../../pipeline.ts"; 13 - import { Views } from "../../../../views/index.ts"; 14 12 15 13 export default function (server: Server, ctx: AppContext) { 16 - const getUnreadCount = createPipeline( 14 + const getUnreadCount = createPipeline({ 17 15 skeleton, 18 16 hydration, 19 - noRules, 20 17 presentation, 21 - ); 18 + }); 22 19 server.so.sprk.notification.getUnreadCount({ 23 20 auth: ctx.authVerifier.standard, 24 21 handler: async ({ auth, params }) => { ··· 71 68 72 69 type Context = { 73 70 hydrator: Hydrator; 74 - views: Views; 75 71 }; 76 72 77 73 type Params = QueryParams & {
+17 -12
api/so/sprk/notification/listNotifications.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { ServerConfig } from "../../../../config.ts"; 4 3 import { AppContext } from "../../../../context.ts"; ··· 8 7 import { QueryParams } from "../../../../lex/types/so/sprk/notification/listNotifications.ts"; 9 8 import { 10 9 createPipeline, 10 + filterSkeletonList, 11 11 HydrationFnInput, 12 + mapSkeletonList, 12 13 PresentationFnInput, 13 14 RulesFnInput, 14 15 SkeletonFnInput, 15 16 } from "../../../../pipeline.ts"; 16 17 import { Views } from "../../../../views/index.ts"; 17 - import { resHeaders } from "../../../util.ts"; 18 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 18 19 19 20 export default function (server: Server, ctx: AppContext) { 20 - const listNotifications = createPipeline( 21 + const listNotifications = createPipeline({ 21 22 skeleton, 22 23 hydration, 23 - noBlockOrMutesOrNeedsFiltering, 24 + rules: noBlockOrMutesOrNeedsFiltering, 24 25 presentation, 25 - ); 26 + }); 26 27 server.so.sprk.notification.listNotifications({ 27 28 auth: ctx.authVerifier.standard, 28 29 handler: async ({ params, auth, req }) => { 29 30 const viewer = auth.credentials.iss; 30 - const labelers = ctx.reqLabelers(req); 31 - const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer }); 31 + const hydrateCtx = await createHydrateCtxFromAuth( 32 + ctx, 33 + req, 34 + auth, 35 + { viewer }, 36 + ); 32 37 const result = await listNotifications( 33 38 { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) }, 34 39 ctx, ··· 172 177 input: RulesFnInput<Context, Params, SkeletonState>, 173 178 ) => { 174 179 const { skeleton, hydration, ctx } = input; 175 - skeleton.notifs = skeleton.notifs.filter((item) => { 180 + return filterSkeletonList(skeleton, "notifs", (item) => { 176 181 // Use authorDid directly (the person who created the notification action) 177 182 // For likes, this is the liker; for replies, this is the replier, etc. 178 183 const did = item.authorDid; ··· 199 204 } 200 205 return true; 201 206 }); 202 - return skeleton; 203 207 }; 204 208 205 209 const presentation = ( 206 210 input: PresentationFnInput<Context, Params, SkeletonState>, 207 211 ) => { 208 212 const { skeleton, hydration, ctx } = input; 209 - const { notifs, lastSeenNotifs, cursor } = skeleton; 210 - const notifications = mapDefined( 211 - notifs, 213 + const { lastSeenNotifs, cursor } = skeleton; 214 + const notifications = mapSkeletonList( 215 + skeleton, 216 + "notifs", 212 217 (notif) => ctx.views.notification(notif, lastSeenNotifs, hydration), 213 218 ); 214 219 return {
+25 -37
api/so/sprk/sound/getActorAudios.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; 5 - import { 6 - HydrateCtx, 7 - HydrationState, 8 - Hydrator, 9 - } from "../../../../hydration/index.ts"; 4 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 10 5 import { Server } from "../../../../lex/index.ts"; 11 6 import { QueryParams } from "../../../../lex/types/so/sprk/sound/getActorAudios.ts"; 12 - import { createPipeline } from "../../../../pipeline.ts"; 7 + import { 8 + createPipeline, 9 + filterSkeletonList, 10 + mapSkeletonList, 11 + type PresentationFnInput, 12 + type RulesFnInput, 13 + } from "../../../../pipeline.ts"; 13 14 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 14 15 import { Views } from "../../../../views/index.ts"; 15 - import { resHeaders } from "../../../util.ts"; 16 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 16 17 17 18 export default function (server: Server, ctx: AppContext) { 18 - const getActorAudios = createPipeline( 19 + const getActorAudios = createPipeline({ 19 20 skeleton, 20 21 hydration, 21 - noBlocks, 22 + rules: noBlocks, 22 23 presentation, 23 - ); 24 + }); 24 25 server.so.sprk.sound.getActorAudios({ 25 26 auth: ctx.authVerifier.standardOptional, 26 27 handler: async ({ params, auth, req }) => { 27 - const viewer = auth.credentials.type === "standard" 28 - ? auth.credentials.iss 29 - : undefined; 30 - const labelers = ctx.reqLabelers(req); 31 - const hydrateCtx = await ctx.hydrator.createContext({ 32 - viewer: viewer ?? null, 33 - labelers, 34 - }); 28 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 35 29 36 30 const results = await getActorAudios({ ...params, hydrateCtx }, ctx); 37 31 ··· 81 75 return ctx.hydrator.hydrateSounds(skeleton.audios, params.hydrateCtx); 82 76 }; 83 77 84 - const noBlocks = (inputs: { 85 - ctx: Context; 86 - skeleton: Skeleton; 87 - hydration: HydrationState; 88 - }) => { 78 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 89 79 const { ctx, skeleton, hydration } = inputs; 90 - skeleton.audios = skeleton.audios.filter((uri) => { 91 - const creator = creatorFromUri(uri); 92 - return !ctx.views.viewerBlockExists(creator, hydration); 93 - }); 94 - return skeleton; 80 + return filterSkeletonList( 81 + skeleton, 82 + "audios", 83 + (uri) => !ctx.views.viewerBlockExists(creatorFromUri(uri), hydration), 84 + ); 95 85 }; 96 86 97 - const presentation = (inputs: { 98 - ctx: Context; 99 - params: Params; 100 - skeleton: Skeleton; 101 - hydration: HydrationState; 102 - }) => { 87 + const presentation = ( 88 + inputs: PresentationFnInput<Context, Params, Skeleton>, 89 + ) => { 103 90 const { ctx, skeleton, hydration } = inputs; 104 - const audios = mapDefined( 105 - skeleton.audios, 91 + const audios = mapSkeletonList( 92 + skeleton, 93 + "audios", 106 94 (uri) => ctx.views.sound(uri, hydration), 107 95 ); 108 96 return { audios, cursor: skeleton.cursor };
+24 -33
api/so/sprk/sound/getAudioPosts.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { InvalidRequestError } from "@atp/xrpc-server"; 3 2 import { AppContext } from "../../../../context.ts"; 4 3 import { DataPlane } from "../../../../data-plane/index.ts"; 5 4 import { 6 5 HydrateCtx, 7 - HydrationState, 8 6 Hydrator, 9 7 mergeManyStates, 10 8 } from "../../../../hydration/index.ts"; 11 9 import { Server } from "../../../../lex/index.ts"; 12 10 import { QueryParams } from "../../../../lex/types/so/sprk/sound/getAudioPosts.ts"; 13 - import { createPipeline } from "../../../../pipeline.ts"; 11 + import { 12 + createPipeline, 13 + filterSkeletonList, 14 + mapSkeletonList, 15 + type PresentationFnInput, 16 + type RulesFnInput, 17 + } from "../../../../pipeline.ts"; 14 18 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 15 19 import { Views } from "../../../../views/index.ts"; 16 - import { resHeaders } from "../../../util.ts"; 20 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 17 21 18 22 export default function (server: Server, ctx: AppContext) { 19 - const getAudioPosts = createPipeline( 23 + const getAudioPosts = createPipeline({ 20 24 skeleton, 21 25 hydration, 22 - noBlocks, 26 + rules: noBlocks, 23 27 presentation, 24 - ); 28 + }); 25 29 server.so.sprk.sound.getAudioPosts({ 26 30 auth: ctx.authVerifier.standardOptional, 27 31 handler: async ({ params, auth, req }) => { 28 - const viewer = auth.credentials.type === "standard" 29 - ? auth.credentials.iss 30 - : undefined; 31 - const labelers = ctx.reqLabelers(req); 32 - const hydrateCtx = await ctx.hydrator.createContext({ 33 - viewer: viewer ?? null, 34 - labelers, 35 - }); 32 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 36 33 37 34 const results = await getAudioPosts({ ...params, hydrateCtx }, ctx); 38 35 ··· 79 76 return mergeManyStates(postState, soundState); 80 77 }; 81 78 82 - const noBlocks = (inputs: { 83 - ctx: Context; 84 - skeleton: Skeleton; 85 - hydration: HydrationState; 86 - }) => { 79 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 87 80 const { ctx, skeleton, hydration } = inputs; 88 - skeleton.posts = skeleton.posts.filter((uri) => { 89 - const creator = creatorFromUri(uri); 90 - return !ctx.views.viewerBlockExists(creator, hydration); 91 - }); 92 - return skeleton; 81 + return filterSkeletonList( 82 + skeleton, 83 + "posts", 84 + (uri) => !ctx.views.viewerBlockExists(creatorFromUri(uri), hydration), 85 + ); 93 86 }; 94 87 95 - const presentation = (inputs: { 96 - ctx: Context; 97 - params: Params; 98 - skeleton: Skeleton; 99 - hydration: HydrationState; 100 - }) => { 88 + const presentation = ( 89 + inputs: PresentationFnInput<Context, Params, Skeleton>, 90 + ) => { 101 91 const { ctx, skeleton, hydration } = inputs; 102 - const posts = mapDefined( 103 - skeleton.posts, 92 + const posts = mapSkeletonList( 93 + skeleton, 94 + "posts", 104 95 (uri) => ctx.views.post(uri, hydration), 105 96 ); 106 97 const audio = ctx.views.sound(skeleton.audioUri, hydration);
+29 -35
api/so/sprk/sound/getAudios.ts
··· 1 - import { dedupeStrs, mapDefined } from "@atp/common"; 1 + import { dedupeStrs } from "@atp/common"; 2 2 import { AppContext } from "../../../../context.ts"; 3 - import { 4 - HydrateCtx, 5 - HydrationState, 6 - Hydrator, 7 - } from "../../../../hydration/index.ts"; 3 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 8 4 import { Server } from "../../../../lex/index.ts"; 9 5 import { QueryParams } from "../../../../lex/types/so/sprk/sound/getAudios.ts"; 10 - import { createPipeline } from "../../../../pipeline.ts"; 6 + import { 7 + createPipeline, 8 + filterSkeletonList, 9 + mapSkeletonList, 10 + type PresentationFnInput, 11 + type RulesFnInput, 12 + } from "../../../../pipeline.ts"; 11 13 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 12 14 import { Views } from "../../../../views/index.ts"; 13 - import { resHeaders } from "../../../util.ts"; 15 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 14 16 15 17 export default function (server: Server, ctx: AppContext) { 16 - const getAudios = createPipeline(skeleton, hydration, noBlocks, presentation); 18 + const getAudios = createPipeline({ 19 + skeleton, 20 + hydration, 21 + rules: noBlocks, 22 + presentation, 23 + }); 17 24 server.so.sprk.sound.getAudios({ 18 25 auth: ctx.authVerifier.standardOptional, 19 26 handler: async ({ params, auth, req }) => { 20 - const viewer = auth.credentials.type === "standard" 21 - ? auth.credentials.iss 22 - : undefined; 23 - const labelers = ctx.reqLabelers(req); 24 - const hydrateCtx = await ctx.hydrator.createContext({ 25 - viewer: viewer ?? null, 26 - labelers, 27 - }); 27 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 28 28 29 29 const results = await getAudios({ ...params, hydrateCtx }, ctx); 30 30 ··· 53 53 ); 54 54 }; 55 55 56 - const noBlocks = (inputs: { 57 - ctx: Context; 58 - skeleton: Skeleton; 59 - hydration: HydrationState; 60 - }) => { 56 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 61 57 const { ctx, skeleton, hydration } = inputs; 62 - skeleton.audios = skeleton.audios.filter((uri) => { 63 - const creator = creatorFromUri(uri); 64 - return !ctx.views.viewerBlockExists(creator, hydration); 65 - }); 66 - return skeleton; 58 + return filterSkeletonList( 59 + skeleton, 60 + "audios", 61 + (uri) => !ctx.views.viewerBlockExists(creatorFromUri(uri), hydration), 62 + ); 67 63 }; 68 64 69 - const presentation = (inputs: { 70 - ctx: Context; 71 - params: Params; 72 - skeleton: Skeleton; 73 - hydration: HydrationState; 74 - }) => { 65 + const presentation = ( 66 + inputs: PresentationFnInput<Context, Params, Skeleton>, 67 + ) => { 75 68 const { ctx, skeleton, hydration } = inputs; 76 - const audios = mapDefined( 77 - skeleton.audios, 69 + const audios = mapSkeletonList( 70 + skeleton, 71 + "audios", 78 72 (uri) => ctx.views.sound(uri, hydration), 79 73 ); 80 74 return { audios };
+25 -37
api/so/sprk/sound/getTrendingAudios.ts
··· 1 - import { mapDefined } from "@atp/common"; 2 1 import { AppContext } from "../../../../context.ts"; 3 2 import { DataPlane } from "../../../../data-plane/index.ts"; 4 - import { 5 - HydrateCtx, 6 - HydrationState, 7 - Hydrator, 8 - } from "../../../../hydration/index.ts"; 3 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 9 4 import { Server } from "../../../../lex/index.ts"; 10 5 import { QueryParams } from "../../../../lex/types/so/sprk/sound/getTrendingAudios.ts"; 11 - import { createPipeline } from "../../../../pipeline.ts"; 6 + import { 7 + createPipeline, 8 + filterSkeletonList, 9 + mapSkeletonList, 10 + type PresentationFnInput, 11 + type RulesFnInput, 12 + } from "../../../../pipeline.ts"; 12 13 import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 13 14 import { Views } from "../../../../views/index.ts"; 14 - import { resHeaders } from "../../../util.ts"; 15 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 15 16 16 17 export default function (server: Server, ctx: AppContext) { 17 - const getTrendingAudios = createPipeline( 18 + const getTrendingAudios = createPipeline({ 18 19 skeleton, 19 20 hydration, 20 - noBlocks, 21 + rules: noBlocks, 21 22 presentation, 22 - ); 23 + }); 23 24 server.so.sprk.sound.getTrendingAudios({ 24 25 auth: ctx.authVerifier.standardOptional, 25 26 handler: async ({ params, auth, req }) => { 26 - const viewer = auth.credentials.type === "standard" 27 - ? auth.credentials.iss 28 - : undefined; 29 - const labelers = ctx.reqLabelers(req); 30 - const hydrateCtx = await ctx.hydrator.createContext({ 31 - viewer: viewer ?? null, 32 - labelers, 33 - }); 27 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 34 28 35 29 const results = await getTrendingAudios({ ...params, hydrateCtx }, ctx); 36 30 ··· 67 61 return ctx.hydrator.hydrateSounds(skeleton.audios, params.hydrateCtx); 68 62 }; 69 63 70 - const noBlocks = (inputs: { 71 - ctx: Context; 72 - skeleton: Skeleton; 73 - hydration: HydrationState; 74 - }) => { 64 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 75 65 const { ctx, skeleton, hydration } = inputs; 76 - skeleton.audios = skeleton.audios.filter((uri) => { 77 - const creator = creatorFromUri(uri); 78 - return !ctx.views.viewerBlockExists(creator, hydration); 79 - }); 80 - return skeleton; 66 + return filterSkeletonList( 67 + skeleton, 68 + "audios", 69 + (uri) => !ctx.views.viewerBlockExists(creatorFromUri(uri), hydration), 70 + ); 81 71 }; 82 72 83 - const presentation = (inputs: { 84 - ctx: Context; 85 - params: Params; 86 - skeleton: Skeleton; 87 - hydration: HydrationState; 88 - }) => { 73 + const presentation = ( 74 + inputs: PresentationFnInput<Context, Params, Skeleton>, 75 + ) => { 89 76 const { ctx, skeleton, hydration } = inputs; 90 - const audios = mapDefined( 91 - skeleton.audios, 77 + const audios = mapSkeletonList( 78 + skeleton, 79 + "audios", 92 80 (uri) => ctx.views.sound(uri, hydration), 93 81 ); 94 82 return { audios, cursor: skeleton.cursor };
+22 -23
api/so/sprk/story/getStories.ts
··· 1 - import { dedupeStrs, mapDefined } from "@atp/common"; 1 + import { dedupeStrs } from "@atp/common"; 2 2 import { AppContext } from "../../../../context.ts"; 3 3 import { 4 4 HydrateCtx, ··· 12 12 } from "../../../../lex/types/so/sprk/story/getStories.ts"; 13 13 import { 14 14 createPipeline, 15 + filterSkeletonList, 15 16 HydrationFnInput, 17 + mapSkeletonList, 16 18 PresentationFnInput, 17 19 RulesFnInput, 18 20 SkeletonFnInput, 19 21 } from "../../../../pipeline.ts"; 20 22 import { uriToDid } from "../../../../utils/uris.ts"; 21 23 import { Views } from "../../../../views/index.ts"; 22 - import { resHeaders } from "../../../util.ts"; 24 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 23 25 24 26 // Constants 25 27 const MAX_STORIES_LIMIT = 25; ··· 54 56 } 55 57 56 58 export default function (server: Server, ctx: AppContext) { 57 - const getStories = createPipeline(skeleton, hydration, rules, presentation); 59 + const getStories = createPipeline({ 60 + skeleton, 61 + hydration, 62 + rules, 63 + presentation, 64 + }); 58 65 server.so.sprk.story.getStories({ 59 66 auth: ctx.authVerifier.standardOptional, 60 67 handler: async ({ params, auth, req }) => { 61 - const viewer = auth.credentials.type === "standard" 62 - ? auth.credentials.iss 63 - : null; 64 - const labelers = ctx.reqLabelers(req); 65 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 68 + const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); 66 69 67 70 // Ensure uris is an array 68 71 const uriArray = Array.isArray(params.uris) ? params.uris : [params.uris]; ··· 102 105 } 103 106 104 107 const result = await getStories( 105 - { ...params, uris: validUris, hydrateCtx, viewer: viewer || null }, 108 + { ...params, uris: validUris, hydrateCtx, viewer: hydrateCtx.viewer }, 106 109 ctx, 107 110 ); 108 111 ··· 132 135 const rules = (inputs: RulesFnInput<Context, Params, Skeleton>): Skeleton => { 133 136 const { ctx, skeleton, hydration } = inputs; 134 137 135 - // Filter out expired stories (24 hours) 136 - const activeStories = skeleton.stories.filter((uri) => { 138 + const activeStories = filterSkeletonList(skeleton, "stories", (uri) => { 137 139 const storyInfo = hydration.stories?.get(uri); 138 140 if (!storyInfo) return false; 139 141 140 - // Check if story is expired (older than 24 hours) 141 142 const twentyFourHoursAgo = new Date(); 142 143 twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24); 143 - const storyDate = storyInfo.indexedAt; 144 - return storyDate >= twentyFourHoursAgo; 145 - }); 146 - 147 - // Filter out blocked stories 148 - const accessibleStories = activeStories.filter((uri) => { 149 - const authorDid = uriToDid(uri); 150 - return !ctx.views.viewerBlockExists(authorDid, hydration); 144 + return storyInfo.indexedAt >= twentyFourHoursAgo; 151 145 }); 152 146 153 - return { stories: accessibleStories }; 147 + return filterSkeletonList( 148 + activeStories, 149 + "stories", 150 + (uri) => !ctx.views.viewerBlockExists(uriToDid(uri), hydration), 151 + ); 154 152 }; 155 153 156 154 const presentation = ( 157 155 inputs: PresentationFnInput<Context, Params, Skeleton>, 158 156 ): OutputSchema => { 159 157 const { ctx, skeleton, hydration } = inputs; 160 - const storyViews = mapDefined( 161 - skeleton.stories, 158 + const storyViews = mapSkeletonList( 159 + skeleton, 160 + "stories", 162 161 (uri) => ctx.views.story(uri, hydration), 163 162 ); 164 163
+23 -24
api/so/sprk/story/getTimeline.ts
··· 9 9 } from "../../../../lex/types/so/sprk/story/getTimeline.ts"; 10 10 import { 11 11 createPipeline, 12 + filterSkeletonList, 12 13 HydrationFnInput, 14 + mapSkeletonList, 13 15 PresentationFnInput, 14 16 RulesFnInput, 15 17 SkeletonFnInput, 16 18 } from "../../../../pipeline.ts"; 17 19 import { uriToDid } from "../../../../utils/uris.ts"; 18 - import { resHeaders } from "../../../util.ts"; 20 + import { createHydrateCtxFromAuth, resHeaders } from "../../../util.ts"; 19 21 20 22 // Constants 21 23 const MAX_LIMIT = 100; 22 24 const DEFAULT_LIMIT = 50; 23 25 24 26 export default function (server: Server, ctx: AppContext) { 25 - const getTimeline = createPipeline( 27 + const getTimeline = createPipeline({ 26 28 skeleton, 27 29 hydration, 28 30 rules, 29 31 presentation, 30 - ); 32 + }); 31 33 server.so.sprk.story.getTimeline({ 32 34 auth: ctx.authVerifier.standard, 33 35 handler: async ({ params, auth, req }) => { 34 36 const viewer = auth.credentials.iss; 35 - const labelers = ctx.reqLabelers(req); 36 - const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers }); 37 + const hydrateCtx = await createHydrateCtxFromAuth( 38 + ctx, 39 + req, 40 + auth, 41 + { viewer }, 42 + ); 37 43 38 44 const { limit: limitParam = DEFAULT_LIMIT, cursor } = params; 39 45 ··· 110 116 const rules = (inputs: RulesFnInput<Context, Params, Skeleton>): Skeleton => { 111 117 const { ctx, skeleton, hydration } = inputs; 112 118 113 - // Filter out expired stories (24 hours) 114 - // Note: The dataplane already filters expired stories, so we only ensure 115 - // records still exist after hydration. 116 - const activeStories = skeleton.stories.filter((uri) => { 117 - const storyInfo = hydration.stories?.get(uri); 118 - if (!storyInfo) return false; 119 - 120 - // The dataplane already filtered expired stories, so we just check if it exists 121 - return true; 119 + const activeStories = filterSkeletonList(skeleton, "stories", (uri) => { 120 + return !!hydration.stories?.get(uri); 122 121 }); 123 122 124 - // Filter out blocked stories 125 - const accessibleStories = activeStories.filter((uri) => { 126 - const authorDid = uriToDid(uri); 127 - return !ctx.views.viewerBlockExists(authorDid, hydration); 128 - }); 129 - 130 - return { stories: accessibleStories, cursor: skeleton.cursor }; 123 + return filterSkeletonList( 124 + activeStories, 125 + "stories", 126 + (uri) => !ctx.views.viewerBlockExists(uriToDid(uri), hydration), 127 + ); 131 128 }; 132 129 133 130 const presentation = ( 134 131 inputs: PresentationFnInput<Context, Params, Skeleton>, 135 132 ): OutputSchema => { 136 133 const { ctx, skeleton, hydration } = inputs; 137 - const storyViews = skeleton.stories 138 - .map((uri) => ctx.views.story(uri, hydration)) 139 - .filter((view): view is NonNullable<typeof view> => view !== undefined); 134 + const storyViews = mapSkeletonList( 135 + skeleton, 136 + "stories", 137 + (uri) => ctx.views.story(uri, hydration), 138 + ); 140 139 141 140 // Group stories by author 142 141 const storiesByAuthor = ctx.views.storiesByAuthor(storyViews);
+31
api/util.ts
··· 1 + import type { 2 + ModServiceOutput, 3 + NullOutput, 4 + RoleOutput, 5 + StandardOutput, 6 + } from "../auth-verifier.ts"; 7 + import type { AppContext } from "../context.ts"; 8 + import type { HydrateCtx, HydrateCtxVals } from "../hydration/index.ts"; 1 9 import { formatLabelerHeader, ParsedLabelers } from "../util.ts"; 2 10 3 11 export const SPRK_USER_AGENT = "SprkAppView"; ··· 25 33 export const clearlyBadCursor = (cursor?: string) => { 26 34 return !!cursor?.includes("::"); 27 35 }; 36 + 37 + type HydrateCtxAuth = 38 + | StandardOutput 39 + | RoleOutput 40 + | NullOutput 41 + | ModServiceOutput; 42 + 43 + export const createHydrateCtxFromAuth = ( 44 + ctx: AppContext, 45 + req: Request, 46 + auth: HydrateCtxAuth, 47 + overrides: Partial<HydrateCtxVals> = {}, 48 + ): Promise<HydrateCtx> => { 49 + const labelers = ctx.reqLabelers(req); 50 + const { canPerformTakedown: _canPerformTakedown, ...hydrateVals } = ctx 51 + .authVerifier.parseCreds(auth); 52 + 53 + return ctx.hydrator.createContext({ 54 + labelers, 55 + ...hydrateVals, 56 + ...overrides, 57 + }); 58 + };
+72 -16
pipeline.ts
··· 1 + import { mapDefined } from "@atp/common"; 1 2 import { HydrationState } from "./hydration/index.ts"; 2 3 4 + export type SkeletonFn<Context, Params, Skeleton> = ( 5 + input: SkeletonFnInput<Context, Params>, 6 + ) => Promise<Skeleton> | Skeleton; 7 + 8 + export type HydrationFn<Context, Params, Skeleton> = ( 9 + input: HydrationFnInput<Context, Params, Skeleton>, 10 + ) => Promise<HydrationState>; 11 + 12 + export type RulesFn<Context, Params, Skeleton> = ( 13 + input: RulesFnInput<Context, Params, Skeleton>, 14 + ) => Skeleton; 15 + 16 + export type PresentationFn<Context, Params, Skeleton, View> = ( 17 + input: PresentationFnInput<Context, Params, Skeleton>, 18 + ) => View; 19 + 20 + export type PipelineDefinition<Params, Skeleton, View, Context> = { 21 + skeleton: SkeletonFn<Context, Params, Skeleton>; 22 + hydration: HydrationFn<Context, Params, Skeleton>; 23 + rules?: RulesFn<Context, Params, Skeleton>; 24 + presentation: PresentationFn<Context, Params, Skeleton, View>; 25 + }; 26 + 3 27 export function createPipeline<Params, Skeleton, View, Context>( 4 - skeletonFn: ( 5 - input: SkeletonFnInput<Context, Params>, 6 - ) => Promise<Skeleton> | Skeleton, 7 - hydrationFn: ( 8 - input: HydrationFnInput<Context, Params, Skeleton>, 9 - ) => Promise<HydrationState>, 10 - rulesFn: (input: RulesFnInput<Context, Params, Skeleton>) => Skeleton, 11 - presentationFn: ( 12 - input: PresentationFnInput<Context, Params, Skeleton>, 13 - ) => View, 28 + definition: PipelineDefinition<Params, Skeleton, View, Context>, 29 + ): (params: Params, ctx: Context) => Promise<View>; 30 + export function createPipeline<Params, Skeleton, View, Context>( 31 + definition: PipelineDefinition<Params, Skeleton, View, Context>, 14 32 ) { 33 + const applyRules = definition.rules ?? 34 + ((input: RulesFnInput<Context, Params, Skeleton>) => input.skeleton); 35 + 15 36 return async (params: Params, ctx: Context) => { 16 - const skeleton = await skeletonFn({ ctx, params }); 17 - const hydration = await hydrationFn({ ctx, params, skeleton }); 18 - const appliedRules = rulesFn({ ctx, params, skeleton, hydration }); 19 - return presentationFn({ ctx, params, skeleton: appliedRules, hydration }); 37 + const skeleton = await definition.skeleton({ ctx, params }); 38 + const hydration = await definition.hydration({ ctx, params, skeleton }); 39 + const appliedRules = applyRules({ ctx, params, skeleton, hydration }); 40 + return definition.presentation({ 41 + ctx, 42 + params, 43 + skeleton: appliedRules, 44 + hydration, 45 + }); 20 46 }; 21 47 } 22 48 ··· 45 71 hydration: HydrationState; 46 72 }; 47 73 48 - export function noRules<S>(input: { skeleton: S }) { 49 - return input.skeleton; 74 + type SkeletonListKey<S> = { 75 + [K in keyof S]: S[K] extends readonly unknown[] ? K : never; 76 + }[keyof S]; 77 + 78 + type SkeletonListItem<T> = T extends readonly (infer Item)[] ? Item : never; 79 + 80 + export function filterSkeletonList< 81 + Skeleton extends Record<string, unknown>, 82 + Key extends SkeletonListKey<Skeleton>, 83 + >( 84 + skeleton: Skeleton, 85 + key: Key, 86 + predicate: (item: SkeletonListItem<Skeleton[Key]>) => boolean, 87 + ): Skeleton { 88 + const items = skeleton[key] as SkeletonListItem<Skeleton[Key]>[]; 89 + return { 90 + ...skeleton, 91 + [key]: items.filter(predicate), 92 + } as Skeleton; 93 + } 94 + 95 + export function mapSkeletonList< 96 + Skeleton extends Record<string, unknown>, 97 + Key extends SkeletonListKey<Skeleton>, 98 + View, 99 + >( 100 + skeleton: Skeleton, 101 + key: Key, 102 + mapper: (item: SkeletonListItem<Skeleton[Key]>) => View | undefined, 103 + ): View[] { 104 + const items = skeleton[key] as SkeletonListItem<Skeleton[Key]>[]; 105 + return mapDefined(items, mapper); 50 106 }