[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 viewer state on searchActors (#7)

authored by

Davi Rodrigues and committed by
GitHub
a9a94bbd 90e6c9cd

+128 -82
+128 -82
services/appview/src/routes/so/sprk/actor/searchActor.ts
··· 1 1 import { Hono } from 'hono' 2 + import { optionalAuthMiddleware } from '../../../../auth/middleware.js' 2 3 import { AppContext } from '../../../../index.js' 3 4 import type { Label } from '../../../../lexicon/types/com/atproto/label/defs.js' 4 5 import type * as SoSprkActorDefs from '../../../../lexicon/types/so/sprk/actor/defs.js' ··· 12 13 export const createSearchActorRouter = (ctx: AppContext) => { 13 14 const router = new Hono() 14 15 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 - } 16 + router.get( 17 + '/xrpc/so.sprk.actor.searchActors', 18 + optionalAuthMiddleware, 19 + async (c) => { 20 + const q = c.req.query('q')?.trim() 21 + let limit = parseInt(c.req.query('limit') ?? '25') 22 + if (isNaN(limit)) limit = 25 23 + if (limit < 1 || limit > 100) { 24 + return c.json({ error: 'Limit must be between 1 and 100' }, 400) 25 + } 22 26 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) 27 + let skip = 0 28 + const cursorParam = c.req.query('cursor') 29 + if (cursorParam) { 30 + skip = parseInt(cursorParam) 31 + if (isNaN(skip) || skip < 0) { 32 + return c.json({ error: 'Invalid cursor' }, 400) 33 + } 29 34 } 30 - } 31 35 32 - const filter: any = {} 33 - const sort: any = {} 36 + const filter: any = {} 37 + const sort: any = {} 34 38 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 - } 39 + if (q) { 40 + const escaped = escapeRegExp(q) 41 + const regex = new RegExp(escaped, 'i') 42 + filter.$or = [ 43 + { displayName: regex }, 44 + { description: regex }, 45 + { handle: regex }, 46 + ] 47 + // fall back to sorting by createdAt 48 + sort.createdAt = -1 49 + } else { 50 + sort.createdAt = -1 51 + } 48 52 49 - const profiles = await ctx.db.models.Profile.find(filter) 50 - .sort(sort) 51 - .skip(skip) 52 - .limit(limit) 53 - .lean() 53 + const profiles = await ctx.db.models.Profile.find(filter) 54 + .sort(sort) 55 + .skip(skip) 56 + .limit(limit) 57 + .lean() 54 58 55 - // For handle search, exclude DIDs already found 56 - const foundDids = profiles.map(p => p.authorDid) 57 - const handleFilter = { ...filter, did: { $nin: foundDids } } 58 - const handles = await ctx.db.models.Actor.find(handleFilter) 59 - .sort(sort) 60 - .skip(skip) 61 - .limit(limit) 62 - .lean() 59 + // For handle search, exclude DIDs already found 60 + const foundDids = profiles.map((p) => p.authorDid) 61 + const handleFilter = { ...filter, did: { $nin: foundDids } } 62 + const handles = await ctx.db.models.Actor.find(handleFilter) 63 + .sort(sort) 64 + .skip(skip) 65 + .limit(limit) 66 + .lean() 63 67 64 - const handleProfiles = await ctx.db.models.Profile.find({ authorDid: { $in: handles.map(h => h.did) } }).lean() 65 - 66 - const fullProfiles = [...profiles, ...handleProfiles] 67 - 68 - const actors: SoSprkActorDefs.ProfileView[] = (await Promise.all( 69 - fullProfiles.map(async (p) => { 70 - const avatar = p.avatar 71 - ? `https://media.sprk.so/avatar/tiny/${p.authorDid}/${(p.avatar as any).ref.$link}/webp` 72 - : undefined 73 - const labels = Array.isArray(p.labels) 74 - ? (p.labels as Label[]) 75 - : undefined 76 - const now = new Date().toISOString() 77 - await ctx.indexingService.indexHandle(p.authorDid, now) 78 - const actor = await ctx.db.models.Actor.findOne({ did: p.authorDid }) 79 - if (!actor || !actor.handle) { 80 - return null 81 - } 82 - return { 83 - $type: 'so.sprk.actor.defs#profileView', 84 - did: p.authorDid, 85 - handle: actor.handle, 86 - displayName: p.displayName, 87 - description: p.description, 88 - avatar, 89 - indexedAt: p.indexedAt, 90 - createdAt: p.createdAt, 91 - labels, 92 - } satisfies SoSprkActorDefs.ProfileView 93 - }), 94 - )).filter((actor): actor is { $type: "so.sprk.actor.defs#profileView"; did: string; handle: string; displayName: string | undefined; description: string | undefined; avatar: string | undefined; indexedAt: string; createdAt: string; labels: Label[] | undefined; } => actor !== null) 68 + const handleProfiles = await ctx.db.models.Profile.find({ 69 + authorDid: { $in: handles.map((h) => h.did) }, 70 + }).lean() 71 + 72 + const fullProfiles = [...profiles, ...handleProfiles] 73 + 74 + const viewerDid = c.get('did') as string | undefined 75 + 76 + const actors: SoSprkActorDefs.ProfileView[] = ( 77 + await Promise.all( 78 + fullProfiles.map(async (p) => { 79 + const avatar = p.avatar 80 + ? `https://media.sprk.so/avatar/tiny/${p.authorDid}/${(p.avatar as any).ref.$link}/webp` 81 + : undefined 82 + const labels = Array.isArray(p.labels) 83 + ? (p.labels as Label[]) 84 + : undefined 85 + const now = new Date().toISOString() 86 + await ctx.indexingService.indexHandle(p.authorDid, now) 87 + const actor = await ctx.db.models.Actor.findOne({ 88 + did: p.authorDid, 89 + }) 90 + if (!actor || !actor.handle) { 91 + return undefined 92 + } 93 + 94 + let viewer: SoSprkActorDefs.ViewerState | undefined = undefined 95 + if (viewerDid) { 96 + const [follow, followedBy, block, blockedBy] = await Promise.all([ 97 + ctx.db.models.Follow.findOne({ 98 + subject: p.authorDid, 99 + authorDid: viewerDid, 100 + }), 101 + ctx.db.models.Follow.findOne({ 102 + subject: viewerDid, 103 + authorDid: p.authorDid, 104 + }), 105 + ctx.db.models.Block.findOne({ 106 + subject: p.authorDid, 107 + authorDid: viewerDid, 108 + }), 109 + ctx.db.models.Block.findOne({ 110 + subject: viewerDid, 111 + authorDid: p.authorDid, 112 + }), 113 + ]) 114 + console.log(follow, followedBy, block, blockedBy) 115 + viewer = {} 116 + if (follow) viewer.following = follow.uri 117 + if (followedBy) viewer.followedBy = followedBy.uri 118 + if (block) viewer.blocking = block.uri 119 + if (blockedBy) viewer.blockedBy = true 120 + if (Object.keys(viewer).length === 0) viewer = undefined 121 + } 95 122 96 - const nextCursor = 97 - profiles.length === limit ? String(skip + limit) : undefined 98 - const result: SoSprkActorSearch.OutputSchema = { actors } 99 - if (nextCursor) { 100 - result.cursor = nextCursor 101 - } 123 + return { 124 + $type: 'so.sprk.actor.defs#profileView', 125 + did: p.authorDid, 126 + handle: actor.handle, 127 + displayName: p.displayName, 128 + description: p.description, 129 + avatar, 130 + indexedAt: p.indexedAt, 131 + createdAt: p.createdAt, 132 + labels, 133 + viewer, 134 + } as SoSprkActorDefs.ProfileView 135 + }), 136 + ) 137 + ).filter( 138 + (actor): actor is SoSprkActorDefs.ProfileView => actor !== undefined, 139 + ) 102 140 103 - return c.json(result) 104 - }) 141 + const nextCursor = 142 + profiles.length === limit ? String(skip + limit) : undefined 143 + const result: SoSprkActorSearch.OutputSchema = { actors } 144 + if (nextCursor) { 145 + result.cursor = nextCursor 146 + } 147 + 148 + return c.json(result) 149 + }, 150 + ) 105 151 106 152 return router 107 - } 153 + }