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

dataplane toplevel (#23)

authored by

Roscoe Rubin-Rottenberg and committed by
GitHub
fecff8f8 51651be1

+33 -172
+4 -3
services/appview/api/so/sprk/actor/getProfile.ts
··· 4 4 import type * as ComAtprotoRepoStrongRef from "../../../../lexicon/types/com/atproto/repo/strongRef.ts"; 5 5 import type * as SoSprkActorDefs from "../../../../lexicon/types/so/sprk/actor/defs.ts"; 6 6 import { AppContext } from "../../../../main.ts"; 7 + import { StoryDocument } from "../../../../data-plane/server/index.ts"; 7 8 8 9 export default function (server: Server, ctx: AppContext) { 9 10 server.so.sprk.actor.getProfile({ ··· 174 175 .sort({ indexedAt: -1 }) 175 176 .limit(15) 176 177 .lean() 177 - .catch((error) => { 178 + .catch((error: Error) => { 178 179 ctx.logger.warn( 179 180 { error, actorDid }, 180 181 "Failed to fetch stories for profile", ··· 187 188 { $match: { subject: actorDid } }, 188 189 { $group: { _id: "$authorDid" } }, 189 190 { $count: "total" }, 190 - ]).then((result) => result[0]?.total || 0), 191 + ]).then((result: { total: number }[]) => result[0]?.total || 0), 191 192 192 193 // Count follows based on actor's follow mode preference 193 194 ctx.db.models.Follow.countDocuments({ ··· 203 204 204 205 // Convert recent stories to strongRefs 205 206 const stories: ComAtprotoRepoStrongRef.Main[] = recentStories.map( 206 - (story) => ({ 207 + (story: StoryDocument) => ({ 207 208 uri: story.uri, 208 209 cid: story.cid, 209 210 }),
+2 -1
services/appview/api/so/sprk/feed/getPosts.ts
··· 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { OutputSchema as GetPostsView } from "../../../../lexicon/types/so/sprk/feed/getPosts.ts"; 4 4 import { transformPostToPostView } from "../../../../utils/post-transformer.ts"; 5 + import { PostDocument } from "../../../../data-plane/server/index.ts"; 5 6 6 7 export default function (server: Server, ctx: AppContext) { 7 8 server.so.sprk.feed.getPosts({ ··· 25 26 26 27 // Transform each post to PostView format 27 28 const postViews = await Promise.all( 28 - dbPosts.map((post) => transformPostToPostView(post, ctx.db, userDid)), 29 + dbPosts.map((post: PostDocument) => transformPostToPostView(post, ctx.db, userDid)), 29 30 ); 30 31 31 32 return {
+2 -2
services/appview/api/so/sprk/feed/getStories.ts
··· 2 2 import { OutputSchema as GetStoriesView } from "../../../../lexicon/types/so/sprk/feed/getStories.ts"; 3 3 import { Server } from "../../../../lexicon/index.ts"; 4 4 import { AppContext } from "../../../../main.ts"; 5 - import { Database } from "../../../../services/data-plane/server/index.ts"; 5 + import { Database, StoryDocument } from "../../../../data-plane/server/index.ts"; 6 6 import { transformStoryToStoryView } from "../../../../utils/story-transformer.ts"; 7 7 8 8 // Function to fetch stories by URIs ··· 26 26 27 27 // Transform each story to StoryView format 28 28 const storyViews = await Promise.all( 29 - dbStories.map((story) => transformStoryToStoryView(story, db)), 29 + dbStories.map((story: StoryDocument) => transformStoryToStoryView(story, db)), 30 30 ); 31 31 32 32 return storyViews;
+3 -3
services/appview/api/so/sprk/feed/getStoriesTimeline.ts
··· 3 3 import { Server } from "../../../../lexicon/index.ts"; 4 4 import { AppContext } from "../../../../main.ts"; 5 5 import { RootFilterQuery } from "mongoose"; 6 - import type { StoryDocument } from "../../../../services/data-plane/server/index.ts"; 6 + import { FollowDocument, StoryDocument } from "../../../../data-plane/server/index.ts"; 7 7 import { Buffer } from "node:buffer"; 8 8 import type { ProfileViewBasic } from "../../../../lexicon/types/so/sprk/actor/defs.ts"; 9 9 import type * as SoSprkFeedDefs from "../../../../lexicon/types/so/sprk/feed/defs.ts"; ··· 39 39 }; 40 40 } 41 41 42 - const followedDids = follows.map((follow) => follow.subject); 42 + const followedDids = follows.map((follow: FollowDocument) => follow.subject); 43 43 44 44 const twentyFourHoursAgo = new Date(); 45 45 twentyFourHoursAgo.setHours(twentyFourHoursAgo.getHours() - 24); ··· 89 89 90 90 // Transform stories to story views 91 91 const storyViews = await Promise.all( 92 - stories.map(async (story) => { 92 + stories.map(async (story: StoryDocument) => { 93 93 return await transformStoryToStoryView(story, ctx.db); 94 94 }), 95 95 );
+2 -1
services/appview/api/so/sprk/graph/getFollowers.ts
··· 1 1 import { Server } from "../../../../lexicon/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import type * as SoSprkActorDefs from "../../../../lexicon/types/so/sprk/actor/defs.ts"; 4 + import { FollowDocument } from "../../../../data-plane/server/index.ts"; 4 5 5 6 export default function (server: Server, ctx: AppContext) { 6 7 server.so.sprk.graph.getFollowers({ ··· 35 36 36 37 // Get profile views for each follower 37 38 const profileViews = await Promise.all( 38 - followers.map(async (follow) => { 39 + followers.map(async (follow: FollowDocument) => { 39 40 const profile = await ctx.db.models.Profile.findOne({ 40 41 authorDid: follow.authorDid, 41 42 });
+2 -1
services/appview/api/so/sprk/graph/getFollows.ts
··· 1 1 import { Server } from "../../../../lexicon/index.ts"; 2 + import { FollowDocument } from "../../../../data-plane/server/index.ts"; 2 3 import { PipelineStage } from "mongoose"; 3 4 import { AppContext } from "../../../../main.ts"; 4 5 import type * as SoSprkActorDefs from "../../../../lexicon/types/so/sprk/actor/defs.ts"; ··· 85 86 86 87 // Get profile views for each follow 87 88 const profileViews = await Promise.all( 88 - follows.map(async (follow) => { 89 + follows.map(async (follow: FollowDocument) => { 89 90 const profile = await ctx.db.models.Profile.findOne({ 90 91 authorDid: follow.subject, 91 92 });
+3 -3
services/appview/main.ts
··· 3 3 import { HTTPException } from "hono/http-exception"; 4 4 import { logger } from "hono/logger"; 5 5 import { pino } from "pino"; 6 - import { Database } from "./services/data-plane/server/index.ts"; 6 + import { Database } from "./data-plane/server/index.ts"; 7 7 import { env } from "./utils/env.ts"; 8 - import { createAuthVerifier } from "./services/auth/auth-verifier.ts"; 8 + import { createAuthVerifier } from "./services/auth-verifier.ts"; 9 9 import API from "./api/index.ts"; 10 10 import { createServer } from "./lexicon/index.ts"; 11 11 import { ··· 18 18 import { IndexingService } from "./services/indexing.ts"; 19 19 import { BidirectionalResolver } from "./utils/id-resolver.ts"; 20 20 import { DidResolver } from "@atproto/identity"; 21 - import { AuthVerifier } from "./services/auth/auth-verifier.ts"; 21 + import { AuthVerifier } from "./services/auth-verifier.ts"; 22 22 import { AuthRequiredError } from "@sprk/xrpc-server"; 23 23 24 24 // Setup logger and database
services/appview/services/auth/auth-verifier.ts services/appview/services/auth-verifier.ts
-144
services/appview/services/auth/middleware.ts
··· 1 - import { Context, Next } from "hono"; 2 - import { HTTPException } from "hono/http-exception"; 3 - import { verifyJwt } from "@sprk/xrpc-server"; 4 - import { DidResolver } from "@atproto/identity"; 5 - import { env } from "../../utils/env.ts"; 6 - import { decodeBase64 } from "@std/encoding"; 7 - 8 - /** 9 - * Authentication middleware for ATP agents 10 - * 11 - * @param c - Hono context 12 - * @param next - Next middleware function 13 - * @param adminRequired - Whether admin privileges are required (checks admin token) 14 - */ 15 - export const authMiddleware = async ( 16 - c: Context, 17 - next: Next, 18 - adminRequired = false, 19 - ) => { 20 - const authHeader = c.req.header("Authorization"); 21 - 22 - if (!authHeader) { 23 - throw new HTTPException(401, { 24 - message: "Unauthorized: Missing Authorization header", 25 - }); 26 - } 27 - 28 - try { 29 - if (authHeader.startsWith("Basic ")) { 30 - const base64Credentials = authHeader.replace("Basic ", "").trim(); 31 - const decodedBytes = decodeBase64(base64Credentials); 32 - const credentials = new TextDecoder().decode(decodedBytes); 33 - const [username, password] = credentials.split(":"); 34 - 35 - console.log("Basic auth attempt:", { 36 - username, 37 - password, 38 - expected: env.ADMIN_PASSWORD, 39 - }); 40 - 41 - if (username === "admin" && password === env.ADMIN_PASSWORD) { 42 - c.set("isAdmin", true); 43 - await next(); 44 - return; 45 - } else { 46 - throw new HTTPException(401, { 47 - message: "Unauthorized: Invalid admin credentials", 48 - }); 49 - } 50 - } else if (authHeader.startsWith("Bearer ")) { 51 - const jwt = authHeader.replace("Bearer ", "").trim(); 52 - 53 - // The service DID and resolver should be passed from app context 54 - const serviceDid = c.env.serviceDid as string; 55 - const didResolver = c.env.didResolver as DidResolver; 56 - 57 - const parsed = await verifyJwt( 58 - jwt, 59 - serviceDid, 60 - null, 61 - (did: string) => { 62 - return didResolver.resolveAtprotoKey(did); 63 - }, 64 - ); 65 - 66 - // Set auth information in the context for route handlers to access 67 - c.set("did", parsed.iss); 68 - c.set("accessJwt", jwt); 69 - 70 - // Check for admin status if required 71 - if (adminRequired) { 72 - if (!c.get("isAdmin")) { 73 - throw new HTTPException(403, { 74 - message: 75 - "Forbidden: Admin privileges required - use Basic auth with admin token", 76 - }); 77 - } 78 - } 79 - 80 - await next(); 81 - } else { 82 - throw new HTTPException(401, { 83 - message: 84 - 'Unauthorized: Authorization header must start with "Basic " or "Bearer "', 85 - }); 86 - } 87 - } catch (err) { 88 - if (err instanceof HTTPException) { 89 - throw err; 90 - } 91 - throw new HTTPException(401, { 92 - message: "Unauthorized: Invalid credentials", 93 - }); 94 - } 95 - }; 96 - 97 - /** 98 - * Optional authentication middleware - doesn't throw on missing/invalid auth 99 - * Still sets isAdmin flag if the user has admin privileges 100 - */ 101 - export const optionalAuthMiddleware = async (c: Context, next: Next) => { 102 - const authHeader = c.req.header("Authorization"); 103 - 104 - if (authHeader) { 105 - if (authHeader.startsWith("Basic ")) { 106 - try { 107 - const base64Credentials = authHeader.replace("Basic ", "").trim(); 108 - const decodedBytes = decodeBase64(base64Credentials); 109 - const credentials = new TextDecoder().decode(decodedBytes); 110 - const [username, password] = credentials.split(":"); 111 - 112 - if (username === "admin" && password === env.ADMIN_PASSWORD) { 113 - c.set("isAdmin", true); 114 - } 115 - } catch { 116 - // On auth failure, just continue without setting admin context 117 - } 118 - } else if (authHeader.startsWith("Bearer ")) { 119 - const jwt = authHeader.replace("Bearer ", "").trim(); 120 - 121 - try { 122 - const serviceDid = c.get("serviceDid"); 123 - const didResolver = c.get("didResolver") as DidResolver; 124 - 125 - const parsed = await verifyJwt( 126 - jwt, 127 - serviceDid, 128 - null, 129 - (did: string) => { 130 - return didResolver.resolveAtprotoKey(did); 131 - }, 132 - ); 133 - 134 - // Set auth information if JWT is valid 135 - c.set("did", parsed.iss); 136 - c.set("accessJwt", jwt); 137 - } catch { 138 - // On auth failure, just continue without setting auth context 139 - } 140 - } 141 - } 142 - 143 - await next(); 144 - };
services/appview/services/data-plane/client/hosts.ts services/appview/data-plane/client/hosts.ts
services/appview/services/data-plane/client/index.ts services/appview/data-plane/client/index.ts
services/appview/services/data-plane/client/util.ts services/appview/data-plane/client/util.ts
services/appview/services/data-plane/server/index.ts services/appview/data-plane/server/index.ts
+2 -2
services/appview/services/indexing.ts
··· 2 2 import { CID } from "multiformats/cid"; 3 3 import { Document } from "mongoose"; 4 4 import { BidirectionalResolver } from "../utils/id-resolver.ts"; 5 - import { Database } from "../services/data-plane/server/index.ts"; 5 + import { Database } from "../data-plane/server/index.ts"; 6 6 import { pino } from "pino"; 7 7 import * as Post from "./plugins/post.ts"; 8 8 import * as BskyFollow from "./plugins/bskyFollow.ts"; 9 9 import { Agent } from "@atproto/api"; 10 - import { ActorDocument } from "../services/data-plane/server/index.ts"; 10 + import { ActorDocument } from "../data-plane/server/index.ts"; 11 11 12 12 // Generic type for model processors 13 13 type RecordProcessor = {
+1 -1
services/appview/services/plugins/bskyFollow.ts
··· 4 4 import { 5 5 Database, 6 6 FollowDocument, 7 - } from "../../services/data-plane/server/index.ts"; 7 + } from "../../data-plane/server/index.ts"; 8 8 9 9 const logger = pino({ name: "bsky-follow-processor" }); 10 10
+1 -1
services/appview/services/plugins/post.ts
··· 4 4 import { 5 5 Database, 6 6 PostDocument, 7 - } from "../../services/data-plane/server/index.ts"; 7 + } from "../../data-plane/server/index.ts"; 8 8 9 9 const logger = pino({ name: "post-processor" }); 10 10
+4 -4
services/appview/services/takedown.ts
··· 1 - import { Database } from "../services/data-plane/server/index.ts"; 1 + import { Database, BlobTakedownDocument, RepoTakedownDocument, TakedownDocument } from "../data-plane/server/index.ts"; 2 2 3 3 export class TakedownService { 4 4 constructor(private db: Database) {} ··· 139 139 const items = takedowns.slice(0, limit); 140 140 141 141 return { 142 - takedowns: items.map((t) => ({ 142 + takedowns: items.map((t: TakedownDocument) => ({ 143 143 targetUri: t.targetUri, 144 144 targetCid: t.targetCid, 145 145 reason: t.reason, ··· 174 174 const items = takedowns.slice(0, limit); 175 175 176 176 return { 177 - repoTakedowns: items.map((t) => ({ 177 + repoTakedowns: items.map((t: RepoTakedownDocument) => ({ 178 178 did: t.did, 179 179 reason: t.reason, 180 180 takenDownBy: t.takenDownBy, ··· 208 208 const items = takedowns.slice(0, limit); 209 209 210 210 return { 211 - blobTakedowns: items.map((t) => ({ 211 + blobTakedowns: items.map((t: BlobTakedownDocument) => ({ 212 212 did: t.did, 213 213 cid: t.cid, 214 214 reason: t.reason,
+2 -2
services/appview/utils/embed-transformer.ts
··· 1 1 import type * as SoSprkEmbedImages from "../lexicon/types/so/sprk/embed/images.ts"; 2 - import { PostEmbed } from "../services/data-plane/server/index.ts"; 2 + import { PostEmbed, EmbedImage } from "../data-plane/server/index.ts"; 3 3 4 4 interface ImageTransformOptions { 5 5 /** If true, only return the first image (useful for stories) */ ··· 24 24 return { 25 25 $type: "so.sprk.embed.images#view", 26 26 images: imagesToProcess.map( 27 - (img): SoSprkEmbedImages.ViewImage => ({ 27 + (img: EmbedImage): SoSprkEmbedImages.ViewImage => ({ 28 28 thumb: 29 29 `https://media.sprk.so/img/medium/${authorDid}/${img.image.ref.$link}/webp`, 30 30 fullsize:
+1 -1
services/appview/utils/post-transformer.ts
··· 1 - import { Database, PostDocument } from "../services/data-plane/server/index.ts"; 1 + import { Database, PostDocument } from "../data-plane/server/index.ts"; 2 2 import type { Label } from "../lexicon/types/com/atproto/label/defs.ts"; 3 3 import type * as SoSprkFeedDefs from "../lexicon/types/so/sprk/feed/defs.ts"; 4 4 import type * as SoSprkFeedPost from "../lexicon/types/so/sprk/feed/post.ts";
+3 -2
services/appview/utils/profile-helper.ts
··· 1 - import { Database } from "../services/data-plane/server/index.ts"; 1 + import { Database } from "../data-plane/server/index.ts"; 2 2 import type { ProfileViewBasic } from "../lexicon/types/so/sprk/actor/defs.ts"; 3 3 import type * as ComAtprotoRepoStrongRef from "../lexicon/types/com/atproto/repo/strongRef.ts"; 4 + import type { StoryDocument } from "../data-plane/server/index.ts"; 4 5 5 6 // Helper function to create ProfileViewBasic with stories 6 7 export async function createProfileViewBasic( ··· 32 33 .lean(); 33 34 34 35 // Convert recent stories to strongRefs 35 - stories = recentStories.map((story) => ({ 36 + stories = recentStories.map((story: StoryDocument) => ({ 36 37 uri: story.uri, 37 38 cid: story.cid, 38 39 }));
+1 -1
services/appview/utils/story-transformer.ts
··· 2 2 import { 3 3 Database, 4 4 StoryDocument, 5 - } from "../services/data-plane/server/index.ts"; 5 + } from "../data-plane/server/index.ts"; 6 6 import { transformEmbed } from "./embed-transformer.ts"; 7 7 import { createProfileViewBasic } from "./profile-helper.ts"; 8 8