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

FOLLOWS

+238
+4
services/appview/README.md
··· 33 33 - `GET /xrpc/so.sprk.actor.getProfile` - Get profile details for an actor 34 34 - `GET /xrpc/so.sprk.actor.searchActors` - Search for actors 35 35 36 + ### Graph Routes 37 + - `GET /xrpc/so.sprk.graph.getFollowers` - Get followers for an actor 38 + - `GET /xrpc/so.sprk.graph.getFollows` - Get accounts an actor follows 39 + 36 40 ### Feed Routes 37 41 - `GET /xrpc/so.sprk.feed.getPosts` - Get post objects from URIs 38 42 - `GET /xrpc/so.sprk.feed.getAuthorFeed` - Get a post and all replies to it
+6
services/appview/src/index.ts
··· 16 16 import { createGetPostsRouter } from './routes/feed/getPosts.js' 17 17 import { createGetPostThreadRouter } from './routes/feed/getPostThread.js' 18 18 import { createGetProfileRouter } from './routes/actor/getProfile.js' 19 + import { createGetFollowersRouter } from './routes/graph/getFollowers.js' 20 + import { createGetFollowsRouter } from './routes/graph/getFollows.js' 19 21 import wellKnownRouter from './well-known.js' 20 22 21 23 export type AppContext = { ··· 75 77 const getPostsRouter = createGetPostsRouter(ctx) 76 78 const getPostThreadRouter = createGetPostThreadRouter(ctx) 77 79 const getProfileRouter = createGetProfileRouter(ctx) 80 + const getFollowersRouter = createGetFollowersRouter(ctx) 81 + const getFollowsRouter = createGetFollowsRouter(ctx) 78 82 app.route('/', getPostsRouter) 79 83 app.route('/', getPostThreadRouter) 80 84 app.route('/', getProfileRouter) 85 + app.route('/', getFollowersRouter) 86 + app.route('/', getFollowsRouter) 81 87 82 88 app.route('/', wellKnownRouter()) 83 89
+114
services/appview/src/routes/graph/getFollowers.ts
··· 1 + import { Hono } from 'hono' 2 + 3 + import { optionalAuthMiddleware } from '../../auth/middleware.js' 4 + import { AppContext } from '../../index.js' 5 + import type * as SoSprkActorDefs from '../../lexicon/types/so/sprk/actor/defs.js' 6 + 7 + export const createGetFollowersRouter = (ctx: AppContext) => { 8 + const router = new Hono() 9 + 10 + router.get('/xrpc/so.sprk.graph.getFollowers', optionalAuthMiddleware, async (c) => { 11 + const actor = c.req.query('actor') 12 + const limit = parseInt(c.req.query('limit') ?? '50') 13 + const cursor = c.req.query('cursor') 14 + 15 + if (!actor) { 16 + return c.json({ error: 'Actor is required' }, 400) 17 + } 18 + 19 + // Validate limit 20 + if (limit < 1 || limit > 100) { 21 + return c.json({ error: 'Limit must be between 1 and 100' }, 400) 22 + } 23 + 24 + // Build query 25 + const query: any = { subject: actor } 26 + if (cursor) { 27 + query._id = { $gt: cursor } 28 + } 29 + 30 + // Get followers with pagination 31 + const followers = await ctx.db.models.Follow.find(query) 32 + .sort({ _id: 1 }) 33 + .limit(limit) 34 + .lean() 35 + 36 + // Get profile views for each follower 37 + const profileViews = await Promise.all( 38 + followers.map(async (follow) => { 39 + const profile = await ctx.db.models.Profile.findOne({ authorDid: follow.authorDid }) 40 + 41 + // Basic profile view with just DID and handle 42 + const basicProfileView: SoSprkActorDefs.ProfileView = { 43 + $type: 'so.sprk.actor.defs#profileView', 44 + did: follow.authorDid, 45 + handle: follow.authorHandle, 46 + viewer: { 47 + $type: 'so.sprk.actor.defs#viewerState', 48 + following: follow.uri 49 + } 50 + } 51 + 52 + // If we found a profile, add the additional fields 53 + if (profile) { 54 + return { 55 + ...basicProfileView, 56 + displayName: profile.displayName, 57 + description: profile.description, 58 + avatar: profile.avatar?.ref?.link, 59 + indexedAt: profile.indexedAt, 60 + createdAt: profile.createdAt 61 + } 62 + } 63 + 64 + return basicProfileView 65 + }) 66 + ) 67 + 68 + // Get next cursor 69 + const nextCursor = followers.length === limit ? followers[followers.length - 1]._id : undefined 70 + console.log('nextCursor', nextCursor) 71 + 72 + // Get subject profile if it exists 73 + const subjectProfile = await ctx.db.models.Profile.findOne({ authorDid: actor }) 74 + 75 + // Basic subject profile view with just DID and handle 76 + let handle = null 77 + try { 78 + if (actor) { 79 + const didData = await ctx.resolver.resolveDidToDidDoc(actor) 80 + handle = didData.handle 81 + } 82 + } catch (error) { 83 + ctx.logger.warn( 84 + { did: actor, error: (error as Error).message }, 85 + 'Failed to resolve DID to handle', 86 + ) 87 + } 88 + const subjectProfileView: SoSprkActorDefs.ProfileView = { 89 + $type: 'so.sprk.actor.defs#profileView', 90 + did: actor, 91 + handle: handle ?? 'unknown' 92 + } 93 + 94 + // If we found the subject profile, add the additional fields 95 + if (subjectProfile) { 96 + Object.assign(subjectProfileView, { 97 + handle: subjectProfile.authorHandle, 98 + displayName: subjectProfile.displayName, 99 + description: subjectProfile.description, 100 + avatar: subjectProfile.avatar?.ref?.link, 101 + indexedAt: subjectProfile.indexedAt, 102 + createdAt: subjectProfile.createdAt 103 + }) 104 + } 105 + 106 + return c.json({ 107 + subject: subjectProfileView, 108 + followers: profileViews, 109 + cursor: nextCursor 110 + }) 111 + }) 112 + 113 + return router 114 + }
+114
services/appview/src/routes/graph/getFollows.ts
··· 1 + import { Hono } from 'hono' 2 + 3 + import { optionalAuthMiddleware } from '../../auth/middleware.js' 4 + import { AppContext } from '../../index.js' 5 + import type * as SoSprkActorDefs from '../../lexicon/types/so/sprk/actor/defs.js' 6 + 7 + export const createGetFollowsRouter = (ctx: AppContext) => { 8 + const router = new Hono() 9 + 10 + router.get('/xrpc/so.sprk.graph.getFollows', optionalAuthMiddleware, async (c) => { 11 + const actor = c.req.query('actor') 12 + const limit = parseInt(c.req.query('limit') ?? '50') 13 + const cursor = c.req.query('cursor') 14 + 15 + if (!actor) { 16 + return c.json({ error: 'Actor is required' }, 400) 17 + } 18 + 19 + // Validate limit 20 + if (limit < 1 || limit > 100) { 21 + return c.json({ error: 'Limit must be between 1 and 100' }, 400) 22 + } 23 + 24 + // Build query 25 + const query: any = { authorDid: actor } 26 + if (cursor) { 27 + query._id = { $gt: cursor } 28 + } 29 + 30 + // Get follows with pagination 31 + const follows = await ctx.db.models.Follow.find(query) 32 + .sort({ _id: 1 }) 33 + .limit(limit) 34 + .lean() 35 + 36 + // Get profile views for each follow 37 + const profileViews = await Promise.all( 38 + follows.map(async (follow) => { 39 + const profile = await ctx.db.models.Profile.findOne({ authorDid: follow.subject }) 40 + // Basic profile view with just DID 41 + const basicProfileView: SoSprkActorDefs.ProfileView = { 42 + $type: 'so.sprk.actor.defs#profileView', 43 + did: follow.authorDid, 44 + handle: follow.authorHandle, 45 + viewer: { 46 + $type: 'so.sprk.actor.defs#viewerState', 47 + followedBy: follow.uri 48 + } 49 + } 50 + 51 + // If we found a profile, add the additional fields 52 + if (profile) { 53 + return { 54 + ...basicProfileView, 55 + handle: profile.authorHandle, 56 + displayName: profile.displayName, 57 + description: profile.description, 58 + avatar: profile.avatar?.ref?.link, 59 + indexedAt: profile.indexedAt, 60 + createdAt: profile.createdAt 61 + } 62 + } 63 + 64 + return basicProfileView 65 + }) 66 + ) 67 + 68 + // Get next cursor 69 + const nextCursor = follows.length === limit ? follows[follows.length - 1]._id : undefined 70 + 71 + // Get subject profile if it exists 72 + const subjectProfile = await ctx.db.models.Profile.findOne({ authorDid: actor }) 73 + const subjectProfileView: SoSprkActorDefs.ProfileView = { 74 + $type: 'so.sprk.actor.defs#profileView', 75 + did: actor, 76 + handle: 'unknown' 77 + } 78 + // If we found the subject profile, add the additional fields 79 + if (subjectProfile) { 80 + Object.assign(subjectProfileView, { 81 + handle: subjectProfile.authorHandle, 82 + displayName: subjectProfile.displayName, 83 + description: subjectProfile.description, 84 + avatar: subjectProfile.avatar?.ref?.link, 85 + indexedAt: subjectProfile.indexedAt, 86 + createdAt: subjectProfile.createdAt 87 + }) 88 + } else { 89 + let handle = null 90 + try { 91 + if (actor) { 92 + const didData = await ctx.resolver.resolveDidToDidDoc(actor) 93 + handle = didData.handle 94 + } 95 + } catch (error) { 96 + ctx.logger.warn( 97 + { did: actor, error: (error as Error).message }, 98 + 'Failed to resolve DID to handle', 99 + ) 100 + } 101 + Object.assign(subjectProfileView, { 102 + handle: handle ?? 'unknown' 103 + }) 104 + } 105 + 106 + return c.json({ 107 + subject: subjectProfileView, 108 + follows: profileViews, 109 + cursor: nextCursor 110 + }) 111 + }) 112 + 113 + return router 114 + }