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

feat(search): searchActorsTypeahead

+166
+2
api/index.ts
··· 18 18 import putPreferences from "./so/sprk/actor/putPreferences.ts"; 19 19 import getPreferences from "./so/sprk/actor/getPreferences.ts"; 20 20 import searchActors from "./so/sprk/actor/searchActors.ts"; 21 + import searchActorsTypeahead from "./so/sprk/actor/searchActorsTypeahead.ts"; 21 22 import getRecord from "./com/atproto/repo/getRecord.ts"; 22 23 import resolveHandle from "./com/atproto/identity/resolveHandle.ts"; 23 24 import getStories from "./so/sprk/story/getStories.ts"; ··· 58 59 putPreferences(server, ctx); 59 60 getPreferences(server, ctx); 60 61 searchActors(server, ctx); 62 + searchActorsTypeahead(server, ctx); 61 63 getRecord(server, ctx); 62 64 resolveHandle(server, ctx); 63 65 getStories(server, ctx);
+120
api/so/sprk/actor/searchActorsTypeahead.ts
··· 1 + import { mapDefined } from "@atp/common"; 2 + import { AppContext } from "../../../../context.ts"; 3 + import { DataPlane } from "../../../../data-plane/index.ts"; 4 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 5 + import { Server } from "../../../../lex/index.ts"; 6 + import { QueryParams } from "../../../../lex/types/so/sprk/actor/searchActorsTypeahead.ts"; 7 + import { 8 + createPipeline, 9 + HydrationFnInput, 10 + PresentationFnInput, 11 + RulesFnInput, 12 + SkeletonFnInput, 13 + } from "../../../../pipeline.ts"; 14 + import { Views } from "../../../../views/index.ts"; 15 + import { resHeaders } from "../../../util.ts"; 16 + 17 + export default function (server: Server, ctx: AppContext) { 18 + const searchActorsTypeahead = createPipeline( 19 + skeleton, 20 + hydration, 21 + noBlocks, 22 + presentation, 23 + ); 24 + 25 + server.so.sprk.actor.searchActorsTypeahead({ 26 + auth: ctx.authVerifier.standardOptional, 27 + handler: async ({ auth, params, req }) => { 28 + const cleanedQuery = params.q?.trim() ?? ""; 29 + const labelers = ctx.reqLabelers(req); 30 + if (!cleanedQuery) { 31 + return { 32 + encoding: "application/json", 33 + body: { 34 + actors: [], 35 + }, 36 + headers: resHeaders({ labelers }), 37 + }; 38 + } 39 + 40 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 41 + const hydrateCtx = await ctx.hydrator.createContext({ 42 + viewer, 43 + labelers, 44 + includeTakedowns, 45 + }); 46 + 47 + const results = await searchActorsTypeahead({ 48 + ...params, 49 + q: cleanedQuery, 50 + hydrateCtx, 51 + }, ctx); 52 + 53 + return { 54 + encoding: "application/json", 55 + body: results, 56 + headers: resHeaders({ 57 + labelers: hydrateCtx.labelers, 58 + }), 59 + }; 60 + }, 61 + }); 62 + } 63 + 64 + const skeleton = async (inputs: SkeletonFnInput<Context, Params>) => { 65 + const { ctx, params } = inputs; 66 + const term = params.q?.trim() ?? ""; 67 + if (!term) { 68 + return { 69 + dids: [], 70 + }; 71 + } 72 + 73 + const res = await ctx.dataplane.search.actorsTypeahead(term, params.limit); 74 + return { 75 + dids: res.dids, 76 + }; 77 + }; 78 + 79 + const hydration = async ( 80 + inputs: HydrationFnInput<Context, Params, Skeleton>, 81 + ) => { 82 + const { ctx, params, skeleton } = inputs; 83 + return await ctx.hydrator.hydrateProfilesBasic( 84 + skeleton.dids, 85 + params.hydrateCtx, 86 + ); 87 + }; 88 + 89 + const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => { 90 + const { ctx, skeleton, hydration } = inputs; 91 + skeleton.dids = skeleton.dids.filter( 92 + (did) => !ctx.views.viewerBlockExists(did, hydration), 93 + ); 94 + return skeleton; 95 + }; 96 + 97 + const presentation = ( 98 + inputs: PresentationFnInput<Context, Params, Skeleton>, 99 + ) => { 100 + const { ctx, skeleton, hydration } = inputs; 101 + const actors = mapDefined( 102 + skeleton.dids, 103 + (did) => ctx.views.profileBasic(did, hydration), 104 + ); 105 + return { 106 + actors, 107 + }; 108 + }; 109 + 110 + type Context = { 111 + dataplane: DataPlane; 112 + hydrator: Hydrator; 113 + views: Views; 114 + }; 115 + 116 + type Params = QueryParams & { hydrateCtx: HydrateCtx }; 117 + 118 + type Skeleton = { 119 + dids: string[]; 120 + };
+44
data-plane/routes/search.ts
··· 93 93 }; 94 94 } 95 95 96 + async actorsTypeahead(term: string, limit = 10) { 97 + const cleanedTerm = cleanQuery(term); 98 + if (!cleanedTerm) { 99 + return { 100 + dids: [], 101 + }; 102 + } 103 + 104 + const safeLimit = Math.max(1, Math.min(limit, 100)); 105 + const candidateLimit = safeLimit * 3; 106 + const handlePrefix = cleanedTerm.toLowerCase(); 107 + const handleRangeEnd = `${handlePrefix}\uffff`; 108 + 109 + const [matchingActors, matchingProfiles] = await Promise.all([ 110 + this.db.models.Actor.find({ 111 + handle: { 112 + $gte: handlePrefix, 113 + $lt: handleRangeEnd, 114 + }, 115 + }) 116 + .select("did -_id") 117 + .sort({ handle: 1 }) 118 + .limit(candidateLimit) 119 + .lean(), 120 + this.db.models.Profile.find({ 121 + $text: { $search: cleanedTerm }, 122 + }) 123 + .select("authorDid -_id") 124 + .limit(candidateLimit) 125 + .lean(), 126 + ]); 127 + 128 + const dids = Array.from( 129 + new Set([ 130 + ...matchingActors.map((actor) => actor.did), 131 + ...matchingProfiles.map((profile) => profile.authorDid), 132 + ]), 133 + ).slice(0, safeLimit); 134 + 135 + return { 136 + dids, 137 + }; 138 + } 139 + 96 140 async posts(term: string, limit = 50, cursor?: string) { 97 141 const { q, author } = parsePostSearchQuery(term); 98 142