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

search endpoint

+95 -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 })
+77
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 + export const createSearchActorRouter = (ctx: AppContext) => { 8 + const router = new Hono() 9 + 10 + router.get('/xrpc/so.sprk.actor.searchActors', async (c) => { 11 + const q = c.req.query('q')?.trim() 12 + let limit = parseInt(c.req.query('limit') ?? '25') 13 + if (isNaN(limit)) limit = 25 14 + if (limit < 1 || limit > 100) { 15 + return c.json({ error: 'Limit must be between 1 and 100' }, 400) 16 + } 17 + 18 + let skip = 0 19 + const cursorParam = c.req.query('cursor') 20 + if (cursorParam) { 21 + skip = parseInt(cursorParam) 22 + if (isNaN(skip) || skip < 0) { 23 + return c.json({ error: 'Invalid cursor' }, 400) 24 + } 25 + } 26 + 27 + const filter: any = {} 28 + const sort: any = {} 29 + 30 + if (q) { 31 + filter.$text = { $search: q } 32 + sort.score = { $meta: 'textScore' } 33 + } else { 34 + sort.createdAt = -1 35 + } 36 + 37 + const profiles = await ctx.db.models.Profile.find(filter) 38 + .sort(sort) 39 + .skip(skip) 40 + .limit(limit) 41 + .lean() 42 + 43 + const actors: SoSprkActorDefs.ProfileView[] = await Promise.all( 44 + profiles.map(async (p) => { 45 + const avatar = p.avatar 46 + ? `https://media.sprk.so/avatar/tiny/${p.authorDid}/${(p.avatar as any).ref.$link}/webp` 47 + : undefined 48 + const labels = Array.isArray(p.labels) 49 + ? (p.labels as Label[]) 50 + : undefined 51 + const handle = await ctx.resolver.resolveDidToHandle(p.authorDid) 52 + return { 53 + $type: 'so.sprk.actor.defs#profileView', 54 + did: p.authorDid, 55 + handle: handle, 56 + displayName: p.displayName, 57 + description: p.description, 58 + avatar, 59 + indexedAt: p.indexedAt, 60 + createdAt: p.createdAt, 61 + labels, 62 + } satisfies SoSprkActorDefs.ProfileView 63 + }), 64 + ) 65 + 66 + const nextCursor = 67 + profiles.length === limit ? String(skip + limit) : undefined 68 + const result: SoSprkActorSearch.OutputSchema = { actors } 69 + if (nextCursor) { 70 + result.cursor = nextCursor 71 + } 72 + 73 + return c.json(result) 74 + }) 75 + 76 + return router 77 + }