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

add search actors endpoint (#2)

authored by

Davi Rodrigues and committed by
GitHub
f84a0e9d 2533d954

+107 -8
+9 -2
services/appview/src/db.ts
··· 1 - import mongoose, { Schema, Document, Model, Connection } from 'mongoose' 2 - import { env } from './env.js' 1 + import mongoose, { Connection, Document, Model, Schema } from 'mongoose' 3 2 import { pino } from 'pino' 3 + import { env } from './env.js' 4 4 5 5 export interface LikeDocument extends Document { 6 6 uri: string ··· 116 116 createdAt: { type: String, required: true }, 117 117 indexedAt: { type: String, required: true }, 118 118 cid: { type: String, required: true }, 119 + }) 120 + 121 + // Add text index for profile search 122 + profileSchema.index({ 123 + displayName: 'text', 124 + authorHandle: 'text', 125 + description: 'text', 119 126 }) 120 127 121 128 export interface AudioDocument extends Document {
+9 -6
services/appview/src/index.ts
··· 12 12 createBidirectionalResolver, 13 13 createIdResolver, 14 14 } from './id-resolver.js' 15 + import { takedownFilterMiddleware } from './middleware/takedown-filter.js' 15 16 import { createGetProfileRouter } from './routes/actor/getProfile.js' 17 + import { createSearchActorRouter } from './routes/actor/searchActor.js' 18 + import { createTakedownRouter } from './routes/admin/takedowns.js' 16 19 import { createGetAuthorFeedRouter } from './routes/feed/getAuthorFeed.js' 17 20 import { createGetPostsRouter } from './routes/feed/getPosts.js' 18 21 import { createGetPostThreadRouter } from './routes/feed/getPostThread.js' 19 22 import { createGetFollowersRouter } from './routes/graph/getFollowers.js' 20 23 import { createGetFollowsRouter } from './routes/graph/getFollows.js' 24 + import { TakedownService } from './services/takedown.js' 21 25 import wellKnownRouter from './well-known.js' 22 - import { TakedownService } from './services/takedown.js' 23 - import { createTakedownRouter } from './routes/admin/takedowns.js' 24 - import { takedownFilterMiddleware } from './middleware/takedown-filter.js' 25 26 26 27 export type AppContext = { 27 28 db: Database ··· 49 50 50 51 // Get service DID from environment 51 52 const serviceDid = env.SERVICE_DID 52 - 53 + 53 54 // Create takedown service 54 55 const takedownService = new TakedownService(db) 55 56 ··· 78 79 79 80 // Apply takedown filter middleware to all routes 80 81 app.use('*', takedownFilterMiddleware) 81 - 82 + 82 83 // TODO: Remove this after getAuthorFeedRouter is properly implemented on frontend 83 84 const feedRouter = createFeedRouter(ctx) 84 85 app.route('/', feedRouter) ··· 89 90 const getFollowersRouter = createGetFollowersRouter(ctx) 90 91 const getFollowsRouter = createGetFollowsRouter(ctx) 91 92 const getAuthorFeedRouter = createGetAuthorFeedRouter(ctx) 92 - 93 + const searchActorRouter = createSearchActorRouter(ctx) 94 + 93 95 app.route('/', getPostsRouter) 94 96 app.route('/', getPostThreadRouter) 95 97 app.route('/', getProfileRouter) 96 98 app.route('/', getFollowersRouter) 97 99 app.route('/', getFollowsRouter) 98 100 app.route('/', getAuthorFeedRouter) 101 + app.route('/', searchActorRouter) 99 102 100 103 // Create and configure the takedown router 101 104 const takedownRouter = createTakedownRouter({ takedownService })
+89
services/appview/src/routes/actor/searchActor.ts
··· 1 + import { Hono } from 'hono' 2 + import { AppContext } from '../../index.js' 3 + import type { Label } from '../../lexicon/types/com/atproto/label/defs.js' 4 + import type * as SoSprkActorDefs from '../../lexicon/types/so/sprk/actor/defs.js' 5 + import type * as SoSprkActorSearch from '../../lexicon/types/so/sprk/actor/searchActors.js' 6 + 7 + // Helper to escape user input for safe RegExp usage 8 + function escapeRegExp(str: string): string { 9 + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 10 + } 11 + 12 + export const createSearchActorRouter = (ctx: AppContext) => { 13 + const router = new Hono() 14 + 15 + router.get('/xrpc/so.sprk.actor.searchActors', async (c) => { 16 + const q = c.req.query('q')?.trim() 17 + let limit = parseInt(c.req.query('limit') ?? '25') 18 + if (isNaN(limit)) limit = 25 19 + if (limit < 1 || limit > 100) { 20 + return c.json({ error: 'Limit must be between 1 and 100' }, 400) 21 + } 22 + 23 + let skip = 0 24 + const cursorParam = c.req.query('cursor') 25 + if (cursorParam) { 26 + skip = parseInt(cursorParam) 27 + if (isNaN(skip) || skip < 0) { 28 + return c.json({ error: 'Invalid cursor' }, 400) 29 + } 30 + } 31 + 32 + const filter: any = {} 33 + const sort: any = {} 34 + 35 + if (q) { 36 + const escaped = escapeRegExp(q) 37 + const regex = new RegExp(escaped, 'i') 38 + filter.$or = [ 39 + { displayName: regex }, 40 + { description: regex }, 41 + { handle: regex }, 42 + ] 43 + // fall back to sorting by createdAt 44 + sort.createdAt = -1 45 + } else { 46 + sort.createdAt = -1 47 + } 48 + 49 + const profiles = await ctx.db.models.Profile.find(filter) 50 + .sort(sort) 51 + .skip(skip) 52 + .limit(limit) 53 + .lean() 54 + 55 + const actors: SoSprkActorDefs.ProfileView[] = await Promise.all( 56 + profiles.map(async (p) => { 57 + const avatar = p.avatar 58 + ? `https://media.sprk.so/avatar/tiny/${p.authorDid}/${(p.avatar as any).ref.$link}/webp` 59 + : undefined 60 + const labels = Array.isArray(p.labels) 61 + ? (p.labels as Label[]) 62 + : undefined 63 + const handle = await ctx.resolver.resolveDidToHandle(p.authorDid) 64 + return { 65 + $type: 'so.sprk.actor.defs#profileView', 66 + did: p.authorDid, 67 + handle: handle, 68 + displayName: p.displayName, 69 + description: p.description, 70 + avatar, 71 + indexedAt: p.indexedAt, 72 + createdAt: p.createdAt, 73 + labels, 74 + } satisfies SoSprkActorDefs.ProfileView 75 + }), 76 + ) 77 + 78 + const nextCursor = 79 + profiles.length === limit ? String(skip + limit) : undefined 80 + const result: SoSprkActorSearch.OutputSchema = { actors } 81 + if (nextCursor) { 82 + result.cursor = nextCursor 83 + } 84 + 85 + return c.json(result) 86 + }) 87 + 88 + return router 89 + }