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

implement getProfile

+252 -14
+4 -4
services/appview/src/db.ts
··· 90 90 uri: string 91 91 displayName?: string 92 92 description?: string 93 - avatar?: string 94 - banner?: string 93 + avatar?: Record<string, any> 94 + banner?: Record<string, any> 95 95 labels?: Record<string, any> 96 96 joinedViaStarterPack?: Record<string, any> 97 97 pinnedPost?: Record<string, any> ··· 106 106 uri: { type: String, required: true, unique: true, index: true }, 107 107 displayName: { type: String, required: false }, 108 108 description: { type: String, required: false }, 109 - avatar: { type: String, required: false }, 110 - banner: { type: String, required: false }, 109 + avatar: { type: Object, required: false }, 110 + banner: { type: Object, required: false }, 111 111 labels: { type: Object, required: false }, 112 112 joinedViaStarterPack: { type: Object, required: false }, 113 113 pinnedPost: { type: Object, required: false },
+15 -10
services/appview/src/index.ts
··· 1 - import { Database } from './db.js' 1 + import { DidResolver } from '@atproto/identity' 2 + import { serve } from '@hono/node-server' 3 + import { Hono } from 'hono' 4 + import { HTTPException } from 'hono/http-exception' 5 + import { logger } from 'hono/logger' 2 6 import { pino } from 'pino' 7 + import { optionalAuthMiddleware } from './auth/middleware.js' 8 + import { Database } from './db.js' 9 + import { env } from './env.js' 10 + import { createFeedRouter } from './feed/feed.js' 3 11 import { 4 12 BidirectionalResolver, 5 13 createBidirectionalResolver, 6 14 createIdResolver, 7 15 } from './id-resolver.js' 8 - import { Hono } from 'hono' 9 - import { logger } from 'hono/logger' 10 - import { env } from './env.js' 11 - import { serve } from '@hono/node-server' 12 - import { HTTPException } from 'hono/http-exception' 13 - import { authMiddleware, optionalAuthMiddleware } from './auth/middleware.js' 14 - import { createFeedRouter } from './feed/feed.js' 15 16 import { createGetPostsRouter } from './routes/getPosts.js' 16 17 import { createGetPostThreadRouter } from './routes/getPostThread.js' 18 + import { createGetProfileRouter } from './routes/getProfile.js' 17 19 import wellKnownRouter from './well-known.js' 18 - import { DidResolver } from '@atproto/identity' 19 20 20 21 export type AppContext = { 21 22 db: Database ··· 73 74 74 75 const getPostsRouter = createGetPostsRouter(ctx) 75 76 const getPostThreadRouter = createGetPostThreadRouter(ctx) 77 + const getProfileRouter = createGetProfileRouter(ctx) 76 78 app.route('/', getPostsRouter) 77 79 app.route('/', getPostThreadRouter) 80 + app.route('/', getProfileRouter) 78 81 79 82 app.route('/', wellKnownRouter()) 80 83 81 84 // Root route 82 85 app.get('/', (c) => { 83 - return c.text('✧・゚: ✧・゚:. ݁₊ ⊹ . ݁˖ . ݁ 𝚂𝙿𝙰𝚁𝙺 𝙰𝙿𝙸 . ݁₊ ⊹ . ݁˖ . ݁ :・゚✧:・゚✧') 86 + return c.text( 87 + '✧・゚: ✧・゚:. ݁₊ ⊹ . ݁˖ . ݁ 𝚂𝙿𝙰𝚁𝙺 𝙰𝙿𝙸 . ݁₊ ⊹ . ݁˖ . ݁ :・゚✧:・゚✧', 88 + ) 84 89 }) 85 90 86 91 app.onError((err, c) => {
+233
services/appview/src/routes/getProfile.ts
··· 1 + import { ensureValidDid, isValidHandle } from '@atproto/syntax' 2 + import { Hono } from 'hono' 3 + 4 + import { optionalAuthMiddleware } from '../auth/middleware.js' 5 + import { AppContext } from '../index.js' 6 + import type { Label } from '../lexicon/types/com/atproto/label/defs.js' 7 + import type * as ComAtprotoRepoStrongRef from '../lexicon/types/com/atproto/repo/strongRef.js' 8 + import type * as SoSprkActorDefs from '../lexicon/types/so/sprk/actor/defs.js' 9 + import type * as SoSprkGraphDefs from '../lexicon/types/so/sprk/graph/defs.js' 10 + 11 + export const createGetProfileRouter = (ctx: AppContext) => { 12 + const router = new Hono() 13 + 14 + router.get( 15 + '/xrpc/so.sprk.actor.getProfile', 16 + optionalAuthMiddleware, 17 + async (c) => { 18 + const actor = c.req.query('actor') 19 + const viewerDid = c.get('did') as string | undefined 20 + 21 + if (!actor) { 22 + return c.json({ error: 'Actor not provided' }, 400) 23 + } 24 + 25 + let actorDidDoc 26 + if (isValidHandle(actor)) { 27 + actorDidDoc = await ctx.resolver.resolveHandleToDidDoc(actor) 28 + } else { 29 + try { 30 + ensureValidDid(actor) 31 + actorDidDoc = await ctx.resolver.resolveDidToDidDoc(actor) 32 + } catch (err) { 33 + return c.json({ error: 'Invalid actor' }, 400) 34 + } 35 + } 36 + 37 + const actorDid = actorDidDoc.did 38 + 39 + // Get profile data 40 + const profile = await ctx.db.models.Profile.findOne({ 41 + authorDid: actorDid, 42 + }).lean() 43 + 44 + if (!profile) { 45 + return c.json({ error: 'Profile not found' }, 404) 46 + } 47 + 48 + const profileHandle = await ctx.resolver.resolveDidToHandle( 49 + profile.authorDid, 50 + ) 51 + 52 + // Get follower count 53 + const followersCount = await ctx.db.models.Follow.countDocuments({ 54 + subject: actorDid, 55 + }) 56 + 57 + // Get follows count 58 + const followsCount = await ctx.db.models.Follow.countDocuments({ 59 + authorDid: actorDid, 60 + }) 61 + 62 + // Get posts count 63 + const postsCount = await ctx.db.models.Post.countDocuments({ 64 + authorDid: actorDid, 65 + }) 66 + 67 + // Build viewer state if a user is authenticated 68 + const viewer: SoSprkActorDefs.ViewerState = {} 69 + 70 + if (viewerDid) { 71 + // Check if viewer follows this profile 72 + const follow = await ctx.db.models.Follow.findOne({ 73 + subject: actorDid, 74 + authorDid: viewerDid, 75 + }) 76 + if (follow) { 77 + viewer.following = follow.uri 78 + } 79 + 80 + // Check if this profile follows the viewer 81 + const followedBy = await ctx.db.models.Follow.findOne({ 82 + subject: viewerDid, 83 + authorDid: actorDid, 84 + }) 85 + if (followedBy) { 86 + viewer.followedBy = followedBy.uri 87 + } 88 + 89 + // Check if viewer has blocked this profile 90 + const block = await ctx.db.models.Block.findOne({ 91 + subject: actorDid, 92 + authorDid: viewerDid, 93 + }) 94 + if (block) { 95 + viewer.blocking = block.uri 96 + } 97 + 98 + // Check if this profile has blocked the viewer 99 + const blockedBy = await ctx.db.models.Block.findOne({ 100 + subject: viewerDid, 101 + authorDid: actorDid, 102 + }) 103 + if (blockedBy) { 104 + viewer.blockedBy = true 105 + } 106 + 107 + // Get known followers (followers of the profile that the viewer also follows) 108 + if (followersCount > 0) { 109 + // Get the followers of this profile 110 + const followers = await ctx.db.models.Follow.find({ 111 + subject: actorDid, 112 + }).lean() 113 + 114 + const followerDids = followers.map((f) => f.authorDid) 115 + 116 + // Check which of these followers the viewer follows 117 + const knownFollowsQuery = await ctx.db.models.Follow.find({ 118 + subject: { $in: followerDids }, 119 + authorDid: viewerDid, 120 + }).lean() 121 + 122 + if (knownFollowsQuery.length > 0) { 123 + const knownFollowerDids = knownFollowsQuery.map((f) => f.subject) 124 + 125 + // Get profiles for known followers 126 + const knownFollowerProfiles = await ctx.db.models.Profile.find({ 127 + authorDid: { $in: knownFollowerDids }, 128 + }) 129 + .limit(3) 130 + .lean() 131 + 132 + const knownFollowersBasic = await Promise.all( 133 + knownFollowerProfiles.map(async (p) => { 134 + const handle = await ctx.resolver.resolveDidToHandle( 135 + p.authorDid, 136 + ) 137 + return { 138 + did: p.authorDid, 139 + handle, 140 + displayName: p.displayName, 141 + avatar: p.avatar 142 + ? `https://cdn.sprk.so/avatar/${p.authorDid}` 143 + : undefined, 144 + } as SoSprkActorDefs.ProfileViewBasic 145 + }), 146 + ) 147 + 148 + viewer.knownFollowers = { 149 + count: knownFollowsQuery.length, 150 + followers: knownFollowersBasic, 151 + } 152 + } 153 + } 154 + } 155 + 156 + // Check for associated services 157 + const associated: SoSprkActorDefs.ProfileAssociated = {} 158 + 159 + // Check for feed generators 160 + let feedgensCount = 0 161 + try { 162 + if (ctx.db.models.Generator) { 163 + feedgensCount = await ctx.db.models.Generator.countDocuments({ 164 + authorDid: actorDid, 165 + }) 166 + } 167 + } catch (error) { 168 + // Ignore if model doesn't exist 169 + } 170 + 171 + if (feedgensCount > 0) { 172 + associated.feedgens = feedgensCount 173 + } 174 + 175 + // Get avatar and banner URLs 176 + const avatar = profile.avatar 177 + ? `https://cdn.sprk.so/avatar/${actorDid}/${profile.avatar.ref.$link}` 178 + : undefined 179 + const banner = profile.banner 180 + ? `https://cdn.sprk.so/banner/${actorDid}/${profile.banner.ref.$link}` 181 + : undefined 182 + 183 + // Convert joinedViaStarterPack to the correct type if it exists 184 + let joinedViaStarterPack: 185 + | SoSprkGraphDefs.StarterPackViewBasic 186 + | undefined = undefined 187 + if (profile.joinedViaStarterPack) { 188 + // Type assertion assuming the structure fits the requirements 189 + joinedViaStarterPack = 190 + profile.joinedViaStarterPack as unknown as SoSprkGraphDefs.StarterPackViewBasic 191 + } 192 + 193 + // Convert labels to the correct type if it exists 194 + let labels: Label[] | undefined = undefined 195 + if (profile.labels) { 196 + labels = Array.isArray(profile.labels) 197 + ? (profile.labels as Label[]) 198 + : undefined 199 + } 200 + 201 + // Convert pinnedPost to the correct type if it exists 202 + let pinnedPost: ComAtprotoRepoStrongRef.Main | undefined = undefined 203 + if (profile.pinnedPost) { 204 + pinnedPost = 205 + profile.pinnedPost as unknown as ComAtprotoRepoStrongRef.Main 206 + } 207 + 208 + // Build the ProfileViewDetailed response 209 + const profileView: SoSprkActorDefs.ProfileViewDetailed = { 210 + did: actorDid, 211 + handle: profileHandle, 212 + displayName: profile.displayName, 213 + description: profile.description, 214 + avatar, 215 + banner, 216 + followersCount, 217 + followsCount, 218 + postsCount, 219 + associated: Object.keys(associated).length > 0 ? associated : undefined, 220 + joinedViaStarterPack, 221 + indexedAt: profile.indexedAt, 222 + createdAt: profile.createdAt, 223 + viewer: Object.keys(viewer).length > 0 ? viewer : undefined, 224 + labels, 225 + pinnedPost, 226 + } 227 + 228 + return c.json(profileView) 229 + }, 230 + ) 231 + 232 + return router 233 + }