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

Pipeline: data-plane, hydration, and views (#41)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

authored by

Roscoe Rubin-Rottenberg
Copilot
and committed by
GitHub
b63f3340 c63b8a75

+5705 -1962
+3 -3
api/com/atproto/admin/getAccountInfos.ts
··· 1 1 import { AppContext } from "../../../../main.ts"; 2 - import { mapDefined } from "@atproto/common"; 3 - import { INVALID_HANDLE } from "@atproto/syntax"; 2 + import { mapDefined } from "@atp/common"; 3 + import { INVALID_HANDLE } from "@atp/syntax"; 4 4 import { Server } from "../../../../lex/index.ts"; 5 - import { AuthRequiredError } from "@sprk/xrpc-server"; 5 + import { AuthRequiredError } from "@atp/xrpc-server"; 6 6 7 7 export default function (server: Server, ctx: AppContext) { 8 8 server.com.atproto.admin.getAccountInfos({
+1 -1
api/com/atproto/admin/getSubjectStatus.ts
··· 1 1 import { AppContext } from "../../../../main.ts"; 2 2 import { Server } from "../../../../lex/index.ts"; 3 - import { AuthRequiredError, XRPCError } from "@sprk/xrpc-server"; 3 + import { AuthRequiredError, XRPCError } from "@atp/xrpc-server"; 4 4 5 5 export default function (server: Server, ctx: AppContext) { 6 6 server.com.atproto.admin.getSubjectStatus({
+1 -1
api/com/atproto/admin/updateSubjectStatus.ts
··· 3 3 import { Server } from "../../../../lex/index.ts"; 4 4 import type * as ComAtprotoAdminDefs from "../../../../lex/types/com/atproto/admin/defs.ts"; 5 5 import type * as ComAtprotoRepoStrongRef from "../../../../lex/types/com/atproto/repo/strongRef.ts"; 6 - import { AuthRequiredError } from "@sprk/xrpc-server"; 6 + import { AuthRequiredError } from "@atp/xrpc-server"; 7 7 8 8 export default function (server: Server, ctx: AppContext) { 9 9 server.com.atproto.admin.updateSubjectStatus({
+2 -2
api/com/atproto/identity/resolveHandle.ts
··· 1 - import * as ident from "@atproto/syntax"; 2 - import { InvalidRequestError } from "@sprk/xrpc-server"; 1 + import * as ident from "@atp/syntax"; 2 + import { InvalidRequestError } from "@atp/xrpc-server"; 3 3 import { Server } from "../../../../lex/index.ts"; 4 4 import { AppContext } from "../../../../main.ts"; 5 5
+24 -86
api/com/atproto/repo/getRecord.ts
··· 1 - import { AtUri } from "@atproto/syntax"; 2 - import { InvalidRequestError } from "@sprk/xrpc-server"; 3 - import { Server } from "../../../../lex/index.ts"; 1 + import { AtUri } from "@atp/syntax"; 2 + import { InvalidRequestError } from "@atp/xrpc-server"; 4 3 import { AppContext } from "../../../../main.ts"; 5 - import { OutputSchema } from "../../../../lex/types/com/atproto/repo/getRecord.ts"; 6 - 7 - interface TakedownInfo { 8 - reason: string; 9 - takenDownBy: string; 10 - takenDownAt: string; 11 - warning: string; 12 - } 4 + import { Server } from "../../../../lex/index.ts"; 13 5 14 6 export default function (server: Server, ctx: AppContext) { 15 7 server.com.atproto.repo.getRecord({ 16 8 auth: ctx.authVerifier.optionalStandardOrRole, 17 - handler: async ({ params, auth }) => { 9 + handler: async ({ auth, params }) => { 18 10 const { repo, collection, rkey, cid } = params; 19 11 const { includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 - 21 - if (!repo || !collection || !rkey) { 22 - throw new InvalidRequestError("Missing required parameters"); 23 - } 24 - 25 - // Resolve the handle to DID if needed 26 - let did; 27 - try { 28 - if (repo.startsWith("did:")) { 29 - did = repo; 30 - } else { 31 - // Assume it's a handle 32 - const didDoc = await ctx.resolver.resolveHandleToDidDoc(repo); 33 - did = didDoc.did; 34 - } 35 - } catch { 12 + const [did] = await ctx.hydrator.actor.getDids([repo]); 13 + if (!did) { 36 14 throw new InvalidRequestError(`Could not find repo: ${repo}`); 37 15 } 38 16 39 - if (!did) { 17 + const actors = await ctx.hydrator.actor.getActors([did], { 18 + includeTakedowns, 19 + }); 20 + if (!actors.get(did)) { 40 21 throw new InvalidRequestError(`Could not find repo: ${repo}`); 41 22 } 42 23 43 - // Create the URI 44 24 const uri = AtUri.make(did, collection, rkey).toString(); 45 - 46 - try { 47 - const record = await ctx.db.models.Record.findOne({ uri }).lean(); 48 - 49 - if (!record || (cid && record.cid !== cid)) { 50 - // For admins, provide more detailed information about what we tried to query 51 - if (includeTakedowns) { 52 - ctx.logger.info("Admin record lookup failed", { 53 - uri, 54 - collection, 55 - did, 56 - rkey, 57 - cid, 58 - foundRecord: !!record, 59 - cidMatch: record ? (cid ? record.cid === cid : true) : false, 60 - }); 61 - } 62 - throw new InvalidRequestError(`Could not locate record: ${uri}`); 63 - } 64 - 65 - // Parse the original record JSON 66 - const recordValue = record.json; 67 - 68 - // Check if the record is subject to a takedown 69 - const takedown = await ctx.takedownService.getTakedown(uri); 25 + const result = await ctx.hydrator.getRecord(uri, includeTakedowns); 70 26 71 - // If record is taken down and user is not an admin, deny access 72 - if (takedown && !includeTakedowns) { 73 - throw new InvalidRequestError(`Record is taken down: ${uri}`); 74 - } 27 + if (!result || (cid && result.cid !== cid)) { 28 + throw new InvalidRequestError( 29 + `Could not locate record: ${uri}`, 30 + "RecordNotFound", 31 + ); 32 + } 75 33 76 - // Format the response according to the output schema 77 - const response: OutputSchema & { takedown?: TakedownInfo } = { 34 + return { 35 + encoding: "application/json" as const, 36 + body: { 78 37 uri: uri, 79 - cid: record.cid, 80 - value: recordValue, 81 - }; 82 - 83 - // Include takedown info for admins 84 - if (includeTakedowns && takedown) { 85 - response.takedown = { 86 - reason: takedown.reason, 87 - takenDownBy: takedown.takenDownBy, 88 - takenDownAt: takedown.takenDownAt, 89 - warning: 90 - "This content has been taken down and is only visible to admins", 91 - }; 92 - } 93 - 94 - return { 95 - encoding: "application/json", 96 - body: response, 97 - }; 98 - } catch (err) { 99 - if (err instanceof InvalidRequestError) { 100 - throw err; 101 - } 102 - throw new InvalidRequestError(`Error retrieving record: ${uri}`); 103 - } 38 + cid: result.cid, 39 + value: result.record, 40 + }, 41 + }; 104 42 }, 105 43 }); 106 44 }
+89 -10
api/so/sprk/actor/getProfile.ts
··· 1 + import { InvalidRequestError } from "@atp/xrpc-server"; 2 + import { AppContext } from "../../../../main.ts"; 3 + import { 4 + HydrateCtx, 5 + HydrationState, 6 + Hydrator, 7 + } from "../../../../hydration/index.ts"; 1 8 import { Server } from "../../../../lex/index.ts"; 2 - import { AppContext } from "../../../../main.ts"; 3 - import { getProfile } from "../../../../utils/profile-helper.ts"; 9 + import { QueryParams } from "../../../../lex/types/so/sprk/actor/getProfile.ts"; 10 + import { createPipeline, noRules } from "../../../../pipeline.ts"; 11 + import { Views } from "../../../../views/index.ts"; 12 + import { resHeaders } from "../../../util.ts"; 4 13 5 14 export default function (server: Server, ctx: AppContext) { 15 + const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 6 16 server.so.sprk.actor.getProfile({ 7 - auth: ctx.authVerifier.standardOptional, 8 - handler: async ({ params, auth }) => { 9 - const { actor: actorParam } = params; 10 - const viewerDid = auth.credentials.type === "standard" 11 - ? auth.credentials.iss 12 - : undefined; 17 + auth: ctx.authVerifier.optionalStandardOrRole, 18 + handler: async ({ auth, params }) => { 19 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 + const hydrateCtx = ctx.hydrator.createContext({ 21 + viewer, 22 + includeTakedowns, 23 + }); 24 + 25 + const result = await getProfile({ ...params, hydrateCtx }, ctx); 13 26 14 - const profileView = await getProfile(ctx, actorParam, viewerDid); 27 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 15 28 16 29 return { 17 30 encoding: "application/json", 18 - body: profileView, 31 + body: result, 32 + headers: resHeaders({ 33 + repoRev, 34 + }), 19 35 }; 20 36 }, 21 37 }); 22 38 } 39 + 40 + const skeleton = async (input: { 41 + ctx: Context; 42 + params: Params; 43 + }): Promise<SkeletonState> => { 44 + const { ctx, params } = input; 45 + const [did] = await ctx.hydrator.actor.getDids([params.actor]); 46 + if (!did) { 47 + throw new InvalidRequestError("Profile not found"); 48 + } 49 + return { did }; 50 + }; 51 + 52 + const hydration = (input: { 53 + ctx: Context; 54 + params: Params; 55 + skeleton: SkeletonState; 56 + }) => { 57 + const { ctx, params, skeleton } = input; 58 + return ctx.hydrator.hydrateProfilesDetailed( 59 + [skeleton.did], 60 + params.hydrateCtx.copy({ 61 + includeActorTakedowns: true, 62 + }), 63 + ); 64 + }; 65 + 66 + const presentation = (input: { 67 + ctx: Context; 68 + params: Params; 69 + skeleton: SkeletonState; 70 + hydration: HydrationState; 71 + }) => { 72 + const { ctx, params, skeleton, hydration } = input; 73 + const profile = ctx.views.profileDetailed(skeleton.did, hydration); 74 + if (!profile) { 75 + throw new InvalidRequestError("Profile not found"); 76 + } else if (!params.hydrateCtx.includeTakedowns) { 77 + if (ctx.views.actorIsTakendown(skeleton.did, hydration)) { 78 + throw new InvalidRequestError( 79 + "Account has been suspended", 80 + "AccountTakedown", 81 + ); 82 + } else if (ctx.views.actorIsDeactivated(skeleton.did, hydration)) { 83 + throw new InvalidRequestError( 84 + "Account is deactivated", 85 + "AccountDeactivated", 86 + ); 87 + } 88 + } 89 + return profile; 90 + }; 91 + 92 + type Context = { 93 + hydrator: Hydrator; 94 + views: Views; 95 + }; 96 + 97 + type Params = QueryParams & { 98 + hydrateCtx: HydrateCtx; 99 + }; 100 + 101 + type SkeletonState = { did: string };
+69 -11
api/so/sprk/actor/getProfiles.ts
··· 1 - import { Server } from "../../../../lex/index.ts"; 1 + import { mapDefined } from "@atp/common"; 2 2 import { AppContext } from "../../../../main.ts"; 3 - import { getProfiles } from "../../../../utils/profile-helper.ts"; 3 + import { 4 + HydrateCtx, 5 + HydrationState, 6 + Hydrator, 7 + } from "../../../../hydration/index.ts"; 8 + import { Server } from "../../../../lex/index.ts"; 9 + import { QueryParams } from "../../../../lex/types/so/sprk/actor/getProfiles.ts"; 10 + import { createPipeline, noRules } from "../../../../pipeline.ts"; 11 + import { Views } from "../../../../views/index.ts"; 12 + import { resHeaders } from "../../../util.ts"; 4 13 5 14 export default function (server: Server, ctx: AppContext) { 15 + const getProfile = createPipeline(skeleton, hydration, noRules, presentation); 6 16 server.so.sprk.actor.getProfiles({ 7 17 auth: ctx.authVerifier.standardOptional, 8 - handler: async ({ params, auth }) => { 9 - const { actors: actorParams } = params; 10 - const viewerDid = auth.credentials.type === "standard" 11 - ? auth.credentials.iss 12 - : undefined; 18 + handler: async ({ auth, params }) => { 19 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 20 + const hydrateCtx = ctx.hydrator.createContext({ 21 + viewer, 22 + includeTakedowns, 23 + }); 13 24 14 - const profiles = await getProfiles(ctx, actorParams, viewerDid); 25 + const result = await getProfile({ ...params, hydrateCtx }, ctx); 26 + 27 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 15 28 16 29 return { 17 30 encoding: "application/json", 18 - body: { 19 - profiles, 20 - }, 31 + body: result, 32 + headers: resHeaders({ 33 + repoRev, 34 + }), 21 35 }; 22 36 }, 23 37 }); 24 38 } 39 + 40 + const skeleton = async (input: { 41 + ctx: Context; 42 + params: Params; 43 + }): Promise<SkeletonState> => { 44 + const { ctx, params } = input; 45 + console.log("actor params:", params.actors, typeof params.actors); 46 + const dids = await ctx.hydrator.actor.getDidsDefined(params.actors); 47 + return { dids }; 48 + }; 49 + 50 + const hydration = (input: { 51 + ctx: Context; 52 + params: Params; 53 + skeleton: SkeletonState; 54 + }) => { 55 + const { ctx, params, skeleton } = input; 56 + return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.hydrateCtx); 57 + }; 58 + 59 + const presentation = (input: { 60 + ctx: Context; 61 + params: Params; 62 + skeleton: SkeletonState; 63 + hydration: HydrationState; 64 + }) => { 65 + const { ctx, skeleton, hydration } = input; 66 + const profiles = mapDefined( 67 + skeleton.dids, 68 + (did) => ctx.views.profileDetailed(did, hydration), 69 + ); 70 + return { profiles }; 71 + }; 72 + 73 + type Context = { 74 + hydrator: Hydrator; 75 + views: Views; 76 + }; 77 + 78 + type Params = QueryParams & { 79 + hydrateCtx: HydrateCtx; 80 + }; 81 + 82 + type SkeletonState = { dids: string[] };
+232 -254
api/so/sprk/feed/getAuthorFeed.ts
··· 1 + import { mapDefined } from "@atp/common"; 2 + import { InvalidRequestError } from "@atp/xrpc-server"; 3 + import { AppContext } from "../../../../main.ts"; 4 + import { DataPlane } from "../../../../data-plane/index.ts"; 5 + import { Actor } from "../../../../hydration/actor.ts"; 6 + import { FeedItem, Post } from "../../../../hydration/feed.ts"; 7 + import { 8 + HydrateCtx, 9 + HydrationState, 10 + Hydrator, 11 + mergeStates, 12 + } from "../../../../hydration/index.ts"; 13 + import { parseString } from "../../../../hydration/util.ts"; 1 14 import { Server } from "../../../../lex/index.ts"; 2 - import { AppContext } from "../../../../main.ts"; 3 - import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts"; 4 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 5 - import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getAuthorFeed.ts"; 6 - import { PostDocument } from "../../../../data-plane/server/models.ts"; 15 + import { QueryParams } from "../../../../lex/types/so/sprk/feed/getAuthorFeed.ts"; 16 + import { createPipeline } from "../../../../pipeline.ts"; 17 + import { safePinnedPost, uriToDid } from "../../../../utils/uris.ts"; 18 + import { Views } from "../../../../views/index.ts"; 19 + import { clearlyBadCursor, resHeaders } from "../../../util.ts"; 7 20 8 - interface CursorData { 9 - createdAt: string; 10 - id: string; 11 - } 12 - 13 - interface BlockCheckResult { 14 - isBlocked: boolean; 15 - isBlocking: boolean; 16 - } 17 - 18 - // Helper function to parse cursor 19 - function parseCursor(cursor: string): CursorData { 20 - try { 21 - const decodedCursor = new TextDecoder().decode(decodeBase64(cursor)); 22 - const [timestamp, id] = decodedCursor.split("::"); 23 - 24 - if (!timestamp || !id) { 25 - throw new Error("Invalid cursor format"); 26 - } 27 - 28 - return { createdAt: timestamp, id }; 29 - } catch { 30 - throw new Error("Invalid cursor format"); 31 - } 32 - } 33 - 34 - // Helper function to generate cursor 35 - function generateCursor(createdAt: string, id: string): string { 36 - return encodeBase64( 37 - new TextEncoder().encode(`${createdAt}::${id}`), 21 + export default function (server: Server, ctx: AppContext) { 22 + const getAuthorFeed = createPipeline( 23 + skeleton, 24 + hydration, 25 + noBlocksOrMutedReposts, 26 + presentation, 38 27 ); 39 - } 28 + server.so.sprk.feed.getAuthorFeed({ 29 + auth: ctx.authVerifier.optionalStandardOrRole, 30 + handler: async ({ params, auth }) => { 31 + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 32 + const hydrateCtx = ctx.hydrator.createContext({ 33 + viewer, 34 + includeTakedowns, 35 + }); 40 36 41 - // Helper function to check block status 42 - async function checkBlockStatus( 43 - ctx: AppContext, 44 - userDid: string, 45 - actorDid: string, 46 - ): Promise<BlockCheckResult> { 47 - const [userIsBlocked, userIsBlocking] = await Promise.all([ 48 - ctx.db.models.Block.findOne({ 49 - authorDid: actorDid, 50 - subject: userDid, 51 - }).lean(), 52 - ctx.db.models.Block.findOne({ 53 - authorDid: userDid, 54 - subject: actorDid, 55 - }).lean(), 56 - ]); 37 + const result = await getAuthorFeed({ ...params, hydrateCtx }, ctx); 38 + 39 + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); 57 40 58 - return { 59 - isBlocked: !!userIsBlocked, 60 - isBlocking: !!userIsBlocking, 61 - }; 41 + return { 42 + encoding: "application/json", 43 + body: result, 44 + headers: resHeaders({ 45 + repoRev, 46 + }), 47 + }; 48 + }, 49 + }); 62 50 } 63 51 64 - // Helper function to build post query 65 - function buildPostQuery( 66 - actorDid: string, 67 - filter?: string, 68 - cursor?: CursorData, 69 - ): Record<string, unknown> { 70 - const query: Record<string, unknown> = { 71 - authorDid: actorDid, 72 - reply: null, 73 - }; 74 - 75 - // Add filter conditions 76 - if (filter === "posts_with_media") { 77 - query.$or = [ 78 - { "embed.$type": "so.sprk.embed.images" }, 79 - { "embed.$type": "so.sprk.embed.video" }, 80 - ]; 81 - } else if (filter === "posts_with_video") { 82 - query["embed.$type"] = "so.sprk.embed.video"; 52 + export const skeleton = async (inputs: { 53 + ctx: Context; 54 + params: Params; 55 + }): Promise<Skeleton> => { 56 + const { ctx, params } = inputs; 57 + const [did] = await ctx.hydrator.actor.getDids([params.actor]); 58 + if (!did) { 59 + throw new InvalidRequestError("Profile not found"); 83 60 } 84 - 85 - // Add cursor-based pagination 86 - if (cursor) { 87 - const paginationConditions = [ 88 - { createdAt: { $lt: cursor.createdAt } }, 89 - { createdAt: cursor.createdAt, _id: { $lt: cursor.id } }, 90 - ]; 91 - 92 - if (query.$or) { 93 - // If filter already uses $or, wrap everything in $and 94 - query.$and = [ 95 - { $or: query.$or }, 96 - { $or: paginationConditions }, 97 - ]; 98 - delete query.$or; 99 - } else { 100 - query.$or = paginationConditions; 101 - } 61 + const actors = await ctx.hydrator.actor.getActors([did], { 62 + includeTakedowns: params.hydrateCtx.includeTakedowns, 63 + }); 64 + const actor = actors.get(did); 65 + if (!actor) { 66 + throw new InvalidRequestError("Profile not found"); 102 67 } 103 - 104 - return query; 105 - } 106 - 107 - // Helper function to get pinned posts 108 - async function getPinnedPosts( 109 - ctx: AppContext, 110 - actorDid: string, 111 - ): Promise<PostDocument[]> { 112 - const profile = await ctx.db.models.Profile.findOne({ 113 - authorDid: actorDid, 114 - }).lean(); 115 - 116 - if (!profile?.pinnedPost?.uri) { 117 - return []; 68 + if (clearlyBadCursor(params.cursor)) { 69 + return { actor, filter: params.filter, items: [] }; 118 70 } 119 71 120 - const pinnedPost = await ctx.db.models.Post.findOne({ 121 - uri: profile.pinnedPost.uri, 122 - }).lean(); 72 + const pinnedPost = safePinnedPost(actor.profile?.pinnedPost); 73 + const isFirstPageRequest = !params.cursor; 74 + const shouldInsertPinnedPost = isFirstPageRequest && 75 + params.includePins && 76 + pinnedPost && 77 + uriToDid(pinnedPost.uri) === actor.did; 123 78 124 - return pinnedPost ? [pinnedPost] : []; 125 - } 79 + const res = await ctx.dataplane.feeds.getAuthorFeed( 80 + did, 81 + params.limit, 82 + params.cursor, 83 + ); 126 84 127 - export default function (server: Server, ctx: AppContext) { 128 - server.so.sprk.feed.getAuthorFeed({ 129 - auth: ctx.authVerifier.standardOptional, 130 - handler: async ({ params, auth }) => { 131 - try { 132 - // Extract and validate parameters 133 - const { actor, limit = 50, cursor, filter, includePins = false } = 134 - params; 135 - const userDid = auth.credentials.type === "standard" 136 - ? auth.credentials.iss 137 - : undefined; 138 - 139 - // Validate required parameters 140 - if (!actor) { 141 - throw new Error("Actor parameter is required"); 142 - } 143 - 144 - // Validate limit 145 - if (limit < 1 || limit > 100) { 146 - throw new Error("Limit must be between 1 and 100"); 147 - } 148 - 149 - // Resolve actor DID 150 - let resolvedActorDid = actor; 151 - if (!actor.startsWith("did:")) { 152 - try { 153 - const didDoc = await ctx.resolver.resolveHandleToDidDoc(actor); 154 - resolvedActorDid = didDoc.did; 155 - } catch (error) { 156 - console.error("Failed to resolve handle:", error); 157 - throw new Error("Could not resolve actor handle"); 158 - } 159 - } 85 + let items: FeedItem[] = res.items.map((item) => ({ 86 + post: { uri: item.uri, cid: item.cid || undefined }, 87 + repost: item.repost 88 + ? { uri: item.repost, cid: item.repostCid || undefined } 89 + : undefined, 90 + })); 160 91 161 - // Check block status if user is authenticated 162 - if (userDid) { 163 - const { isBlocked, isBlocking } = await checkBlockStatus( 164 - ctx, 165 - userDid, 166 - resolvedActorDid, 167 - ); 92 + if (shouldInsertPinnedPost && pinnedPost) { 93 + const pinnedItem = { 94 + post: { 95 + uri: pinnedPost.uri, 96 + cid: pinnedPost.cid, 97 + }, 98 + authorPinned: true, 99 + }; 168 100 169 - if (isBlocked) { 170 - return { 171 - status: 400, 172 - error: "BlockedByActor" as const, 173 - message: "The requesting account is blocked by the actor", 174 - }; 175 - } 101 + items = items.filter((item) => item.post.uri !== pinnedItem.post.uri); 102 + items.unshift(pinnedItem); 103 + } 176 104 177 - if (isBlocking) { 178 - return { 179 - status: 400, 180 - error: "BlockedActor" as const, 181 - message: "The requesting account has blocked the actor", 182 - }; 183 - } 184 - } 105 + return { 106 + actor, 107 + filter: params.filter, 108 + items, 109 + cursor: parseString(res.cursor), 110 + }; 111 + }; 185 112 186 - // Parse cursor if provided 187 - let cursorData: CursorData | undefined; 188 - if (cursor) { 189 - cursorData = parseCursor(cursor); 190 - } 113 + const hydration = async (inputs: { 114 + ctx: Context; 115 + params: Params; 116 + skeleton: Skeleton; 117 + }): Promise<HydrationState> => { 118 + const { ctx, params, skeleton } = inputs; 119 + const [feedPostState, profileViewerState] = await Promise.all([ 120 + ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx), 121 + ctx.hydrator.hydrateProfileViewers([skeleton.actor.did], params.hydrateCtx), 122 + ]); 123 + return mergeStates(feedPostState, profileViewerState); 124 + }; 191 125 192 - // Build and execute query 193 - const query = buildPostQuery(resolvedActorDid, filter, cursorData); 194 - const posts = await ctx.db.models.Post.find(query) 195 - .sort({ createdAt: -1, _id: -1 }) 196 - .limit(limit + 1) // Get one extra for hasMore check 197 - .lean(); 126 + const noBlocksOrMutedReposts = (inputs: { 127 + ctx: Context; 128 + skeleton: Skeleton; 129 + hydration: HydrationState; 130 + }): Skeleton => { 131 + const { ctx, skeleton, hydration } = inputs; 132 + const relationship = hydration.profileViewers?.get(skeleton.actor.did); 133 + if ( 134 + relationship && 135 + (relationship.blocking) 136 + ) { 137 + throw new InvalidRequestError( 138 + `Requester has blocked actor: ${skeleton.actor.did}`, 139 + "BlockedActor", 140 + ); 141 + } 142 + if ( 143 + relationship && 144 + (relationship.blockedBy) 145 + ) { 146 + throw new InvalidRequestError( 147 + `Requester is blocked by actor: ${skeleton.actor.did}`, 148 + "BlockedByActor", 149 + ); 150 + } 198 151 199 - // Check if there are more results 200 - const hasMore = posts.length > limit; 201 - if (hasMore) { 202 - posts.pop(); // Remove the extra item 203 - } 152 + const checkBlocksAndMutes = (item: FeedItem) => { 153 + const bam = ctx.views.feedItemBlocksAndMutes(item, hydration); 154 + return ( 155 + !bam.authorBlocked && 156 + !bam.originatorBlocked && 157 + (!bam.authorMuted || bam.originatorMuted) // repost of muted content 158 + ); 159 + }; 204 160 205 - // Get pinned posts if requested (only on first page) 206 - let pinnedPosts: PostDocument[] = []; 207 - if (includePins && !cursor) { 208 - pinnedPosts = await getPinnedPosts(ctx, resolvedActorDid); 209 - } 161 + if (skeleton.filter === "posts_and_author_threads") { 162 + // ensure replies are only included if the feed contains all 163 + // replies up to the thread root (i.e. a complete self-thread.) 164 + const selfThread = new SelfThreadTracker(skeleton.items, hydration); 165 + skeleton.items = skeleton.items.filter((item) => { 166 + return ( 167 + checkBlocksAndMutes(item) && 168 + (item.repost || item.authorPinned || selfThread.ok(item.post.uri)) 169 + ); 170 + }); 171 + } else { 172 + skeleton.items = skeleton.items.filter(checkBlocksAndMutes); 173 + } 210 174 211 - // Transform posts to feed view posts 212 - const allPosts = [...pinnedPosts, ...posts]; 213 - const feedViewPosts = await transformPostsToPostViews( 214 - allPosts, 215 - ctx, 216 - userDid, 217 - ); 175 + return skeleton; 176 + }; 218 177 219 - // Generate next cursor if there are more results 220 - let nextCursor: string | undefined; 221 - if (hasMore && posts.length > 0) { 222 - const lastPost = posts[posts.length - 1]; 223 - nextCursor = generateCursor( 224 - String(lastPost.createdAt), 225 - String(lastPost._id), 226 - ); 227 - } 178 + const presentation = (inputs: { 179 + ctx: Context; 180 + skeleton: Skeleton; 181 + hydration: HydrationState; 182 + }) => { 183 + const { ctx, skeleton, hydration } = inputs; 184 + const feed = mapDefined( 185 + skeleton.items, 186 + (item) => ctx.views.feedViewPost(item, hydration), 187 + ); 188 + return { feed, cursor: skeleton.cursor }; 189 + }; 228 190 229 - // Prepare response 230 - const response: OutputSchema = { 231 - feed: feedViewPosts.map((post) => ({ post })), 232 - }; 191 + type Context = { 192 + hydrator: Hydrator; 193 + views: Views; 194 + dataplane: DataPlane; 195 + }; 233 196 234 - if (nextCursor) { 235 - response.cursor = nextCursor; 236 - } 197 + type Params = QueryParams & { 198 + hydrateCtx: HydrateCtx; 199 + }; 237 200 238 - return { 239 - encoding: "application/json", 240 - body: response, 241 - }; 242 - } catch (error) { 243 - // Handle specific error cases 244 - if (error instanceof Error) { 245 - const message = error.message; 201 + type Skeleton = { 202 + actor: Actor; 203 + items: FeedItem[]; 204 + filter: QueryParams["filter"]; 205 + cursor?: string; 206 + }; 246 207 247 - if (message === "BlockedByActor" || message === "BlockedActor") { 248 - return { 249 - status: 400, 250 - error: message as "BlockedByActor" | "BlockedActor", 251 - message: message === "BlockedByActor" 252 - ? "The requesting account is blocked by the actor" 253 - : "The requesting account has blocked the actor", 254 - }; 255 - } 208 + class SelfThreadTracker { 209 + feedUris = new Set<string>(); 210 + cache = new Map<string, boolean>(); 256 211 257 - if (message.includes("cursor") || message.includes("Cursor")) { 258 - return { 259 - status: 400, 260 - message: "The provided cursor is invalid", 261 - }; 262 - } 212 + constructor( 213 + items: FeedItem[], 214 + private hydration: HydrationState, 215 + ) { 216 + items.forEach((item) => { 217 + if (!item.repost) { 218 + this.feedUris.add(item.post.uri); 219 + } 220 + }); 221 + } 263 222 264 - if (message.includes("limit") || message.includes("Limit")) { 265 - return { 266 - status: 400, 267 - message: "Limit must be between 1 and 100", 268 - }; 269 - } 223 + ok(uri: string, loop = new Set<string>()) { 224 + // if we've already checked this uri, pull from the cache 225 + if (this.cache.has(uri)) { 226 + return this.cache.get(uri) ?? false; 227 + } 228 + // loop detection 229 + if (loop.has(uri)) { 230 + this.cache.set(uri, false); 231 + return false; 232 + } else { 233 + loop.add(uri); 234 + } 235 + // cache through the result 236 + const result = this._ok(uri, loop); 237 + this.cache.set(uri, result); 238 + return result; 239 + } 270 240 271 - if (message.includes("Actor") || message.includes("actor")) { 272 - return { 273 - status: 400, 274 - message: "Invalid actor parameter or could not resolve handle", 275 - }; 276 - } 277 - } 241 + private _ok(uri: string, loop: Set<string>): boolean { 242 + // must be in the feed to be in a self-thread 243 + if (!this.feedUris.has(uri)) { 244 + return false; 245 + } 246 + // must be hydratable to be part of self-thread 247 + const post = this.hydration.posts?.get(uri); 248 + if (!post) { 249 + return false; 250 + } 251 + // root posts (no parent) are trivial case of self-thread 252 + const parentUri = getParentUri(post); 253 + if (parentUri === null) { 254 + return true; 255 + } 256 + // recurse w/ cache: this post is in a self-thread if its parent is. 257 + return this.ok(parentUri, loop); 258 + } 259 + } 278 260 279 - // Log unexpected errors and rethrow 280 - console.error("Unexpected error in getAuthorFeed:", error); 281 - throw error; 282 - } 283 - }, 284 - }); 261 + function getParentUri(post: Post) { 262 + return post.record.reply?.parent.uri ?? null; 285 263 }
+2 -2
api/so/sprk/feed/getPostThread.ts
··· 3 3 import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getPostThread.ts"; 4 4 import type * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts"; 5 5 import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts"; 6 - import { PostDocument } from "../../../../data-plane/server/models.ts"; 6 + import { PostDocument } from "../../../../data-plane/db/models.ts"; 7 7 import { type $Typed } from "../../../../lex/util.ts"; 8 8 9 9 // Constants ··· 34 34 const childToParent = new Map<string, string>(); 35 35 36 36 // Step 1: Get the root post first 37 - const rootPosts = await ctx.db.models.Post.find({ uri: rootUri }).lean(); 37 + const rootPosts = await ctx.db.models.Post.find({ uri: rootUri }); 38 38 if (rootPosts.length === 0) { 39 39 return { 40 40 posts,
+71 -239
api/so/sprk/feed/getPosts.ts
··· 1 - import { Server } from "../../../../lex/index.ts"; 1 + import { dedupeStrs, mapDefined } from "@atp/common"; 2 2 import { AppContext } from "../../../../main.ts"; 3 - import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getPosts.ts"; 4 - import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts"; 5 - import { PostDocument } from "../../../../data-plane/server/models.ts"; 6 - 7 - // Constants 8 - const MAX_POSTS_LIMIT = 25; 9 - const MAX_URI_LENGTH = 3000; 10 - 11 - // Helper function to validate URIs 12 - function validateUris(uris: string[]): { valid: string[]; invalid: string[] } { 13 - const valid: string[] = []; 14 - const invalid: string[] = []; 15 - 16 - for (const uri of uris) { 17 - if (typeof uri !== "string" || uri.length === 0) { 18 - invalid.push(uri); 19 - continue; 20 - } 21 - 22 - if (uri.length > MAX_URI_LENGTH) { 23 - invalid.push(uri); 24 - continue; 25 - } 26 - 27 - // Basic AT-URI validation 28 - if (!uri.startsWith("at://")) { 29 - invalid.push(uri); 30 - continue; 31 - } 32 - 33 - valid.push(uri); 34 - } 35 - 36 - return { valid, invalid }; 37 - } 38 - 39 - // Helper function to deduplicate URIs while preserving order 40 - function deduplicateUris(uris: string[]): string[] { 41 - const seen = new Set<string>(); 42 - return uris.filter((uri) => { 43 - if (seen.has(uri)) { 44 - return false; 45 - } 46 - seen.add(uri); 47 - return true; 48 - }); 49 - } 50 - 51 - // Helper function to check for blocked relationships 52 - async function checkBlockedPosts( 53 - ctx: AppContext, 54 - posts: PostDocument[], 55 - userDid?: string, 56 - ): Promise<Set<string>> { 57 - if (!userDid || posts.length === 0) { 58 - return new Set(); 59 - } 60 - 61 - const authorDids = [...new Set(posts.map((p) => p.authorDid))]; 62 - 63 - // Check if user is blocking any of the authors or is blocked by them 64 - const [userBlocking, userBlocked] = await Promise.all([ 65 - ctx.db.models.Block.find({ 66 - authorDid: userDid, 67 - subject: { $in: authorDids }, 68 - }).lean(), 69 - ctx.db.models.Block.find({ 70 - authorDid: { $in: authorDids }, 71 - subject: userDid, 72 - }).lean(), 73 - ]); 74 - 75 - const blockedAuthorDids = new Set([ 76 - ...userBlocking.map((b) => b.subject), 77 - ...userBlocked.map((b) => b.authorDid), 78 - ]); 79 - 80 - // Return URIs of posts from blocked authors 81 - return new Set( 82 - posts 83 - .filter((p) => blockedAuthorDids.has(p.authorDid)) 84 - .map((p) => p.uri), 85 - ); 86 - } 87 - 88 - // Helper function to sort posts by original URI order 89 - function sortPostsByUriOrder( 90 - posts: PostDocument[], 91 - originalUris: string[], 92 - ): PostDocument[] { 93 - const postMap = new Map(posts.map((post) => [post.uri, post])); 94 - const sortedPosts: PostDocument[] = []; 95 - 96 - for (const uri of originalUris) { 97 - const post = postMap.get(uri); 98 - if (post) { 99 - sortedPosts.push(post); 100 - } 101 - } 102 - 103 - return sortedPosts; 104 - } 3 + import { 4 + HydrateCtx, 5 + HydrationState, 6 + Hydrator, 7 + } from "../../../../hydration/index.ts"; 8 + import { Server } from "../../../../lex/index.ts"; 9 + import { QueryParams } from "../../../../lex/types/so/sprk/feed/getPosts.ts"; 10 + import { createPipeline } from "../../../../pipeline.ts"; 11 + import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; 12 + import { Views } from "../../../../views/index.ts"; 13 + import { resHeaders } from "../../../util.ts"; 105 14 106 15 export default function (server: Server, ctx: AppContext) { 16 + const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation); 107 17 server.so.sprk.feed.getPosts({ 108 18 auth: ctx.authVerifier.standardOptional, 109 19 handler: async ({ params, auth }) => { 110 - try { 111 - const { uris } = params; 112 - const userDid = auth.credentials.type === "standard" 113 - ? auth.credentials.iss 114 - : undefined; 115 - 116 - // Validate input 117 - if (!uris) { 118 - return { 119 - status: 400, 120 - message: "URIs parameter is required", 121 - }; 122 - } 123 - 124 - // Ensure uris is an array 125 - const uriArray = Array.isArray(uris) ? uris : [uris]; 126 - 127 - // Check if empty array 128 - if (uriArray.length === 0) { 129 - return { 130 - encoding: "application/json", 131 - body: { posts: [] } as OutputSchema, 132 - }; 133 - } 134 - 135 - // Enforce maximum limit 136 - if (uriArray.length > MAX_POSTS_LIMIT) { 137 - return { 138 - status: 400, 139 - message: `Too many URIs requested. Maximum is ${MAX_POSTS_LIMIT}`, 140 - }; 141 - } 142 - 143 - // Validate URIs 144 - const { valid: validUris, invalid: invalidUris } = validateUris( 145 - uriArray, 146 - ); 147 - 148 - if (invalidUris.length > 0) { 149 - console.warn( 150 - `Invalid URIs provided: ${invalidUris.slice(0, 5).join(", ")}${ 151 - invalidUris.length > 5 ? "..." : "" 152 - }`, 153 - ); 154 - } 20 + const viewer = auth.credentials.iss; 21 + const hydrateCtx = ctx.hydrator.createContext({ viewer }); 155 22 156 - if (validUris.length === 0) { 157 - return { 158 - encoding: "application/json", 159 - body: { posts: [] } as OutputSchema, 160 - }; 161 - } 23 + const results = await getPosts({ ...params, hydrateCtx }, ctx); 162 24 163 - // Deduplicate URIs while preserving order 164 - const uniqueUris = deduplicateUris(validUris); 25 + return { 26 + encoding: "application/json", 27 + body: results, 28 + headers: resHeaders({}), 29 + }; 30 + }, 31 + }); 32 + } 165 33 166 - // Fetch posts from database 167 - const dbPosts = await ctx.db.models.Post.find({ 168 - uri: { $in: uniqueUris }, 169 - }) 170 - .lean() 171 - .exec(); 34 + const skeleton = (inputs: { params: Params }) => { 35 + return { posts: dedupeStrs(inputs.params.uris) }; 36 + }; 172 37 173 - if (dbPosts.length === 0) { 174 - return { 175 - encoding: "application/json", 176 - body: { posts: [] } as OutputSchema, 177 - }; 178 - } 38 + const hydration = (inputs: { 39 + ctx: Context; 40 + params: Params; 41 + skeleton: Skeleton; 42 + }) => { 43 + const { ctx, params, skeleton } = inputs; 44 + return ctx.hydrator.hydratePosts( 45 + skeleton.posts.map((uri) => ({ uri })), 46 + params.hydrateCtx, 47 + ); 48 + }; 179 49 180 - // Check for blocked relationships 181 - const blockedPostUris = await checkBlockedPosts(ctx, dbPosts, userDid); 50 + const noBlocks = (inputs: { 51 + ctx: Context; 52 + skeleton: Skeleton; 53 + hydration: HydrationState; 54 + }) => { 55 + const { ctx, skeleton, hydration } = inputs; 56 + skeleton.posts = skeleton.posts.filter((uri) => { 57 + const creator = creatorFromUri(uri); 58 + return !ctx.views.viewerBlockExists(creator, hydration); 59 + }); 60 + return skeleton; 61 + }; 182 62 183 - // Filter out blocked posts 184 - const accessiblePosts = dbPosts.filter((post) => 185 - !blockedPostUris.has(post.uri) 186 - ); 63 + const presentation = (inputs: { 64 + ctx: Context; 65 + params: Params; 66 + skeleton: Skeleton; 67 + hydration: HydrationState; 68 + }) => { 69 + const { ctx, skeleton, hydration } = inputs; 70 + const posts = mapDefined( 71 + skeleton.posts, 72 + (uri) => ctx.views.post(uri, hydration), 73 + ); 74 + return { posts }; 75 + }; 187 76 188 - if (accessiblePosts.length === 0) { 189 - return { 190 - encoding: "application/json", 191 - body: { posts: [] } as OutputSchema, 192 - }; 193 - } 77 + type Context = { 78 + hydrator: Hydrator; 79 + views: Views; 80 + }; 194 81 195 - // Sort posts to match the original URI order 196 - const sortedPosts = sortPostsByUriOrder(accessiblePosts, uniqueUris); 82 + type Params = QueryParams & { hydrateCtx: HydrateCtx }; 197 83 198 - // Transform posts to PostView format 199 - const postViews = await transformPostsToPostViews( 200 - sortedPosts, 201 - ctx, 202 - userDid, 203 - ); 204 - 205 - const response: OutputSchema = { 206 - posts: postViews, 207 - }; 208 - 209 - return { 210 - encoding: "application/json", 211 - body: response, 212 - }; 213 - } catch (error) { 214 - // Log error for debugging 215 - console.error("Error in getPosts:", error); 216 - 217 - // Handle specific error cases 218 - if (error instanceof Error) { 219 - const message = error.message; 220 - 221 - // MongoDB connection errors 222 - if (message.includes("connection") || message.includes("timeout")) { 223 - return { 224 - status: 503, 225 - message: "Database temporarily unavailable", 226 - }; 227 - } 228 - 229 - // Validation errors 230 - if (message.includes("validation") || message.includes("invalid")) { 231 - return { 232 - status: 400, 233 - message: "Invalid request parameters", 234 - }; 235 - } 236 - 237 - // Rate limiting or resource errors 238 - if (message.includes("limit") || message.includes("quota")) { 239 - return { 240 - status: 429, 241 - message: "Rate limit exceeded", 242 - }; 243 - } 244 - } 245 - 246 - // Generic server error for unexpected cases 247 - return { 248 - status: 500, 249 - message: "Internal server error", 250 - }; 251 - } 252 - }, 253 - }); 254 - } 84 + type Skeleton = { 85 + posts: string[]; 86 + };
+1 -2
api/so/sprk/feed/getStories.ts
··· 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getStories.ts"; 4 4 import { transformStoriesToStoryViews } from "../../../../utils/story-transformer.ts"; 5 - import { StoryDocument } from "../../../../data-plane/server/models.ts"; 5 + import { StoryDocument } from "../../../../data-plane/db/models.ts"; 6 6 7 7 // Constants 8 8 const MAX_STORIES_LIMIT = 25; ··· 182 182 const dbStories = await ctx.db.models.Story.find({ 183 183 uri: { $in: uniqueUris }, 184 184 }) 185 - .lean() 186 185 .exec(); 187 186 188 187 if (dbStories.length === 0) {
+91 -140
api/so/sprk/feed/getStoriesTimeline.ts
··· 1 - import { InvalidRequestError } from "@sprk/xrpc-server"; 1 + import { InvalidRequestError } from "@atp/xrpc-server"; 2 2 import { Server } from "../../../../lex/index.ts"; 3 3 import { AppContext } from "../../../../main.ts"; 4 4 import { transformStoriesToStoryViews } from "../../../../utils/story-transformer.ts"; 5 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 5 + import { decodeBase64, encodeBase64 } from "@std/encoding"; 6 6 import type { ProfileViewBasic } from "../../../../lex/types/so/sprk/actor/defs.ts"; 7 7 import type * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts"; 8 8 ··· 168 168 server.so.sprk.feed.getStoriesTimeline({ 169 169 auth: ctx.authVerifier.standard, 170 170 handler: async ({ params, auth }) => { 171 - try { 172 - const { limit: limitParam = DEFAULT_LIMIT, cursor } = params; 173 - const userDid = auth.credentials.iss; 171 + const { limit: limitParam = DEFAULT_LIMIT, cursor } = params; 172 + const userDid = auth.credentials.iss; 174 173 175 - // Validate and sanitize limit 176 - const limit = typeof limitParam === "string" 177 - ? parseInt(limitParam, 10) 178 - : limitParam; 174 + // Validate and sanitize limit 175 + const limit = typeof limitParam === "string" 176 + ? parseInt(limitParam, 10) 177 + : limitParam; 179 178 180 - if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) { 181 - throw new InvalidRequestError( 182 - `Invalid limit: must be between 1 and ${MAX_LIMIT}`, 183 - ); 184 - } 179 + if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) { 180 + throw new InvalidRequestError( 181 + `Invalid limit: must be between 1 and ${MAX_LIMIT}`, 182 + ); 183 + } 185 184 186 - // Parse cursor if provided 187 - let cursorData: CursorData | undefined; 188 - if (cursor) { 189 - cursorData = parseCursor(cursor); 190 - } 185 + // Parse cursor if provided 186 + let cursorData: CursorData | undefined; 187 + if (cursor) { 188 + cursorData = parseCursor(cursor); 189 + } 191 190 192 - // Get accounts that the viewer follows (with optimization) 193 - const followedDids = await getUserFollows(ctx, userDid); 191 + // Get accounts that the viewer follows (with optimization) 192 + const followedDids = await getUserFollows(ctx, userDid); 194 193 195 - if (followedDids.length === 0) { 196 - return { 197 - encoding: "application/json", 198 - body: { 199 - storiesByAuthor: [], 200 - }, 201 - }; 202 - } 194 + if (followedDids.length === 0) { 195 + return { 196 + encoding: "application/json", 197 + body: { 198 + storiesByAuthor: [], 199 + }, 200 + }; 201 + } 203 202 204 - // Build optimized query 205 - const query = buildStoriesQuery(followedDids, cursorData); 203 + // Build optimized query 204 + const query = buildStoriesQuery(followedDids, cursorData); 206 205 207 - // Get stories from database with optimized query 208 - const stories = await ctx.db.models.Story.find(query) 209 - .sort({ indexedAt: -1, _id: -1 }) 210 - .limit(limit + 1) // Get one extra for hasMore check 211 - .lean() 212 - .exec(); 206 + // Get stories from database with optimized query 207 + const stories = await ctx.db.models.Story.find(query) 208 + .sort({ indexedAt: -1, _id: -1 }) 209 + .limit(limit + 1) // Get one extra for hasMore check 210 + .exec(); 213 211 214 - if (stories.length === 0) { 215 - return { 216 - encoding: "application/json", 217 - body: { 218 - storiesByAuthor: [], 219 - }, 220 - }; 221 - } 212 + if (stories.length === 0) { 213 + return { 214 + encoding: "application/json", 215 + body: { 216 + storiesByAuthor: [], 217 + }, 218 + }; 219 + } 222 220 223 - // Check if we have more results (for cursor) 224 - const hasMore = stories.length > limit; 225 - if (hasMore) { 226 - stories.pop(); // Remove the extra item 227 - } 221 + // Check if we have more results (for cursor) 222 + const hasMore = stories.length > limit; 223 + if (hasMore) { 224 + stories.pop(); // Remove the extra item 225 + } 228 226 229 - // Get all unique author DIDs for batch block checking 230 - const authorDids = [ 231 - ...new Set(stories.map((story) => story.authorDid)), 232 - ]; 227 + // Get all unique author DIDs for batch block checking 228 + const authorDids = [ 229 + ...new Set(stories.map((story) => story.authorDid)), 230 + ]; 233 231 234 - // Batch check all block relationships 235 - const blockedAuthorDids = await batchCheckBlockedAuthors( 236 - ctx, 237 - authorDids, 238 - userDid, 239 - ); 232 + // Batch check all block relationships 233 + const blockedAuthorDids = await batchCheckBlockedAuthors( 234 + ctx, 235 + authorDids, 236 + userDid, 237 + ); 240 238 241 - // Filter out stories from blocked authors 242 - const accessibleStories = stories.filter( 243 - (story) => !blockedAuthorDids.has(story.authorDid), 244 - ); 239 + // Filter out stories from blocked authors 240 + const accessibleStories = stories.filter( 241 + (story) => !blockedAuthorDids.has(story.authorDid), 242 + ); 245 243 246 - if (accessibleStories.length === 0) { 247 - return { 248 - encoding: "application/json", 249 - body: { 250 - storiesByAuthor: [], 251 - }, 252 - }; 253 - } 254 - 255 - // Transform stories to story views using batch transformer 256 - const storyViews = await transformStoriesToStoryViews( 257 - accessibleStories, 258 - ctx, 259 - ); 260 - 261 - // Group stories by author efficiently 262 - const storiesByAuthor = groupStoriesByAuthor(storyViews); 263 - 264 - // Generate next cursor if there are more results 265 - let nextCursor: string | undefined; 266 - if (hasMore && accessibleStories.length > 0) { 267 - const lastStory = accessibleStories[accessibleStories.length - 1]; 268 - nextCursor = generateCursor( 269 - lastStory.indexedAt, 270 - String(lastStory._id), 271 - ); 272 - } 273 - 274 - const response = { 275 - storiesByAuthor, 276 - ...(nextCursor && { cursor: nextCursor }), 277 - }; 278 - 244 + if (accessibleStories.length === 0) { 279 245 return { 280 246 encoding: "application/json", 281 - body: response, 247 + body: { 248 + storiesByAuthor: [], 249 + }, 282 250 }; 283 - } catch (error) { 284 - // Handle specific error types 285 - if (error instanceof InvalidRequestError) { 286 - return { 287 - status: 400, 288 - message: error.message, 289 - }; 290 - } 251 + } 291 252 292 - // Log unexpected errors 293 - console.error("Error fetching stories timeline:", error); 294 - 295 - // Handle specific error cases 296 - if (error instanceof Error) { 297 - const message = error.message; 253 + // Transform stories to story views using batch transformer 254 + const storyViews = await transformStoriesToStoryViews( 255 + accessibleStories, 256 + ctx, 257 + ); 298 258 299 - // Database connectivity issues 300 - if (message.includes("connection") || message.includes("timeout")) { 301 - return { 302 - status: 503, 303 - message: "Database temporarily unavailable", 304 - }; 305 - } 259 + // Group stories by author efficiently 260 + const storiesByAuthor = groupStoriesByAuthor(storyViews); 306 261 307 - // Rate limiting 308 - if (message.includes("limit") || message.includes("quota")) { 309 - return { 310 - status: 429, 311 - message: "Rate limit exceeded", 312 - }; 313 - } 262 + // Generate next cursor if there are more results 263 + let nextCursor: string | undefined; 264 + if (hasMore && accessibleStories.length > 0) { 265 + const lastStory = accessibleStories[accessibleStories.length - 1]; 266 + nextCursor = generateCursor( 267 + lastStory.indexedAt, 268 + String(lastStory._id), 269 + ); 270 + } 314 271 315 - // Authentication errors 316 - if (message.includes("auth") || message.includes("credential")) { 317 - return { 318 - status: 401, 319 - message: "Authentication required", 320 - }; 321 - } 322 - } 272 + const response = { 273 + storiesByAuthor, 274 + ...(nextCursor && { cursor: nextCursor }), 275 + }; 323 276 324 - // Generic server error 325 - return { 326 - status: 500, 327 - message: "Internal server error", 328 - }; 329 - } 277 + return { 278 + encoding: "application/json", 279 + body: response, 280 + }; 330 281 }, 331 282 }); 332 283 }
+4 -6
api/so/sprk/feed/getSuggestedFeeds.ts
··· 3 3 import { 4 4 BskyGeneratorDocument, 5 5 SprkGeneratorDocument, 6 - } from "../../../../data-plane/server/models.ts"; 6 + } from "../../../../data-plane/db/models.ts"; 7 7 import { getProfileView } from "../../../../utils/profile-helper.ts"; 8 8 import type * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts"; 9 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 9 + import { decodeBase64, encodeBase64 } from "@std/encoding"; 10 10 11 11 interface CursorData { 12 12 likeCount: number; ··· 121 121 // Get both BskyGenerator and SprkGenerator documents 122 122 const [bskyGenerators, sprkGenerators] = await Promise.all([ 123 123 ctx.db.models.BskyGenerator.find(query) 124 - .sort({ likeCount: -1, _id: -1 }) 125 - .lean(), 124 + .sort({ likeCount: -1, _id: -1 }), 126 125 ctx.db.models.SprkGenerator.find(query) 127 - .sort({ likeCount: -1, _id: -1 }) 128 - .lean(), 126 + .sort({ likeCount: -1, _id: -1 }), 129 127 ]); 130 128 131 129 // Combine and sort all generators by like count
+2 -3
api/so/sprk/feed/getTimeline.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts"; 4 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 4 + import { decodeBase64, encodeBase64 } from "@std/encoding"; 5 5 import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getTimeline.ts"; 6 6 7 7 interface CursorData { ··· 101 101 const query = buildTimelineQuery(followedDids, cursorData); 102 102 const posts = await ctx.db.models.Post.find(query) 103 103 .sort({ createdAt: -1, _id: -1 }) 104 - .limit(limit + 1) // Get one extra for hasMore check 105 - .lean(); 104 + .limit(limit + 1); // Get one extra for hasMore check 106 105 107 106 // Check if there are more results 108 107 const hasMore = posts.length > limit;
+1 -1
api/so/sprk/feed/searchPosts.ts
··· 4 4 import * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts"; 5 5 import { OutputSchema } from "../../../../lex/types/so/sprk/feed/searchPosts.ts"; 6 6 import { RootFilterQuery } from "mongoose"; 7 - import { PostDocument } from "../../../../data-plane/server/models.ts"; 7 + import { PostDocument } from "../../../../data-plane/db/models.ts"; 8 8 9 9 // Helper to escape user input for safe RegExp usage 10 10 function escapeRegExp(str: string): string {
+4 -6
api/so/sprk/graph/getFollowers.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 - import { FollowDocument } from "../../../../data-plane/server/models.ts"; 4 - import { ensureValidDid, isValidHandle } from "@atproto/syntax"; 3 + import { FollowDocument } from "../../../../data-plane/db/models.ts"; 4 + import { ensureValidDid, isValidHandle } from "@atp/syntax"; 5 5 import { RootFilterQuery } from "mongoose"; 6 - import { XRPCError } from "@sprk/xrpc-server"; 6 + import { XRPCError } from "@atp/xrpc-server"; 7 7 import { OutputSchema } from "../../../../lex/types/so/sprk/graph/getFollowers.ts"; 8 8 import { 9 9 getProfileView, ··· 62 62 : undefined; 63 63 64 64 // Extract follower DIDs and batch fetch profile views 65 - const followerDids = followers.map((follow: FollowDocument) => 66 - follow.authorDid 67 - ); 65 + const followerDids = followers.map((follow) => follow.authorDid); 68 66 const profileViews = await getProfileViews(ctx, followerDids, viewerDid); 69 67 70 68 const res = {
+4 -6
api/so/sprk/graph/getFollows.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 - import { FollowDocument } from "../../../../data-plane/server/models.ts"; 2 + import { FollowDocument } from "../../../../data-plane/db/models.ts"; 3 3 import { AppContext } from "../../../../main.ts"; 4 - import { ensureValidDid, isValidHandle } from "@atproto/syntax"; 4 + import { ensureValidDid, isValidHandle } from "@atp/syntax"; 5 5 import { RootFilterQuery } from "mongoose"; 6 - import { XRPCError } from "@sprk/xrpc-server"; 6 + import { XRPCError } from "@atp/xrpc-server"; 7 7 import { OutputSchema } from "../../../../lex/types/so/sprk/graph/getFollows.ts"; 8 8 import { 9 9 getProfileView, ··· 62 62 : undefined; 63 63 64 64 // Extract follow subject DIDs and batch fetch profile views 65 - const followSubjectDids = follows.map((follow: FollowDocument) => 66 - follow.subject 67 - ); 65 + const followSubjectDids = follows.map((follow) => follow.subject); 68 66 const profileViews = await getProfileViews( 69 67 ctx, 70 68 followSubjectDids,
+2 -3
api/so/sprk/sound/getActorAudios.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { transformAudiosToAudioViews } from "../../../../utils/audio-transformer.ts"; 4 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 4 + import { decodeBase64, encodeBase64 } from "@std/encoding"; 5 5 6 6 interface CursorData { 7 7 createdAt: string; ··· 94 94 95 95 const audios = await ctx.db.models.Audio.find(query) 96 96 .sort({ createdAt: -1, _id: -1 }) 97 - .limit(limit + 1) 98 - .lean(); 97 + .limit(limit + 1); 99 98 100 99 const hasMore = audios.length > limit; 101 100 if (hasMore) audios.pop();
+5 -7
api/so/sprk/sound/getAudioPosts.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts"; 4 - import { decodeBase64, encodeBase64 } from "jsr:@std/encoding"; 4 + import { decodeBase64, encodeBase64 } from "@std/encoding"; 5 5 import { transformAudioToAudioView } from "../../../../utils/audio-transformer.ts"; 6 6 import { RootFilterQuery } from "mongoose"; 7 - import { PostDocument } from "../../../../data-plane/server/models.ts"; 7 + import { PostDocument } from "../../../../data-plane/db/models.ts"; 8 8 9 9 interface CursorData { 10 10 createdAt: string; ··· 48 48 const dbAudio = await ctx.db.models.Audio.findOne({ 49 49 uri: uri, 50 50 }) 51 - .lean() 52 51 .exec(); 53 52 54 53 if (!dbAudio) { ··· 72 71 const posts = await ctx.db.models.Post 73 72 .find(query) 74 73 .sort({ createdAt: -1, _id: -1 }) 75 - .limit(limit + 1) 76 - .lean(); 74 + .limit(limit + 1); 77 75 78 76 const hasMore = posts.length > limit; 79 77 if (hasMore) posts.pop(); ··· 85 83 ctx.db.models.Block.find({ 86 84 authorDid: userDid, 87 85 subject: { $in: authorDids }, 88 - }).lean(), 86 + }), 89 87 ctx.db.models.Block.find({ 90 88 authorDid: { $in: authorDids }, 91 89 subject: userDid, 92 - }).lean(), 90 + }), 93 91 ]); 94 92 const blockedAuthorDids = new Set<string>([ 95 93 ...userBlocking.map((b) => b.subject),
+1 -2
api/so/sprk/sound/getAudios.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { transformAudiosToAudioViews } from "../../../../utils/audio-transformer.ts"; 4 - import { AudioDocument } from "../../../../data-plane/server/models.ts"; 4 + import { AudioDocument } from "../../../../data-plane/db/models.ts"; 5 5 6 6 // Constants 7 7 const MAX_URI_LENGTH = 3000; ··· 120 120 const dbAudios = await ctx.db.models.Audio.find({ 121 121 uri: { $in: uniqueUris }, 122 122 }) 123 - .lean() 124 123 .exec(); 125 124 126 125 if (dbAudios.length === 0) {
+2 -3
api/so/sprk/sound/getTrendingAudios.ts
··· 1 1 import { Server } from "../../../../lex/index.ts"; 2 2 import { AppContext } from "../../../../main.ts"; 3 3 import { transformAudiosToAudioViews } from "../../../../utils/audio-transformer.ts"; 4 - import { AudioDocument } from "../../../../data-plane/server/models.ts"; 4 + import { AudioDocument } from "../../../../data-plane/db/models.ts"; 5 5 6 6 interface AudioAggDoc { 7 7 uri: string; ··· 36 36 const uris = docsPage.map((a) => a.uri); 37 37 38 38 // Fetch full audio documents and preserve order 39 - const docs = await ctx.db.models.Audio.find({ uri: { $in: uris } }) 40 - .lean(); 39 + const docs = await ctx.db.models.Audio.find({ uri: { $in: uris } }); 41 40 const byUri = new Map(docs.map((d) => [d.uri, d] as const)); 42 41 let audiosOrdered: AudioDocument[] = []; 43 42 for (const uri of uris) {
+21
api/util.ts
··· 1 + export const SPRK_USER_AGENT = "SprkAppView"; 2 + export const ATPROTO_CONTENT_LABELERS = "Atproto-Content-Labelers"; 3 + export const ATPROTO_REPO_REV = "Atproto-Repo-Rev"; 4 + 5 + type ResHeaderOpts = { 6 + repoRev: string | null; 7 + }; 8 + 9 + export const resHeaders = ( 10 + opts: Partial<ResHeaderOpts>, 11 + ): Record<string, string> => { 12 + const headers: Record<string, string> = {}; 13 + if (opts.repoRev) { 14 + headers[ATPROTO_REPO_REV] = opts.repoRev; 15 + } 16 + return headers; 17 + }; 18 + 19 + export const clearlyBadCursor = (cursor?: string) => { 20 + return !!cursor?.includes("::"); 21 + };
+1 -1
api/well-known.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { env } from "../utils/env.ts"; 3 - import { formatMultikey, Secp256k1Keypair } from "@atproto/crypto"; 3 + import { formatMultikey, Secp256k1Keypair } from "@atp/crypto"; 4 4 5 5 const wellKnownRouter = () => { 6 6 const router = new Hono();
+9
compose.dev.yaml
··· 78 78 - path: ./main.ts 79 79 action: sync 80 80 target: /app/main.ts 81 + - path: ./views 82 + action: sync 83 + target: /app/views 84 + - path: ./data-plane 85 + action: sync 86 + target: /app/data-plane 87 + - path: ./hydration 88 + action: sync 89 + target: /app/hydration 81 90 restart: unless-stopped
-117
data-plane/client/hosts.ts
··· 1 - import mongoose, { Connection, Document, Schema } from "mongoose"; 2 - 3 - /** 4 - * Interface for a reactive list of hosts, i.e. for use with the dataplane client. 5 - */ 6 - export interface HostList { 7 - get: () => Iterable<string>; 8 - onUpdate(handler: HostListHandler): void; 9 - } 10 - 11 - type HostListHandler = (hosts: Iterable<string>) => void; 12 - 13 - /** 14 - * Maintains a reactive HostList based on a simple setter. 15 - */ 16 - export class BasicHostList implements HostList { 17 - private hosts: Iterable<string>; 18 - private handlers: HostListHandler[] = []; 19 - 20 - constructor(hosts: Iterable<string>) { 21 - this.hosts = hosts; 22 - } 23 - 24 - get() { 25 - return this.hosts; 26 - } 27 - 28 - set(hosts: Iterable<string>) { 29 - this.hosts = hosts; 30 - this.update(); 31 - } 32 - 33 - private update() { 34 - for (const handler of this.handlers) { 35 - handler(this.hosts); 36 - } 37 - } 38 - 39 - onUpdate(handler: HostListHandler) { 40 - this.handlers.push(handler); 41 - } 42 - } 43 - 44 - interface HostDocument extends Document { 45 - url: string; 46 - active: boolean; 47 - updatedAt: Date; 48 - } 49 - 50 - const hostSchema = new Schema<HostDocument>({ 51 - url: { type: String, required: true, unique: true }, 52 - active: { type: Boolean, required: true, default: true }, 53 - updatedAt: { type: Date, required: true, default: Date.now }, 54 - }); 55 - 56 - /** 57 - * Maintains a reactive HostList based on MongoDB documents. 58 - * When fallback is provided, ensures that this fallback is used whenever no hosts are available. 59 - */ 60 - export class MongoHostList implements HostList { 61 - private connection: Connection; 62 - private inner = new BasicHostList(new Set()); 63 - private fallback: Set<string>; 64 - private changeStream: mongoose.mongo.ChangeStream | null = null; 65 - private model: mongoose.Model<HostDocument>; 66 - 67 - constructor(connection: Connection, fallback?: string[]) { 68 - this.fallback = new Set(fallback); 69 - this.connection = connection; 70 - this.model = this.connection.model<HostDocument>("Host", hostSchema); 71 - } 72 - 73 - async connect() { 74 - await this.updateHosts(); 75 - this.startWatching(); 76 - } 77 - 78 - private async updateHosts() { 79 - const hosts = new Set<string>(); 80 - const activeHosts = await this.model.find({ active: true }).exec(); 81 - 82 - for (const host of activeHosts) { 83 - if (URL.canParse(host.url)) { 84 - hosts.add(host.url); 85 - } 86 - } 87 - 88 - if (hosts.size) { 89 - this.inner.set(hosts); 90 - } else if (this.fallback.size) { 91 - this.inner.set(this.fallback); 92 - } 93 - } 94 - 95 - private startWatching() { 96 - this.changeStream = this.model.watch(); 97 - 98 - this.changeStream.on("change", async () => { 99 - await this.updateHosts(); 100 - }); 101 - } 102 - 103 - get() { 104 - return this.inner.get(); 105 - } 106 - 107 - onUpdate(handler: HostListHandler) { 108 - this.inner.onUpdate(handler); 109 - } 110 - 111 - async disconnect() { 112 - if (this.changeStream) { 113 - await this.changeStream.close(); 114 - this.changeStream = null; 115 - } 116 - } 117 - }
-160
data-plane/client/index.ts
··· 1 - import assert from "node:assert"; 2 - import { randomInt } from "node:crypto"; 3 - import mongoose from "mongoose"; 4 - import { Code, ConnectError } from "./util.ts"; 5 - import { HostList } from "./hosts.ts"; 6 - import { decodeBase64 } from "jsr:@std/encoding"; 7 - 8 - export * from "./hosts.ts"; 9 - export * from "./util.ts"; 10 - 11 - export interface DataPlaneClient { 12 - getIdentityByDid: ( 13 - params: { did: string }, 14 - ) => Promise<GetIdentityByDidResponse>; 15 - } 16 - 17 - export interface GetIdentityByDidResponse { 18 - did: string; 19 - keys: Uint8Array; 20 - services: Uint8Array; 21 - handle?: string; 22 - } 23 - 24 - const MAX_RETRIES = 3; 25 - 26 - export const createDataPlaneClient = ( 27 - hostList: HostList, 28 - opts: { rejectUnauthorized?: boolean } = {}, 29 - ) => { 30 - const clients = new DataPlaneClients(hostList, opts); 31 - 32 - // Create the base implementation 33 - const implementation: DataPlaneClient = { 34 - async getIdentityByDid(params) { 35 - let tries = 0; 36 - let error: unknown; 37 - let remainingClients = clients.get(); 38 - while (tries < MAX_RETRIES) { 39 - const client = randomElement(remainingClients); 40 - assert(client, "no clients available"); 41 - try { 42 - return await client.getIdentityByDid(params); 43 - } catch (err) { 44 - if ( 45 - err instanceof Error && 46 - (err.name === "MongoNetworkError" || 47 - err.name === "MongoServerError") 48 - ) { 49 - tries++; 50 - error = err; 51 - remainingClients = getRemainingClients(remainingClients, client); 52 - } else { 53 - throw err; 54 - } 55 - } 56 - } 57 - assert(error); 58 - throw error; 59 - }, 60 - }; 61 - 62 - // Create a proxy that wraps the implementation with retry logic 63 - return new Proxy(implementation, { 64 - get: (target, method: string) => { 65 - // Return the method from our implementation if it exists 66 - if (method in target) { 67 - return target[method as keyof DataPlaneClient]; 68 - } 69 - // For any methods we haven't implemented yet, return a function that throws 70 - return () => { 71 - throw new Error(`Method ${method} not implemented`); 72 - }; 73 - }, 74 - }); 75 - }; 76 - 77 - export { Code }; 78 - 79 - /** 80 - * Uses a reactive HostList in order to maintain a pool of DataPlaneClients. 81 - * Each DataPlaneClient is cached per host so that it maintains connections 82 - * and other internal state when the underlying HostList is updated. 83 - */ 84 - class DataPlaneClients { 85 - private clients: DataPlaneClient[] = []; 86 - private clientsByHost = new Map<string, DataPlaneClient>(); 87 - 88 - constructor( 89 - private hostList: HostList, 90 - private clientOpts: { rejectUnauthorized?: boolean }, 91 - ) { 92 - this.refresh(); 93 - this.hostList.onUpdate(() => this.refresh()); 94 - } 95 - 96 - get(): readonly DataPlaneClient[] { 97 - return this.clients; 98 - } 99 - 100 - private refresh() { 101 - this.clients = []; 102 - for (const host of this.hostList.get()) { 103 - let client = this.clientsByHost.get(host); 104 - if (!client) { 105 - client = this.createClient(host); 106 - this.clientsByHost.set(host, client); 107 - } 108 - this.clients.push(client); 109 - } 110 - } 111 - 112 - private createClient(host: string): DataPlaneClient { 113 - const connection = mongoose.createConnection(host); 114 - 115 - return { 116 - async getIdentityByDid( 117 - { did }: { did: string }, 118 - ): Promise<GetIdentityByDidResponse> { 119 - const Actor = connection.model( 120 - "Actor", 121 - new mongoose.Schema({ 122 - did: { type: String, required: true }, 123 - keys: { type: String, required: true }, 124 - services: { type: String, required: true }, 125 - handle: String, 126 - }), 127 - ); 128 - 129 - const actor = await Actor.findOne({ did }).exec(); 130 - if (!actor) { 131 - throw new ConnectError("Actor not found", Code.NotFound); 132 - } 133 - 134 - if (!actor.did || !actor.keys || !actor.services) { 135 - throw new ConnectError("Invalid actor data", Code.InternalError); 136 - } 137 - 138 - return { 139 - did: actor.did, 140 - keys: decodeBase64(actor.keys), 141 - services: decodeBase64(actor.services), 142 - handle: actor.handle || undefined, 143 - }; 144 - }, 145 - }; 146 - } 147 - } 148 - 149 - const getRemainingClients = ( 150 - clients: readonly DataPlaneClient[], 151 - lastClient: DataPlaneClient, 152 - ) => { 153 - if (clients.length < 2) return clients; // no clients to choose from 154 - return clients.filter((c) => c !== lastClient); 155 - }; 156 - 157 - const randomElement = <T>(arr: readonly T[]): T | undefined => { 158 - if (arr.length === 0) return; 159 - return arr[randomInt(arr.length)]; 160 - };
-96
data-plane/client/util.ts
··· 1 - import * as ui8 from "npm:uint8arrays"; 2 - import { getDidKeyFromMultibase } from "@atproto/identity"; 3 - 4 - export enum Code { 5 - NotFound = "NotFound", 6 - InvalidRequest = "InvalidRequest", 7 - Unauthorized = "Unauthorized", 8 - Forbidden = "Forbidden", 9 - InternalError = "InternalError", 10 - } 11 - 12 - export class ConnectError extends Error { 13 - constructor( 14 - message: string, 15 - public code: Code, 16 - public status: number = 500, 17 - ) { 18 - super(message); 19 - this.name = "ConnectError"; 20 - } 21 - } 22 - 23 - export const isDataplaneError = ( 24 - err: unknown, 25 - code?: Code, 26 - ): err is ConnectError => { 27 - if (err instanceof ConnectError) { 28 - return !code || err.code === code; 29 - } 30 - return false; 31 - }; 32 - 33 - export const unpackIdentityServices = (servicesBytes: Uint8Array) => { 34 - const servicesStr = ui8.toString(servicesBytes, "utf8"); 35 - if (!servicesStr) return {}; 36 - return JSON.parse(servicesStr) as UnpackedServices; 37 - }; 38 - 39 - export const unpackIdentityKeys = (keysBytes: Uint8Array) => { 40 - const keysStr = ui8.toString(keysBytes, "utf8"); 41 - if (!keysStr) return {}; 42 - return JSON.parse(keysStr) as UnpackedKeys; 43 - }; 44 - 45 - export const getServiceEndpoint = ( 46 - services: UnpackedServices, 47 - opts: { id: string; type: string }, 48 - ) => { 49 - const endpoint = services[opts.id] && 50 - services[opts.id].Type === opts.type && 51 - validateUrl(services[opts.id].URL); 52 - return endpoint || undefined; 53 - }; 54 - 55 - export const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => { 56 - const key = keys[opts.id] && 57 - getDidKeyFromMultibase({ 58 - type: keys[opts.id].Type, 59 - publicKeyMultibase: keys[opts.id].PublicKeyMultibase, 60 - }); 61 - return key || undefined; 62 - }; 63 - 64 - type UnpackedServices = Record<string, { Type: string; URL: string }>; 65 - 66 - type UnpackedKeys = Record< 67 - string, 68 - { Type: string; PublicKeyMultibase: string } 69 - >; 70 - 71 - const validateUrl = (urlStr: string): string | undefined => { 72 - let url; 73 - try { 74 - url = new URL(urlStr); 75 - } catch { 76 - return undefined; 77 - } 78 - if (!["http:", "https:"].includes(url.protocol)) { 79 - return undefined; 80 - } else if (!url.hostname) { 81 - return undefined; 82 - } else { 83 - return urlStr; 84 - } 85 - }; 86 - 87 - export const handleMongoError = (error: unknown): never => { 88 - if (error instanceof Error) { 89 - if (error.name === "MongoServerError") { 90 - throw new ConnectError(error.message, Code.InternalError); 91 - } else if (error.name === "MongoNetworkError") { 92 - throw new ConnectError("Database connection error", Code.InternalError); 93 - } 94 - } 95 - throw new ConnectError("Unknown database error", Code.InternalError); 96 - };
+399
data-plane/db/pagination.ts
··· 1 + import { ensureValidRecordKey } from "@atp/syntax"; 2 + import { InvalidRequestError } from "@atp/xrpc-server"; 3 + import { Document, FilterQuery, Query } from "mongoose"; 4 + 5 + type KeysetCursor = { primary: string; secondary: string }; 6 + type KeysetLabeledResult = { 7 + primary: string | number; 8 + secondary: string | number; 9 + }; 10 + 11 + /** 12 + * The GenericKeyset is an abstract class that sets-up the interface and partial implementation 13 + * of a keyset-paginated cursor with two parts. There are three types involved: 14 + * - Result: a raw result (i.e. a document from the db) containing data that will make-up a cursor. 15 + * - E.g. { createdAt: '2022-01-01T12:00:00Z', cid: 'bafyx' } 16 + * - LabeledResult: a Result processed such that the "primary" and "secondary" parts of the cursor are labeled. 17 + * - E.g. { primary: '2022-01-01T12:00:00Z', secondary: 'bafyx' } 18 + * - Cursor: the two string parts that make-up the packed/string cursor. 19 + * - E.g. packed cursor '1641038400000__bafyx' in parts { primary: '1641038400000', secondary: 'bafyx' } 20 + * 21 + * These types relate as such. Implementers define the relations marked with a *: 22 + * Result -*-> LabeledResult <-*-> Cursor <--> packed/string cursor 23 + * ↳ MongoDB Filter Condition 24 + */ 25 + export abstract class GenericKeyset<R, LR extends KeysetLabeledResult> { 26 + constructor( 27 + public primary: string, 28 + public secondary: string, 29 + ) {} 30 + abstract labelResult(result: R): LR; 31 + abstract labeledResultToCursor(labeled: LR): KeysetCursor; 32 + abstract cursorToLabeledResult(cursor: KeysetCursor): LR; 33 + packFromResult(results: R | R[]): string | undefined { 34 + const result = Array.isArray(results) ? results.at(-1) : results; 35 + if (!result) return; 36 + return this.pack(this.labelResult(result)); 37 + } 38 + pack(labeled?: LR): string | undefined { 39 + if (!labeled) return; 40 + const cursor = this.labeledResultToCursor(labeled); 41 + return this.packCursor(cursor); 42 + } 43 + unpack(cursorStr?: string): LR | undefined { 44 + const cursor = this.unpackCursor(cursorStr); 45 + if (!cursor) return; 46 + return this.cursorToLabeledResult(cursor); 47 + } 48 + packCursor(cursor?: KeysetCursor): string | undefined { 49 + if (!cursor) return; 50 + // Use colon as separator (more compact than double underscore) 51 + return `${cursor.primary}:${cursor.secondary}`; 52 + } 53 + unpackCursor(cursorStr?: string): KeysetCursor | undefined { 54 + if (!cursorStr) return; 55 + const separatorIndex = cursorStr.indexOf(":"); 56 + if (separatorIndex === -1) { 57 + throw new InvalidRequestError("Malformed cursor: missing separator"); 58 + } 59 + const primary = cursorStr.slice(0, separatorIndex); 60 + const secondary = cursorStr.slice(separatorIndex + 1); 61 + if (!primary || !secondary) { 62 + throw new InvalidRequestError( 63 + "Malformed cursor: missing primary or secondary", 64 + ); 65 + } 66 + return { 67 + primary, 68 + secondary, 69 + }; 70 + } 71 + getFilter<T>( 72 + labeled?: LR, 73 + direction?: "asc" | "desc", 74 + ): FilterQuery<T> | undefined { 75 + if (labeled === undefined) return undefined; 76 + 77 + // MongoDB compound key comparison using $or 78 + if (direction === "asc") { 79 + return { 80 + $or: [ 81 + { [this.primary]: { $gt: labeled.primary } }, 82 + { 83 + [this.primary]: labeled.primary, 84 + [this.secondary]: { $gt: labeled.secondary }, 85 + }, 86 + ], 87 + } as FilterQuery<T>; 88 + } else { 89 + return { 90 + $or: [ 91 + { [this.primary]: { $lt: labeled.primary } }, 92 + { 93 + [this.primary]: labeled.primary, 94 + [this.secondary]: { $lt: labeled.secondary }, 95 + }, 96 + ], 97 + } as FilterQuery<T>; 98 + } 99 + } 100 + paginate<T extends Document>( 101 + query: Query<T[], T>, 102 + opts: { 103 + limit?: number; 104 + cursor?: string; 105 + direction?: "asc" | "desc"; 106 + }, 107 + ): Query<T[], T> { 108 + const { limit, cursor, direction = "desc" } = opts; 109 + const keysetFilter = this.getFilter<T>(this.unpack(cursor), direction); 110 + 111 + if (keysetFilter) { 112 + query = query.where(keysetFilter); 113 + } 114 + 115 + if (limit) { 116 + query = query.limit(limit); 117 + } 118 + 119 + // Set up sorting 120 + const sortOrder = direction === "asc" ? 1 : -1; 121 + query = query.sort({ 122 + [this.primary]: sortOrder, 123 + [this.secondary]: sortOrder, 124 + }); 125 + 126 + return query; 127 + } 128 + } 129 + 130 + type SortedAtCidResult = { sortAt?: string; cid: string }; 131 + type TimeCidLabeledResult = KeysetCursor; 132 + 133 + export class TimeCidKeyset< 134 + TimeCidResult = SortedAtCidResult, 135 + > extends GenericKeyset<TimeCidResult, TimeCidLabeledResult> { 136 + constructor() { 137 + super("sortAt", "cid"); 138 + } 139 + 140 + labelResult(result: TimeCidResult): TimeCidLabeledResult; 141 + labelResult<TimeCidResult extends SortedAtCidResult>(result: TimeCidResult) { 142 + // Use current time as fallback if sortAt is missing 143 + const sortAt = result.sortAt || new Date().toISOString(); 144 + return { primary: sortAt, secondary: result.cid }; 145 + } 146 + labeledResultToCursor(labeled: TimeCidLabeledResult) { 147 + const timestamp = new Date(labeled.primary).getTime(); 148 + if (isNaN(timestamp)) { 149 + throw new InvalidRequestError("Invalid date for cursor"); 150 + } 151 + // Use seconds instead of milliseconds and base36 encoding for compactness 152 + const secondsBase36 = Math.floor(timestamp / 1000).toString(36); 153 + return { 154 + primary: secondsBase36, 155 + secondary: labeled.secondary, 156 + }; 157 + } 158 + cursorToLabeledResult(cursor: KeysetCursor) { 159 + // Parse from base36 and convert seconds back to milliseconds 160 + const seconds = parseInt(cursor.primary, 36); 161 + if (isNaN(seconds)) { 162 + throw new InvalidRequestError("Malformed cursor: invalid timestamp"); 163 + } 164 + const primaryDate = new Date(seconds * 1000); 165 + if (isNaN(primaryDate.getTime())) { 166 + throw new InvalidRequestError("Malformed cursor: invalid date"); 167 + } 168 + return { 169 + primary: primaryDate.toISOString(), 170 + secondary: cursor.secondary, 171 + }; 172 + } 173 + } 174 + 175 + export class CreatedAtDidKeyset extends TimeCidKeyset<{ 176 + createdAt: string; 177 + did: string; // dids are treated identically to cids in TimeCidKeyset 178 + }> { 179 + constructor() { 180 + super(); 181 + this.primary = "createdAt"; 182 + this.secondary = "did"; 183 + } 184 + 185 + override labelResult(result: { createdAt: string; did: string }) { 186 + // Use current time as fallback if createdAt is missing 187 + const createdAt = result.createdAt || new Date().toISOString(); 188 + return { primary: createdAt, secondary: result.did }; 189 + } 190 + } 191 + 192 + export class IndexedAtDidKeyset extends TimeCidKeyset<{ 193 + indexedAt: string; 194 + did: string; // dids are treated identically to cids in TimeCidKeyset 195 + }> { 196 + constructor() { 197 + super(); 198 + this.primary = "indexedAt"; 199 + this.secondary = "did"; 200 + } 201 + 202 + override labelResult(result: { indexedAt: string; did: string }) { 203 + // Use current time as fallback if indexedAt is missing 204 + const indexedAt = result.indexedAt || new Date().toISOString(); 205 + return { primary: indexedAt, secondary: result.did }; 206 + } 207 + } 208 + 209 + /** 210 + * This is being deprecated. Use {@link GenericKeyset#paginate} instead. 211 + */ 212 + export const paginate = < 213 + T extends Document, 214 + K extends GenericKeyset<unknown, KeysetLabeledResult>, 215 + >( 216 + query: Query<T[], T>, 217 + opts: { 218 + limit?: number; 219 + cursor?: string; 220 + direction?: "asc" | "desc"; 221 + keyset: K; 222 + }, 223 + ): Query<T[], T> => { 224 + return opts.keyset.paginate(query, opts); 225 + }; 226 + 227 + type SingleKeyCursor = { 228 + primary: string; 229 + }; 230 + 231 + type SingleKeyLabeledResult = { 232 + primary: string | number; 233 + }; 234 + 235 + /** 236 + * GenericSingleKey is similar to {@link GenericKeyset} but for a single key cursor. 237 + */ 238 + export abstract class GenericSingleKey<R, LR extends SingleKeyLabeledResult> { 239 + constructor(public primary: string) {} 240 + abstract labelResult(result: R): LR; 241 + abstract labeledResultToCursor(labeled: LR): SingleKeyCursor; 242 + abstract cursorToLabeledResult(cursor: SingleKeyCursor): LR; 243 + packFromResult(results: R | R[]): string | undefined { 244 + const result = Array.isArray(results) ? results.at(-1) : results; 245 + if (!result) return; 246 + return this.pack(this.labelResult(result)); 247 + } 248 + pack(labeled?: LR): string | undefined { 249 + if (!labeled) return; 250 + const cursor = this.labeledResultToCursor(labeled); 251 + return this.packCursor(cursor); 252 + } 253 + unpack(cursorStr?: string): LR | undefined { 254 + const cursor = this.unpackCursor(cursorStr); 255 + if (!cursor) return; 256 + return this.cursorToLabeledResult(cursor); 257 + } 258 + packCursor(cursor?: SingleKeyCursor): string | undefined { 259 + if (!cursor) return; 260 + return cursor.primary; 261 + } 262 + unpackCursor(cursorStr?: string): SingleKeyCursor | undefined { 263 + if (!cursorStr) return; 264 + // Single key cursors don't use separators 265 + if (cursorStr.includes(":") || cursorStr.includes("__")) { 266 + throw new InvalidRequestError( 267 + "Malformed cursor: unexpected separator in single key cursor", 268 + ); 269 + } 270 + return { 271 + primary: cursorStr, 272 + }; 273 + } 274 + getFilter<T>( 275 + labeled?: LR, 276 + direction?: "asc" | "desc", 277 + ): FilterQuery<T> | undefined { 278 + if (labeled === undefined) return undefined; 279 + if (direction === "asc") { 280 + return { [this.primary]: { $gt: labeled.primary } } as FilterQuery<T>; 281 + } 282 + return { [this.primary]: { $lt: labeled.primary } } as FilterQuery<T>; 283 + } 284 + paginate<T extends Document>( 285 + query: Query<T[], T>, 286 + opts: { 287 + limit?: number; 288 + cursor?: string; 289 + direction?: "asc" | "desc"; 290 + }, 291 + ): Query<T[], T> { 292 + const { limit, cursor, direction = "desc" } = opts; 293 + const keyFilter = this.getFilter<T>(this.unpack(cursor), direction); 294 + 295 + if (keyFilter) { 296 + query = query.where(keyFilter); 297 + } 298 + 299 + if (limit) { 300 + query = query.limit(limit); 301 + } 302 + 303 + const sortOrder = direction === "asc" ? 1 : -1; 304 + query = query.sort({ [this.primary]: sortOrder }); 305 + 306 + return query; 307 + } 308 + } 309 + 310 + type SortAtResult = { sortAt: string }; 311 + type TimeLabeledResult = SingleKeyCursor; 312 + 313 + export class IsoTimeKey<TimeResult = SortAtResult> extends GenericSingleKey< 314 + TimeResult, 315 + TimeLabeledResult 316 + > { 317 + constructor() { 318 + super("sortAt"); 319 + } 320 + 321 + labelResult(result: TimeResult): TimeLabeledResult; 322 + labelResult<TimeResult extends SortAtResult>(result: TimeResult) { 323 + return { primary: result.sortAt }; 324 + } 325 + labeledResultToCursor(labeled: TimeLabeledResult) { 326 + const primaryDate = new Date(labeled.primary); 327 + if (isNaN(primaryDate.getTime())) { 328 + throw new InvalidRequestError("Invalid date for cursor"); 329 + } 330 + return { 331 + primary: primaryDate.toISOString(), 332 + }; 333 + } 334 + cursorToLabeledResult(cursor: SingleKeyCursor) { 335 + const primaryDate = new Date(cursor.primary); 336 + if (isNaN(primaryDate.getTime())) { 337 + throw new InvalidRequestError("Malformed cursor: invalid date"); 338 + } 339 + return { 340 + primary: primaryDate.toISOString(), 341 + }; 342 + } 343 + } 344 + 345 + export class IsoSortAtKey extends IsoTimeKey<{ 346 + sortAt: string; 347 + }> { 348 + constructor() { 349 + super(); 350 + } 351 + 352 + override labelResult(result: { sortAt: string }) { 353 + return { primary: result.sortAt }; 354 + } 355 + } 356 + 357 + type KeyResult = { key: string }; 358 + type RkeyLabeledResult = SingleKeyCursor; 359 + 360 + export class RkeyKey<RkeyResult = KeyResult> extends GenericSingleKey< 361 + RkeyResult, 362 + RkeyLabeledResult 363 + > { 364 + constructor() { 365 + super("key"); 366 + } 367 + 368 + labelResult(result: RkeyResult): RkeyLabeledResult; 369 + labelResult<RkeyResult extends KeyResult>(result: RkeyResult) { 370 + return { primary: result.key }; 371 + } 372 + labeledResultToCursor(labeled: RkeyLabeledResult) { 373 + return { 374 + primary: labeled.primary, 375 + }; 376 + } 377 + cursorToLabeledResult(cursor: SingleKeyCursor) { 378 + try { 379 + ensureValidRecordKey(cursor.primary); 380 + return { 381 + primary: cursor.primary, 382 + }; 383 + } catch { 384 + throw new InvalidRequestError("Malformed cursor"); 385 + } 386 + } 387 + } 388 + 389 + export class StashKeyKey extends RkeyKey<{ 390 + key: string; 391 + }> { 392 + constructor() { 393 + super(); 394 + } 395 + 396 + override labelResult(result: { key: string }) { 397 + return { primary: result.key }; 398 + } 399 + }
+55
data-plane/db/util.ts
··· 1 + import { FilterQuery } from "mongoose"; 2 + 3 + // MongoDB query builder for actor matching (DID or handle) 4 + export const actorFilter = <T>(actor: string): FilterQuery<T> => { 5 + if (actor.startsWith("did:")) { 6 + return { did: actor } as FilterQuery<T>; 7 + } else { 8 + return { handle: actor } as FilterQuery<T>; 9 + } 10 + }; 11 + 12 + // Filter for documents that are not soft deleted 13 + export const notSoftDeletedFilter = <T>(): FilterQuery<T> => { 14 + return { takedownRef: { $exists: false } } as FilterQuery<T>; 15 + }; 16 + 17 + // Check if a document is soft deleted 18 + export const softDeleted = ( 19 + actorOrRecord: { takedownRef?: string | null }, 20 + ): boolean => { 21 + return !!actorOrRecord.takedownRef; 22 + }; 23 + 24 + // Helper for date range queries 25 + export const dateRangeFilter = <T>( 26 + field: string, 27 + start?: Date, 28 + end?: Date, 29 + ): FilterQuery<T> => { 30 + const filter: Record<string, unknown> = {}; 31 + if (start || end) { 32 + filter[field] = {}; 33 + if (start) (filter[field] as Record<string, unknown>).$gte = start; 34 + if (end) (filter[field] as Record<string, unknown>).$lte = end; 35 + } 36 + return filter as FilterQuery<T>; 37 + }; 38 + 39 + // Helper for pagination 40 + export interface PaginationOptions { 41 + limit?: number; 42 + skip?: number; 43 + sort?: Record<string, 1 | -1>; 44 + } 45 + 46 + // Helper for creating compound filters 47 + export const andFilter = <T>( 48 + ...filters: FilterQuery<T>[] 49 + ): FilterQuery<T> => ({ 50 + $and: filters.filter((f) => Object.keys(f).length > 0), 51 + } as FilterQuery<T>); 52 + 53 + export const orFilter = <T>(...filters: FilterQuery<T>[]): FilterQuery<T> => ({ 54 + $or: filters.filter((f) => Object.keys(f).length > 0), 55 + } as FilterQuery<T>);
+68
data-plane/index.ts
··· 1 + import { IdResolver } from "@atp/identity"; 2 + import { Database } from "./db/index.ts"; 3 + import { getLogger, Logger } from "@logtape/logtape"; 4 + import { Blocks } from "./routes/blocks.ts"; 5 + import { Feeds } from "./routes/feeds.ts"; 6 + import { Follows } from "./routes/follows.ts"; 7 + import { Likes } from "./routes/likes.ts"; 8 + import { Moderation } from "./routes/moderation.ts"; 9 + import { Actors } from "./routes/actors.ts"; 10 + import { Identity } from "./routes/identity.ts"; 11 + import { Records } from "./routes/records.ts"; 12 + import { Relationships } from "./routes/relationships.ts"; 13 + import { Interactions } from "./routes/interactions.ts"; 14 + import { Reposts } from "./routes/reposts.ts"; 15 + import { Sync } from "./routes/sync.ts"; 16 + import { Threads } from "./routes/threads.ts"; 17 + 18 + export { RepoSubscription } from "./subscription.ts"; 19 + 20 + export type ServerContext = { 21 + db: Database; 22 + idResolver?: IdResolver; 23 + }; 24 + 25 + export class DataPlane { 26 + private db: Database; 27 + public logger: Logger; 28 + private idResolver?: IdResolver; 29 + 30 + // Route handlers as root-level properties 31 + public blocks: Blocks; 32 + public feeds: Feeds; 33 + public follows: Follows; 34 + public likes: Likes; 35 + public moderation: Moderation; 36 + public actors: Actors; 37 + public identity: Identity; 38 + public records: Records; 39 + public relationships: Relationships; 40 + public interactions: Interactions; 41 + public reposts: Reposts; 42 + public sync: Sync; 43 + public threads: Threads; 44 + 45 + constructor( 46 + db: Database, 47 + idResolver?: IdResolver, 48 + ) { 49 + this.db = db; 50 + this.idResolver = idResolver; 51 + this.logger = getLogger(["appview", "data-plane"]); 52 + 53 + // Initialize all route handlers 54 + this.blocks = new Blocks(db); 55 + this.feeds = new Feeds(db); 56 + this.follows = new Follows(db); 57 + this.likes = new Likes(db); 58 + this.moderation = new Moderation(db); 59 + this.actors = new Actors(db); 60 + this.identity = new Identity(idResolver); 61 + this.records = new Records(db); 62 + this.relationships = new Relationships(db); 63 + this.interactions = new Interactions(db); 64 + this.reposts = new Reposts(db); 65 + this.sync = new Sync(db); 66 + this.threads = new Threads(db); 67 + } 68 + }
+70
data-plane/routes/actors.ts
··· 1 + import { keyBy } from "@atp/common"; 2 + import { Database } from "../db/index.ts"; 3 + import { getRecords } from "./records.ts"; 4 + 5 + export class Actors { 6 + private db: Database; 7 + 8 + constructor(db: Database) { 9 + this.db = db; 10 + } 11 + 12 + async getActors(dids: string[]) { 13 + const profileUris = dids.map( 14 + (did) => `at://${did}/so.sprk.actor.profile/self`, 15 + ); 16 + 17 + const [ 18 + handlesRes, 19 + profiles, 20 + ] = await Promise.all([ 21 + this.db.models.Actor.find({ 22 + did: { $in: dids }, 23 + }), 24 + getRecords(this.db, profileUris), 25 + ]); 26 + 27 + const byDid = keyBy(handlesRes, "did"); 28 + const actors = dids.map((did, i) => { 29 + const row = byDid.get(did); 30 + 31 + return { 32 + exists: !!row, 33 + handle: row?.handle ?? undefined, 34 + profile: profiles.records[i], 35 + takenDown: !!row?.takedownRef, 36 + takedownRef: row?.takedownRef || undefined, 37 + tombstonedAt: undefined, // in current implementation, tombstoned actors are deleted 38 + upstreamStatus: row?.upstreamStatus ?? "", 39 + createdAt: profiles.records[i].createdAt, // @NOTE profile creation date not trusted in production 40 + tags: [], 41 + profileTags: [], 42 + }; 43 + }); 44 + 45 + return { actors }; 46 + } 47 + 48 + async getDidsByHandles(handles: string[]) { 49 + if (handles.length === 0) { 50 + return { dids: [] }; 51 + } 52 + 53 + const res = await this.db.models.Actor.find({ 54 + handle: { $in: handles }, 55 + }).select("did handle"); 56 + 57 + const byHandle = keyBy(res, "handle"); 58 + const dids = handles.map((handle) => byHandle.get(handle)?.did ?? ""); 59 + return { dids }; 60 + } 61 + 62 + async updateActorUpstreamStatus(actorDid: string, upstreamStatus: string) { 63 + await this.db.models.Actor.updateOne( 64 + { did: actorDid }, 65 + { $set: { upstreamStatus } }, 66 + ); 67 + 68 + return { success: true }; 69 + } 70 + }
+55
data-plane/routes/blocks.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { TimeCidKeyset } from "../db/pagination.ts"; 3 + 4 + export class Blocks { 5 + private db: Database; 6 + private timeCidKeyset: TimeCidKeyset; 7 + 8 + constructor(db: Database) { 9 + this.db = db; 10 + this.timeCidKeyset = new TimeCidKeyset(); 11 + } 12 + 13 + async getBidirectionalBlock(actorDid: string, targetDid: string) { 14 + // Check for blocks in both directions 15 + const block = await this.db.models.Block.findOne({ 16 + $or: [ 17 + { authorDid: actorDid, subject: targetDid }, 18 + { authorDid: targetDid, subject: actorDid }, 19 + ], 20 + }); 21 + 22 + return { 23 + blockUri: block?.uri || null, 24 + }; 25 + } 26 + 27 + async getBlocks(actorDid: string, limit = 50, cursor?: string) { 28 + // Build query for blocks by this actor 29 + const blocksQuery = this.db.models.Block.find({ authorDid: actorDid }); 30 + 31 + // Apply pagination using TimeCidKeyset 32 + const paginatedQuery = this.timeCidKeyset.paginate(blocksQuery, { 33 + limit, 34 + cursor, 35 + direction: "desc", 36 + }); 37 + 38 + const blocks = await paginatedQuery.exec(); 39 + 40 + // Generate cursor from the last item if we have a full page 41 + let nextCursor: string | undefined; 42 + if (blocks.length === limit && blocks.length > 0) { 43 + const lastBlock = blocks[blocks.length - 1]; 44 + nextCursor = this.timeCidKeyset.pack({ 45 + primary: lastBlock.createdAt, 46 + secondary: lastBlock.cid, 47 + }); 48 + } 49 + 50 + return { 51 + blockUris: blocks.map((b) => b.uri), 52 + cursor: nextCursor, 53 + }; 54 + } 55 + }
+161
data-plane/routes/feeds.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { TimeCidKeyset } from "../db/pagination.ts"; 3 + import { compositeTime } from "./records.ts"; 4 + 5 + // Helper function to format feed items 6 + function feedItemFromRow( 7 + item: { uri: string; cid: string; repostUri?: string }, 8 + ): { uri: string; cid: string; repost?: string; repostCid?: string } { 9 + return { 10 + uri: item.uri, 11 + cid: item.cid, 12 + repost: item.repostUri && item.repostUri !== item.uri 13 + ? item.repostUri 14 + : undefined, 15 + repostCid: item.repostUri && item.repostUri !== item.uri 16 + ? item.cid 17 + : undefined, 18 + }; 19 + } 20 + 21 + interface FeedItem { 22 + uri: string; 23 + cid: string; 24 + authorDid: string; 25 + createdAt: string; 26 + type: "post" | "repost"; 27 + repostUri?: string; 28 + sortAt: string; 29 + } 30 + 31 + export class Feeds { 32 + private db: Database; 33 + private timeCidKeyset: TimeCidKeyset; 34 + 35 + constructor(db: Database) { 36 + this.db = db; 37 + this.timeCidKeyset = new TimeCidKeyset(); 38 + } 39 + 40 + async getAuthorFeed( 41 + actorDid: string, 42 + limit = 50, 43 + cursor?: string, 44 + ) { 45 + // Get posts by this author (exclude replies) 46 + const postsQuery = this.db.models.Post.find({ 47 + authorDid: actorDid, 48 + reply: null, 49 + }); 50 + 51 + // Apply pagination to posts query 52 + const paginatedPostsQuery = this.timeCidKeyset.paginate(postsQuery, { 53 + limit, 54 + cursor, 55 + direction: "desc", 56 + }); 57 + 58 + const posts = await paginatedPostsQuery.exec(); 59 + 60 + // Transform posts 61 + const transformedPosts: FeedItem[] = posts.map((p) => ({ 62 + uri: p.uri, 63 + cid: p.cid, 64 + authorDid: p.authorDid, 65 + createdAt: p.createdAt, 66 + type: "post" as const, 67 + sortAt: compositeTime(p.createdAt, p.indexedAt) || p.createdAt, 68 + })); 69 + 70 + return { 71 + items: transformedPosts.map(feedItemFromRow), 72 + cursor: this.timeCidKeyset.packFromResult(transformedPosts), 73 + }; 74 + } 75 + 76 + async getTimeline(actorDid: string, limit = 50, cursor?: string) { 77 + // Get people this actor follows 78 + const follows = await this.db.models.Follow.find({ authorDid: actorDid }); 79 + 80 + const followedDids = follows.map((f) => f.subject); 81 + const timelineDids = [...followedDids, actorDid]; 82 + 83 + const postsLimit = Math.floor(limit * 0.8); 84 + const repostsLimit = limit - postsLimit; 85 + 86 + // Get timeline posts (exclude replies) 87 + const postsQuery = this.db.models.Post.find({ 88 + authorDid: { $in: timelineDids }, 89 + reply: null, 90 + }); 91 + 92 + // Apply pagination to posts query 93 + const paginatedPostsQuery = this.timeCidKeyset.paginate(postsQuery, { 94 + limit: postsLimit, 95 + cursor, 96 + direction: "desc", 97 + }); 98 + 99 + const posts = await paginatedPostsQuery.exec(); 100 + 101 + // Get timeline reposts 102 + const repostsQuery = this.db.models.Repost.find({ 103 + authorDid: { $in: timelineDids }, 104 + }); 105 + 106 + // Apply pagination to reposts query 107 + const paginatedRepostsQuery = this.timeCidKeyset.paginate(repostsQuery, { 108 + limit: repostsLimit, 109 + cursor, 110 + direction: "desc", 111 + }); 112 + 113 + const reposts = await paginatedRepostsQuery.exec(); 114 + 115 + // Transform and combine results 116 + const transformedPosts: FeedItem[] = posts.map((p) => ({ 117 + uri: p.uri, 118 + cid: p.cid, 119 + authorDid: p.authorDid, 120 + createdAt: p.createdAt, 121 + type: "post" as const, 122 + sortAt: compositeTime(p.createdAt, p.indexedAt) || p.createdAt, 123 + })); 124 + 125 + const transformedReposts: FeedItem[] = reposts.map((r) => ({ 126 + uri: r.subject?.uri || r.uri, 127 + cid: r.cid, 128 + authorDid: r.authorDid, 129 + createdAt: r.createdAt, 130 + type: "repost" as const, 131 + repostUri: r.uri, 132 + sortAt: compositeTime(r.createdAt, r.indexedAt) || r.createdAt, 133 + })); 134 + 135 + // Combine and sort all items 136 + const allItems = [...transformedPosts, ...transformedReposts] 137 + .sort((a, b) => { 138 + // Sort by sortAt descending, then by cid descending 139 + if (a.sortAt !== b.sortAt) { 140 + return a.sortAt > b.sortAt ? -1 : 1; 141 + } 142 + return a.cid > b.cid ? -1 : 1; 143 + }) 144 + .slice(0, limit); 145 + 146 + // Generate cursor from the last item if we have a full page 147 + let nextCursor: string | undefined; 148 + if (allItems.length === limit && allItems.length > 0) { 149 + const lastItem = allItems[allItems.length - 1]; 150 + nextCursor = this.timeCidKeyset.pack({ 151 + primary: lastItem.sortAt, 152 + secondary: lastItem.cid, 153 + }); 154 + } 155 + 156 + return { 157 + items: allItems.map(feedItemFromRow), 158 + cursor: nextCursor, 159 + }; 160 + } 161 + }
+142
data-plane/routes/follows.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { TimeCidKeyset } from "../db/pagination.ts"; 3 + 4 + // Create a simple FollowsFollowing class since we removed the proto import 5 + class FollowsFollowing { 6 + targetDid: string; 7 + dids: string[]; 8 + 9 + constructor(data: { targetDid: string; dids: string[] }) { 10 + this.targetDid = data.targetDid; 11 + this.dids = data.dids; 12 + } 13 + } 14 + 15 + export class Follows { 16 + private db: Database; 17 + private timeCidKeyset: TimeCidKeyset; 18 + 19 + constructor(db: Database) { 20 + this.db = db; 21 + this.timeCidKeyset = new TimeCidKeyset(); 22 + } 23 + 24 + async getActorFollowsActors(actorDid: string, targetDids: string[]) { 25 + if (targetDids.length < 1) { 26 + return { uris: [] }; 27 + } 28 + 29 + const follows = await this.db.models.Follow.find({ 30 + authorDid: actorDid, 31 + subject: { $in: targetDids }, 32 + }); 33 + 34 + // Create a map for quick lookup 35 + const followMap = new Map(follows.map((f) => [f.subject, f.uri])); 36 + const uris = targetDids.map((did) => followMap.get(did) || ""); 37 + 38 + return { uris }; 39 + } 40 + 41 + async getFollowers(actorDid: string, limit = 50, cursor?: string) { 42 + // Build query for followers (people who follow this actor) 43 + const followersQuery = this.db.models.Follow.find({ subject: actorDid }) 44 + .populate("actor", "did handle indexedAt takedownRef upstreamStatus"); 45 + 46 + // Apply pagination using TimeCidKeyset 47 + const paginatedQuery = this.timeCidKeyset.paginate(followersQuery, { 48 + limit, 49 + cursor, 50 + direction: "desc", 51 + }); 52 + 53 + const followers = await paginatedQuery.exec(); 54 + 55 + // Generate cursor from the last item if we have a full page 56 + let nextCursor: string | undefined; 57 + if (followers.length === limit && followers.length > 0) { 58 + const lastFollower = followers[followers.length - 1]; 59 + nextCursor = this.timeCidKeyset.pack({ 60 + primary: lastFollower.createdAt, 61 + secondary: lastFollower.cid, 62 + }); 63 + } 64 + 65 + return { 66 + followers: followers.map((f) => ({ 67 + uri: f.uri, 68 + actorDid: f.authorDid, 69 + subjectDid: f.subject, 70 + })), 71 + cursor: nextCursor, 72 + }; 73 + } 74 + 75 + async getFollows(actorDid: string, limit = 50, cursor?: string) { 76 + // Build query for follows (people this actor follows) 77 + const followsQuery = this.db.models.Follow.find({ authorDid: actorDid }) 78 + .populate("actor", "did handle indexedAt takedownRef upstreamStatus"); 79 + 80 + // Apply pagination using TimeCidKeyset 81 + const paginatedQuery = this.timeCidKeyset.paginate(followsQuery, { 82 + limit, 83 + cursor, 84 + direction: "desc", 85 + }); 86 + 87 + const follows = await paginatedQuery.exec(); 88 + 89 + // Generate cursor from the last item if we have a full page 90 + let nextCursor: string | undefined; 91 + if (follows.length === limit && follows.length > 0) { 92 + const lastFollow = follows[follows.length - 1]; 93 + nextCursor = this.timeCidKeyset.pack({ 94 + primary: lastFollow.createdAt, 95 + secondary: lastFollow.cid, 96 + }); 97 + } 98 + 99 + return { 100 + follows: follows.map((f) => ({ 101 + uri: f.uri, 102 + actorDid: f.authorDid, 103 + subjectDid: f.subject, 104 + })), 105 + cursor: nextCursor, 106 + }; 107 + } 108 + 109 + async getFollowsFollowing(viewerDid: string, subjectDids: string[]) { 110 + /* 111 + * 1. Get all the people Alice is following 112 + * 2. Get all the people Dan is followed by 113 + * 3. Find the intersection 114 + */ 115 + 116 + const results: FollowsFollowing[] = []; 117 + 118 + for (const subjectDid of subjectDids) { 119 + // Get people who follow the subject (Dan's followers) 120 + const subjectFollowers = await this.db.models.Follow.find({ 121 + subject: subjectDid, 122 + }); 123 + 124 + const followerDids = subjectFollowers.map((f) => f.authorDid); 125 + 126 + // Find which of these followers Alice also follows 127 + const mutualConnections = await this.db.models.Follow.find({ 128 + authorDid: viewerDid, 129 + subject: { $in: followerDids }, 130 + }); 131 + 132 + results.push( 133 + new FollowsFollowing({ 134 + targetDid: subjectDid, 135 + dids: mutualConnections.map((connection) => connection.subject), 136 + }), 137 + ); 138 + } 139 + 140 + return { results }; 141 + } 142 + }
+191
data-plane/routes/identity.ts
··· 1 + import { DidDocument, getDid, getHandle, IdResolver } from "@atp/identity"; 2 + 3 + // Helper function to format DID document result 4 + function getResultFromDoc(doc: DidDocument) { 5 + const keys: Record<string, { Type: string; PublicKeyMultibase: string }> = {}; 6 + doc.verificationMethod?.forEach((method) => { 7 + const id = method.id.split("#").at(1); 8 + if (!id) return; 9 + keys[id] = { 10 + Type: method.type, 11 + PublicKeyMultibase: method.publicKeyMultibase || "", 12 + }; 13 + }); 14 + 15 + const services: Record<string, { Type: string; URL: string }> = {}; 16 + doc.service?.forEach((service) => { 17 + const id = service.id.split("#").at(1); 18 + if (!id) return; 19 + if (typeof service.serviceEndpoint !== "string") return; 20 + services[id] = { 21 + Type: service.type, 22 + URL: service.serviceEndpoint, 23 + }; 24 + }); 25 + 26 + return { 27 + did: getDid(doc), 28 + handle: getHandle(doc), 29 + keys: JSON.stringify(keys), 30 + services: JSON.stringify(services), 31 + updated: new Date().toISOString(), 32 + }; 33 + } 34 + 35 + export class Identity { 36 + private idResolver?: IdResolver; 37 + 38 + constructor(idResolver?: IdResolver) { 39 + this.idResolver = idResolver; 40 + } 41 + 42 + async getByDid(did: string) { 43 + if (!this.idResolver) { 44 + throw new Error("ID resolver not available"); 45 + } 46 + 47 + try { 48 + const doc = await this.idResolver.did.resolve(did); 49 + if (!doc) { 50 + throw new Error("Identity not found"); 51 + } 52 + 53 + const result = getResultFromDoc(doc); 54 + return result; 55 + } catch (error) { 56 + console.error("Error resolving DID:", error); 57 + throw new Error("Failed to resolve identity"); 58 + } 59 + } 60 + 61 + async getByHandle(handle: string) { 62 + if (!this.idResolver) { 63 + throw new Error("ID resolver not available"); 64 + } 65 + 66 + try { 67 + const did = await this.idResolver.handle.resolve(handle); 68 + if (!did) { 69 + throw new Error("Identity not found"); 70 + } 71 + 72 + const doc = await this.idResolver.did.resolve(did); 73 + if (!doc || did !== getDid(doc)) { 74 + throw new Error("Identity not found"); 75 + } 76 + 77 + const result = getResultFromDoc(doc); 78 + return result; 79 + } catch (error) { 80 + console.error("Error resolving handle:", error); 81 + throw new Error("Failed to resolve identity"); 82 + } 83 + } 84 + 85 + async resolve(identifier: string, type?: "did" | "handle") { 86 + if (!this.idResolver) { 87 + throw new Error("ID resolver not available"); 88 + } 89 + 90 + try { 91 + let doc: DidDocument | null = null; 92 + let resolvedDid: string | null = null; 93 + 94 + // Auto-detect type if not specified 95 + const identifierType = type || 96 + (identifier.startsWith("did:") ? "did" : "handle"); 97 + 98 + if (identifierType === "did") { 99 + doc = await this.idResolver.did.resolve(identifier); 100 + resolvedDid = identifier; 101 + } else { 102 + resolvedDid = await this.idResolver.handle.resolve(identifier) || null; 103 + if (resolvedDid) { 104 + doc = await this.idResolver.did.resolve(resolvedDid); 105 + } 106 + } 107 + 108 + if (!doc || (resolvedDid && resolvedDid !== getDid(doc))) { 109 + throw new Error("Identity not found"); 110 + } 111 + 112 + const result = getResultFromDoc(doc); 113 + return { 114 + ...result, 115 + resolvedFrom: { 116 + identifier, 117 + type: identifierType, 118 + }, 119 + }; 120 + } catch (error) { 121 + console.error("Error resolving identity:", error); 122 + throw new Error("Failed to resolve identity"); 123 + } 124 + } 125 + 126 + async resolveBatch( 127 + identifiers: Array<{ value: string; type?: "did" | "handle" }>, 128 + ) { 129 + if (!this.idResolver) { 130 + throw new Error("ID resolver not available"); 131 + } 132 + 133 + const results = await Promise.allSettled( 134 + identifiers.map(async ({ value, type }) => { 135 + try { 136 + let doc: DidDocument | null = null; 137 + let resolvedDid: string | null = null; 138 + 139 + const identifierType = type || 140 + (value.startsWith("did:") ? "did" : "handle"); 141 + 142 + if (identifierType === "did") { 143 + doc = await this.idResolver!.did.resolve(value); 144 + resolvedDid = value; 145 + } else { 146 + resolvedDid = await this.idResolver!.handle.resolve(value) || null; 147 + if (resolvedDid) { 148 + doc = await this.idResolver!.did.resolve(resolvedDid); 149 + } 150 + } 151 + 152 + if (!doc || (resolvedDid && resolvedDid !== getDid(doc))) { 153 + return { 154 + identifier: value, 155 + type: identifierType, 156 + error: "Identity not found", 157 + }; 158 + } 159 + 160 + return { 161 + identifier: value, 162 + type: identifierType, 163 + ...getResultFromDoc(doc), 164 + }; 165 + } catch (_error) { 166 + return { 167 + identifier: value, 168 + type: type || "unknown", 169 + error: "Failed to resolve identity", 170 + }; 171 + } 172 + }), 173 + ); 174 + 175 + const resolved = results.map((result, index) => ({ 176 + index, 177 + success: result.status === "fulfilled", 178 + data: result.status === "fulfilled" ? result.value : null, 179 + error: result.status === "rejected" ? result.reason?.message : null, 180 + })); 181 + 182 + return { 183 + results: resolved, 184 + summary: { 185 + total: identifiers.length, 186 + successful: resolved.filter((r) => r.success).length, 187 + failed: resolved.filter((r) => !r.success).length, 188 + }, 189 + }; 190 + } 191 + }
+124
data-plane/routes/interactions.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + 3 + // Types for MongoDB aggregation results 4 + interface AggregationResult { 5 + _id: string; 6 + count: number; 7 + } 8 + 9 + export class Interactions { 10 + private db: Database; 11 + 12 + constructor(db: Database) { 13 + this.db = db; 14 + } 15 + 16 + async getInteractionCounts(refs: Array<{ uri: string }>) { 17 + const uris = refs.map((ref) => ref.uri); 18 + if (uris.length === 0) { 19 + return { likes: [], replies: [], reposts: [], quotes: [] }; 20 + } 21 + 22 + // Get interaction counts for posts 23 + const [likes, reposts] = await Promise.all([ 24 + // Count likes for each URI 25 + this.db.models.Like.aggregate([ 26 + { $match: { "subject.uri": { $in: uris } } }, 27 + { $group: { _id: "$subject.uri", count: { $sum: 1 } } }, 28 + ]), 29 + // Count reposts for each URI 30 + this.db.models.Repost.aggregate([ 31 + { $match: { "subject.uri": { $in: uris } } }, 32 + { $group: { _id: "$subject.uri", count: { $sum: 1 } } }, 33 + ]), 34 + ]); 35 + 36 + // Count replies by finding posts that have a reply.parent.uri matching our URIs 37 + const replies = await this.db.models.Post.aggregate([ 38 + { $match: { "reply.parent.uri": { $in: uris } } }, 39 + { $group: { _id: "$reply.parent.uri", count: { $sum: 1 } } }, 40 + ]); 41 + 42 + // Count quotes by finding posts that have an embed.record.uri matching our URIs 43 + const quotes = await this.db.models.Post.aggregate([ 44 + { $match: { "embed.record.uri": { $in: uris } } }, 45 + { $group: { _id: "$embed.record.uri", count: { $sum: 1 } } }, 46 + ]); 47 + 48 + // Create lookup maps 49 + const likesMap = new Map( 50 + likes.map((item: AggregationResult) => [item._id, item.count]), 51 + ); 52 + const repliesMap = new Map( 53 + replies.map((item: AggregationResult) => [item._id, item.count]), 54 + ); 55 + const repostsMap = new Map( 56 + reposts.map((item: AggregationResult) => [item._id, item.count]), 57 + ); 58 + const quotesMap = new Map( 59 + quotes.map((item: AggregationResult) => [item._id, item.count]), 60 + ); 61 + 62 + return { 63 + likes: uris.map((uri) => likesMap.get(uri) ?? 0), 64 + replies: uris.map((uri) => repliesMap.get(uri) ?? 0), 65 + reposts: uris.map((uri) => repostsMap.get(uri) ?? 0), 66 + quotes: uris.map((uri) => quotesMap.get(uri) ?? 0), 67 + }; 68 + } 69 + 70 + async getCountsForUsers(dids: string[]) { 71 + if (dids.length === 0) { 72 + return { 73 + followers: [], 74 + following: [], 75 + posts: [], 76 + feeds: [], 77 + }; 78 + } 79 + 80 + const [followers, following, posts, feeds] = await Promise.all([ 81 + // Count followers for each DID 82 + this.db.models.Follow.aggregate([ 83 + { $match: { subject: { $in: dids } } }, 84 + { $group: { _id: "$subject", count: { $sum: 1 } } }, 85 + ]), 86 + // Count following for each DID 87 + this.db.models.Follow.aggregate([ 88 + { $match: { authorDid: { $in: dids } } }, 89 + { $group: { _id: "$authorDid", count: { $sum: 1 } } }, 90 + ]), 91 + // Count posts for each DID 92 + this.db.models.Post.aggregate([ 93 + { $match: { authorDid: { $in: dids } } }, 94 + { $group: { _id: "$authorDid", count: { $sum: 1 } } }, 95 + ]), 96 + // Count generators for each DID 97 + this.db.models.BskyGenerator.aggregate([ 98 + { $match: { authorDid: { $in: dids } } }, 99 + { $group: { _id: "$authorDid", count: { $sum: 1 } } }, 100 + ]), 101 + ]); 102 + 103 + // Create lookup maps 104 + const followersMap = new Map( 105 + followers.map((item: AggregationResult) => [item._id, item.count]), 106 + ); 107 + const followingMap = new Map( 108 + following.map((item: AggregationResult) => [item._id, item.count]), 109 + ); 110 + const postsMap = new Map( 111 + posts.map((item: AggregationResult) => [item._id, item.count]), 112 + ); 113 + const feedsMap = new Map( 114 + feeds.map((item: AggregationResult) => [item._id, item.count]), 115 + ); 116 + 117 + return { 118 + followers: dids.map((did) => followersMap.get(did) ?? 0), 119 + following: dids.map((did) => followingMap.get(did) ?? 0), 120 + posts: dids.map((did) => postsMap.get(did) ?? 0), 121 + feeds: dids.map((did) => feedsMap.get(did) ?? 0), 122 + }; 123 + } 124 + }
+103
data-plane/routes/likes.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { TimeCidKeyset } from "../db/pagination.ts"; 3 + 4 + export class Likes { 5 + private db: Database; 6 + private timeCidKeyset: TimeCidKeyset; 7 + 8 + constructor(db: Database) { 9 + this.db = db; 10 + this.timeCidKeyset = new TimeCidKeyset(); 11 + } 12 + 13 + async bySubject( 14 + subject?: { uri: string; cid?: string }, 15 + limit = 50, 16 + cursor?: string, 17 + ) { 18 + if (!subject?.uri) { 19 + return { uris: [], cursor: undefined }; 20 + } 21 + 22 + // Build query for likes on this subject 23 + const likesQuery = this.db.models.Like.find({ subject: subject.uri }); 24 + 25 + // Apply pagination using TimeCidKeyset 26 + const paginatedQuery = this.timeCidKeyset.paginate(likesQuery, { 27 + limit, 28 + cursor, 29 + direction: "desc", 30 + }); 31 + 32 + const likes = await paginatedQuery.exec(); 33 + 34 + // Generate cursor from the last item if we have a full page 35 + let nextCursor: string | undefined; 36 + if (likes.length === limit && likes.length > 0) { 37 + const lastLike = likes[likes.length - 1]; 38 + nextCursor = this.timeCidKeyset.pack({ 39 + primary: lastLike.createdAt, 40 + secondary: lastLike.cid, 41 + }); 42 + } 43 + 44 + return { 45 + uris: likes.map((l) => l.uri), 46 + cursor: nextCursor, 47 + }; 48 + } 49 + 50 + async byActorAndSubjects( 51 + actorDid: string, 52 + refs: Array<{ uri: string; cid?: string }>, 53 + ) { 54 + if (refs.length === 0) { 55 + return { uris: [] }; 56 + } 57 + 58 + // Get all likes by this actor for the specified subjects 59 + const subjectUris = refs.map(({ uri }) => uri); 60 + const likes = await this.db.models.Like.find({ 61 + authorDid: actorDid, 62 + subject: { $in: subjectUris }, 63 + }); 64 + 65 + // Create a map for quick lookup 66 + const likeMap = new Map(likes.map((l) => [l.subject, l.uri])); 67 + const uris = refs.map(({ uri }) => likeMap.get(uri) || ""); 68 + 69 + return { uris }; 70 + } 71 + 72 + async getActor(actorDid: string, limit = 50, cursor?: string) { 73 + // Build query for likes by this actor 74 + const likesQuery = this.db.models.Like.find({ authorDid: actorDid }); 75 + 76 + // Apply pagination using TimeCidKeyset 77 + const paginatedQuery = this.timeCidKeyset.paginate(likesQuery, { 78 + limit, 79 + cursor, 80 + direction: "desc", 81 + }); 82 + 83 + const likes = await paginatedQuery.exec(); 84 + 85 + // Generate cursor from the last item if we have a full page 86 + let nextCursor: string | undefined; 87 + if (likes.length === limit && likes.length > 0) { 88 + const lastLike = likes[likes.length - 1]; 89 + nextCursor = this.timeCidKeyset.pack({ 90 + primary: lastLike.createdAt, 91 + secondary: lastLike.cid, 92 + }); 93 + } 94 + 95 + return { 96 + likes: likes.map((l) => ({ 97 + uri: l.uri, 98 + subject: l.subject, 99 + })), 100 + cursor: nextCursor, 101 + }; 102 + } 103 + }
+101
data-plane/routes/moderation.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + 3 + export class Moderation { 4 + private db: Database; 5 + 6 + constructor(db: Database) { 7 + this.db = db; 8 + } 9 + 10 + async getActorTakedown(did: string) { 11 + const actor = await this.db.models.Actor.findOne({ did }); 12 + 13 + return { 14 + takenDown: !!actor?.takedownRef, 15 + takedownRef: actor?.takedownRef || undefined, 16 + }; 17 + } 18 + 19 + async getBlobTakedown(did: string, cid: string) { 20 + const blobTakedown = await this.db.models.BlobTakedown.findOne({ 21 + did, 22 + cid, 23 + }); 24 + 25 + return { 26 + takenDown: !!blobTakedown, 27 + takedownRef: blobTakedown?.ref || undefined, 28 + }; 29 + } 30 + 31 + async getRecordTakedown(recordUri: string) { 32 + const record = await this.db.models.Record.findOne({ 33 + uri: recordUri, 34 + }); 35 + 36 + return { 37 + takenDown: !!record?.takedownRef, 38 + takedownRef: record?.takedownRef || undefined, 39 + }; 40 + } 41 + 42 + async takedownActor(did: string, ref?: string) { 43 + await this.db.models.Actor.updateOne( 44 + { did }, 45 + { $set: { takedownRef: ref || "TAKEDOWN" } }, 46 + ); 47 + 48 + return { success: true }; 49 + } 50 + 51 + async takedownBlob(did: string, cid: string, ref?: string) { 52 + await this.db.models.BlobTakedown.findOneAndUpdate( 53 + { did, cid }, 54 + { 55 + did, 56 + cid, 57 + ref: ref || "TAKEDOWN", 58 + reason: "Manual takedown", 59 + takenDownBy: "system", 60 + takenDownAt: new Date().toISOString(), 61 + applied: true, 62 + }, 63 + { upsert: true, new: true }, 64 + ); 65 + 66 + return { success: true }; 67 + } 68 + 69 + async takedownRecord(recordUri: string, ref?: string) { 70 + await this.db.models.Record.updateOne( 71 + { uri: recordUri }, 72 + { $set: { takedownRef: ref || "TAKEDOWN" } }, 73 + ); 74 + 75 + return { success: true }; 76 + } 77 + 78 + async untakedownActor(did: string) { 79 + await this.db.models.Actor.updateOne( 80 + { did }, 81 + { $unset: { takedownRef: "" } }, 82 + ); 83 + 84 + return { success: true }; 85 + } 86 + 87 + async untakedownBlob(did: string, cid: string) { 88 + await this.db.models.BlobTakedown.deleteOne({ did, cid }); 89 + 90 + return { success: true }; 91 + } 92 + 93 + async untakedownRecord(recordUri: string) { 94 + await this.db.models.Record.updateOne( 95 + { uri: recordUri }, 96 + { $unset: { takedownRef: "" } }, 97 + ); 98 + 99 + return { success: true }; 100 + } 101 + }
+167
data-plane/routes/records.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { AtUri } from "@atp/syntax"; 3 + import { ids } from "../../lex/lexicons.ts"; 4 + import { keyBy } from "@atp/common"; 5 + 6 + export type Record = { 7 + record: string; 8 + uri: string; 9 + cid?: string; 10 + createdAt?: string; 11 + sortedAt?: string; 12 + indexedAt?: string; 13 + takenDown: boolean; 14 + takedownRef?: string; 15 + }; 16 + 17 + // Helper function to get records by collection 18 + export async function getRecords( 19 + db: Database, 20 + uris: string[], 21 + collection?: string | string[], 22 + ): Promise<{ 23 + records: Array<Record>; 24 + }> { 25 + const validUris = collection 26 + ? uris.filter((uri) => new AtUri(uri).collection === collection) 27 + : uris; 28 + 29 + const res = validUris.length 30 + ? await db.models.Record.find({ 31 + uri: { $in: validUris }, 32 + }) 33 + : []; 34 + 35 + const byUri = keyBy(res, "uri"); 36 + 37 + const records: Record[] = uris.map((uri) => { 38 + const row = byUri.get(uri); 39 + const json = row ? row.json : JSON.stringify(null); 40 + const createdAt = JSON.parse(json)?.["createdAt"] 41 + ? new Date(JSON.parse(json)?.["createdAt"]).toISOString() 42 + : undefined; 43 + const indexedAt = row?.indexedAt 44 + ? new Date(row.indexedAt).toISOString() 45 + : undefined; 46 + 47 + return { 48 + record: json, 49 + uri, 50 + cid: row?.cid, 51 + createdAt, 52 + indexedAt, 53 + sortedAt: compositeTime(createdAt, indexedAt), 54 + takenDown: !!row?.takedownRef, 55 + takedownRef: row?.takedownRef || undefined, 56 + }; 57 + }); 58 + 59 + return { records }; 60 + } 61 + 62 + // Helper function to get post records with metadata 63 + async function getPostRecords( 64 + db: Database, 65 + uris: string[], 66 + ): Promise<{ 67 + records: Array<Record>; 68 + }> { 69 + const [{ records }] = await Promise.all([ 70 + getRecords(db, uris, ids.SoSprkFeedPost), 71 + uris.length 72 + ? db.models.Post.find({ 73 + uri: { $in: uris }, 74 + }) 75 + : [], 76 + ]); 77 + 78 + return { records }; 79 + } 80 + 81 + // Helper function for composite time 82 + export function compositeTime(ts1?: string, ts2?: string): string | undefined { 83 + if (!ts1) return ts2; 84 + if (!ts2) return ts1; 85 + return new Date(ts1) < new Date(ts2) ? ts1 : ts2; 86 + } 87 + 88 + export class Records { 89 + private db: Database; 90 + 91 + constructor(db: Database) { 92 + this.db = db; 93 + } 94 + 95 + async getBlockRecords(uris: string[]) { 96 + const result = await getRecords(this.db, uris, ids.AppBskyGraphBlock); 97 + return result; 98 + } 99 + 100 + async getFeedGeneratorRecords(uris: string[]) { 101 + try { 102 + const result = await getRecords(this.db, uris, [ 103 + ids.AppBskyFeedGenerator, 104 + ids.SoSprkFeedGenerator, 105 + ]); 106 + return result; 107 + } catch (error) { 108 + console.error("Error fetching feed generator records:", error); 109 + throw new Error("Failed to fetch records"); 110 + } 111 + } 112 + 113 + async getFollowRecords(uris: string[]) { 114 + const result = await getRecords(this.db, uris, ids.AppBskyGraphFollow); 115 + return result; 116 + } 117 + 118 + async getLikeRecords(uris: string[]) { 119 + try { 120 + const result = await getRecords(this.db, uris, ids.SoSprkFeedLike); 121 + return result; 122 + } catch (error) { 123 + console.error("Error fetching like records:", error); 124 + throw new Error("Failed to fetch records"); 125 + } 126 + } 127 + 128 + async getPostRecords(uris: string[]) { 129 + try { 130 + const result = await getPostRecords(this.db, uris); 131 + return result; 132 + } catch (error) { 133 + console.error("Error fetching post records:", error); 134 + throw new Error("Failed to fetch records"); 135 + } 136 + } 137 + 138 + async getProfileRecords(uris: string[]) { 139 + try { 140 + const result = await getRecords(this.db, uris, ids.SoSprkActorProfile); 141 + return result; 142 + } catch (error) { 143 + console.error("Error fetching profile records:", error); 144 + throw new Error("Failed to fetch records"); 145 + } 146 + } 147 + 148 + async getRepostRecords(uris: string[]) { 149 + try { 150 + const result = await getRecords(this.db, uris, ids.AppBskyFeedRepost); 151 + return result; 152 + } catch (error) { 153 + console.error("Error fetching repost records:", error); 154 + throw new Error("Failed to fetch records"); 155 + } 156 + } 157 + 158 + async getRecords(uris: string[]) { 159 + try { 160 + const result = await getRecords(this.db, uris); 161 + return result; 162 + } catch (error) { 163 + console.error("Error fetching records:", error); 164 + throw new Error("Failed to fetch records"); 165 + } 166 + } 167 + }
+103
data-plane/routes/relationships.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + 3 + export class Relationships { 4 + private db: Database; 5 + 6 + constructor(db: Database) { 7 + this.db = db; 8 + } 9 + 10 + async getRelationships(actorDid: string, targetDids: string[]) { 11 + if (targetDids.length === 0) { 12 + return { relationships: [] }; 13 + } 14 + 15 + const relationships = await Promise.all( 16 + targetDids.map(async (targetDid) => { 17 + const [ 18 + blocking, 19 + blockedBy, 20 + following, 21 + followedBy, 22 + ] = await Promise.all([ 23 + // Check if actor blocks target 24 + this.db.models.Block.findOne({ 25 + authorDid: actorDid, 26 + subjectDid: targetDid, 27 + }), 28 + // Check if target blocks actor 29 + this.db.models.Block.findOne({ 30 + authorDid: targetDid, 31 + subjectDid: actorDid, 32 + }), 33 + // Check if actor follows target 34 + this.db.models.Follow.findOne({ 35 + authorDid: actorDid, 36 + subject: targetDid, 37 + }), 38 + // Check if target follows actor 39 + this.db.models.Follow.findOne({ 40 + authorDid: targetDid, 41 + subject: actorDid, 42 + }), 43 + ]); 44 + 45 + return { 46 + muted: false, 47 + blockedBy: blockedBy?.uri || "", 48 + blocking: blocking?.uri || "", 49 + following: following?.uri || "", 50 + followedBy: followedBy?.uri || "", 51 + }; 52 + }), 53 + ); 54 + 55 + return { relationships }; 56 + } 57 + 58 + async getBlockExistence(pairs: Array<{ a: string; b: string }>) { 59 + if (pairs.length === 0) { 60 + return { exists: [], blocks: [] }; 61 + } 62 + 63 + const results = await Promise.all( 64 + pairs.map(async (pair) => { 65 + const [ 66 + blocking, 67 + blockedBy, 68 + ] = await Promise.all([ 69 + // Check if A blocks B 70 + this.db.models.Block.findOne({ 71 + authorDid: pair.a, 72 + subjectDid: pair.b, 73 + }), 74 + // Check if B blocks A 75 + this.db.models.Block.findOne({ 76 + authorDid: pair.b, 77 + subjectDid: pair.a, 78 + }), 79 + ]); 80 + 81 + const hasBlocks = !!( 82 + blocking || 83 + blockedBy 84 + ); 85 + 86 + return { 87 + exists: hasBlocks, 88 + blocks: { 89 + blockedBy: blockedBy?.uri || undefined, 90 + blocking: blocking?.uri || undefined, 91 + blockedByList: undefined, 92 + blockingByList: undefined, 93 + }, 94 + }; 95 + }), 96 + ); 97 + 98 + return { 99 + exists: results.map((r) => r.exists), 100 + blocks: results.map((r) => r.blocks), 101 + }; 102 + } 103 + }
+102
data-plane/routes/reposts.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + import { TimeCidKeyset } from "../db/pagination.ts"; 3 + 4 + export class Reposts { 5 + private db: Database; 6 + private timeCidKeyset: TimeCidKeyset; 7 + 8 + constructor(db: Database) { 9 + this.db = db; 10 + this.timeCidKeyset = new TimeCidKeyset(); 11 + } 12 + 13 + async bySubject( 14 + subject?: { uri: string; cid?: string }, 15 + limit = 50, 16 + cursor?: string, 17 + ) { 18 + if (!subject?.uri) { 19 + return { uris: [], cursor: undefined }; 20 + } 21 + 22 + // Build query for reposts of this subject 23 + const repostsQuery = this.db.models.Repost.find({ 24 + "subject.uri": subject.uri, 25 + }); 26 + 27 + // Apply pagination using TimeCidKeyset 28 + const paginatedQuery = this.timeCidKeyset.paginate(repostsQuery, { 29 + limit, 30 + cursor, 31 + direction: "desc", 32 + }); 33 + 34 + const reposts = await paginatedQuery.exec(); 35 + 36 + // Generate cursor from the last item if we have a full page 37 + let nextCursor: string | undefined; 38 + if (reposts.length === limit && reposts.length > 0) { 39 + const lastRepost = reposts[reposts.length - 1]; 40 + nextCursor = this.timeCidKeyset.pack({ 41 + primary: lastRepost.createdAt, 42 + secondary: lastRepost.cid, 43 + }); 44 + } 45 + 46 + return { 47 + uris: reposts.map((r) => r.uri), 48 + cursor: nextCursor, 49 + }; 50 + } 51 + 52 + async byActorAndSubjects( 53 + actorDid: string, 54 + refs: Array<{ uri: string; cid?: string }>, 55 + ) { 56 + if (refs.length === 0) { 57 + return { uris: [] }; 58 + } 59 + 60 + // Get all reposts by this actor for the specified subjects 61 + const subjectUris = refs.map(({ uri }) => uri); 62 + const reposts = await this.db.models.Repost.find({ 63 + authorDid: actorDid, 64 + "subject.uri": { $in: subjectUris }, 65 + }); 66 + 67 + // Create a map for quick lookup 68 + const repostMap = new Map(reposts.map((r) => [r.subject.uri, r.uri])); 69 + const uris = refs.map(({ uri }) => repostMap.get(uri) || ""); 70 + 71 + return { uris }; 72 + } 73 + 74 + async getActor(actorDid: string, limit = 50, cursor?: string) { 75 + // Build query for reposts by this actor 76 + const repostsQuery = this.db.models.Repost.find({ authorDid: actorDid }); 77 + 78 + // Apply pagination using TimeCidKeyset 79 + const paginatedQuery = this.timeCidKeyset.paginate(repostsQuery, { 80 + limit, 81 + cursor, 82 + direction: "desc", 83 + }); 84 + 85 + const reposts = await paginatedQuery.exec(); 86 + 87 + // Generate cursor from the last item if we have a full page 88 + let nextCursor: string | undefined; 89 + if (reposts.length === limit && reposts.length > 0) { 90 + const lastRepost = reposts[reposts.length - 1]; 91 + nextCursor = this.timeCidKeyset.pack({ 92 + primary: lastRepost.createdAt, 93 + secondary: lastRepost.cid, 94 + }); 95 + } 96 + 97 + return { 98 + uris: reposts.map((r) => r.uri), 99 + cursor: nextCursor, 100 + }; 101 + } 102 + }
+21
data-plane/routes/sync.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + 3 + async function getLatestRev(actorDid: string, db: Database) { 4 + const res = await db.models.ActorSync.findOne({ where: { did: actorDid } }); 5 + return { 6 + rev: res?.repoRev ?? undefined, 7 + }; 8 + } 9 + 10 + export class Sync { 11 + private db: Database; 12 + 13 + constructor(db: Database) { 14 + this.db = db; 15 + } 16 + 17 + async latestRev(actorDid: string) { 18 + const { rev } = await getLatestRev(actorDid, this.db); 19 + return { rev }; 20 + } 21 + }
+232
data-plane/routes/threads.ts
··· 1 + import { Database } from "../db/index.ts"; 2 + 3 + // Parameter validation 4 + function validateThreadParams(above: number, below: number) { 5 + if (!Number.isInteger(above) || above < 0 || above > 100) { 6 + throw new Error("Invalid above: must be an integer between 0 and 100"); 7 + } 8 + 9 + if (!Number.isInteger(below) || below < 0 || below > 100) { 10 + throw new Error("Invalid below: must be an integer between 0 and 100"); 11 + } 12 + } 13 + 14 + // Helper function to get ancestors (parent posts going up the thread) 15 + async function getAncestors( 16 + db: Database, 17 + postUri: string, 18 + maxDepth: number, 19 + ): Promise<string[]> { 20 + const ancestors: string[] = []; 21 + let currentUri = postUri; 22 + let depth = 0; 23 + 24 + while (depth < maxDepth) { 25 + const post = await db.models.Post.findOne({ uri: currentUri }); 26 + 27 + if (!post || !post.reply?.parent?.uri) { 28 + break; 29 + } 30 + 31 + const parentUri = post.reply.parent.uri; 32 + ancestors.unshift(parentUri); // Add to beginning to maintain order 33 + currentUri = parentUri; 34 + depth++; 35 + } 36 + 37 + return ancestors; 38 + } 39 + 40 + // Helper function to get descendants (child posts going down the thread) 41 + async function getDescendants( 42 + db: Database, 43 + postUri: string, 44 + maxDepth: number, 45 + ): Promise<string[]> { 46 + const descendants: string[] = []; 47 + const visited = new Set<string>(); 48 + 49 + // Use BFS to traverse descendants 50 + const queue: Array<{ uri: string; depth: number }> = [{ 51 + uri: postUri, 52 + depth: 0, 53 + }]; 54 + 55 + while (queue.length > 0) { 56 + const { uri: currentUri, depth } = queue.shift()!; 57 + 58 + if (depth >= maxDepth || visited.has(currentUri)) { 59 + continue; 60 + } 61 + 62 + visited.add(currentUri); 63 + 64 + // Find all replies to this post 65 + const replies = await db.models.Post.find({ 66 + "reply.parent.uri": currentUri, 67 + }) 68 + .sort({ createdAt: -1 }); // Most recent first 69 + 70 + for (const reply of replies) { 71 + if (!visited.has(reply.uri)) { 72 + descendants.push(reply.uri); 73 + 74 + // Add to queue for further traversal if we haven't reached max depth 75 + if (depth + 1 < maxDepth) { 76 + queue.push({ uri: reply.uri, depth: depth + 1 }); 77 + } 78 + } 79 + } 80 + } 81 + 82 + return descendants; 83 + } 84 + 85 + export class Threads { 86 + private db: Database; 87 + 88 + constructor(db: Database) { 89 + this.db = db; 90 + } 91 + 92 + async getThread(postUri: string, above: number = 10, below: number = 50) { 93 + validateThreadParams(above, below); 94 + 95 + try { 96 + // Get ancestors (parents going up) and descendants (replies going down) in parallel 97 + const [ancestors, descendants] = await Promise.all([ 98 + getAncestors(this.db, postUri, above), 99 + getDescendants(this.db, postUri, below), 100 + ]); 101 + 102 + // Verify the original post exists 103 + const originalPost = await this.db.models.Post.findOne({ uri: postUri }); 104 + 105 + if (!originalPost) { 106 + throw new Error("Post not found"); 107 + } 108 + 109 + // Combine all URIs: ancestors + self + descendants 110 + const uris = [ 111 + ...ancestors, 112 + postUri, // The original post 113 + ...descendants, 114 + ]; 115 + 116 + // Remove duplicates while preserving order 117 + const uniqueUris = Array.from(new Set(uris)); 118 + 119 + return { 120 + uris: uniqueUris, 121 + meta: { 122 + ancestorCount: ancestors.length, 123 + descendantCount: descendants.length, 124 + totalCount: uniqueUris.length, 125 + }, 126 + }; 127 + } catch (error) { 128 + console.error("Error fetching thread:", error); 129 + throw new Error("Failed to fetch thread"); 130 + } 131 + } 132 + 133 + async getThreadStructure( 134 + postUri: string, 135 + above: number = 10, 136 + below: number = 50, 137 + ) { 138 + validateThreadParams(above, below); 139 + 140 + try { 141 + // Get the original post 142 + const originalPost = await this.db.models.Post.findOne({ uri: postUri }); 143 + 144 + if (!originalPost) { 145 + throw new Error("Post not found"); 146 + } 147 + 148 + // Get ancestors with metadata 149 + const ancestors: Array<{ uri: string; depth: number }> = []; 150 + let currentUri = postUri; 151 + let depth = 0; 152 + 153 + while (depth < above) { 154 + const post = await this.db.models.Post.findOne({ uri: currentUri }); 155 + 156 + if (!post || !post.reply?.parent?.uri) { 157 + break; 158 + } 159 + 160 + const parentUri = post.reply.parent.uri; 161 + ancestors.unshift({ uri: parentUri, depth: depth + 1 }); 162 + currentUri = parentUri; 163 + depth++; 164 + } 165 + 166 + // Get descendants with metadata using BFS 167 + const descendants: Array< 168 + { uri: string; depth: number; parent: string } 169 + > = []; 170 + const queue: Array<{ uri: string; depth: number; parent: string }> = [ 171 + { uri: postUri, depth: 0, parent: postUri }, 172 + ]; 173 + const visited = new Set<string>([postUri]); 174 + 175 + while (queue.length > 0) { 176 + const { uri: currentUri, depth: currentDepth } = queue.shift()!; 177 + 178 + if (currentDepth >= below) { 179 + continue; 180 + } 181 + 182 + const replies = await this.db.models.Post.find({ 183 + "reply.parent.uri": currentUri, 184 + }) 185 + .sort({ createdAt: -1 }); 186 + 187 + for (const reply of replies) { 188 + if (!visited.has(reply.uri)) { 189 + visited.add(reply.uri); 190 + const childDepth = currentDepth + 1; 191 + 192 + descendants.push({ 193 + uri: reply.uri, 194 + depth: childDepth, 195 + parent: currentUri, 196 + }); 197 + 198 + if (childDepth < below) { 199 + queue.push({ 200 + uri: reply.uri, 201 + depth: childDepth, 202 + parent: reply.uri, 203 + }); 204 + } 205 + } 206 + } 207 + } 208 + 209 + return { 210 + root: { 211 + uri: postUri, 212 + isRoot: !originalPost.reply?.parent?.uri, 213 + }, 214 + ancestors: ancestors.reverse(), // Top ancestor first 215 + descendants, 216 + meta: { 217 + ancestorCount: ancestors.length, 218 + descendantCount: descendants.length, 219 + maxAncestorDepth: ancestors.length > 0 220 + ? Math.max(...ancestors.map((a) => a.depth)) 221 + : 0, 222 + maxDescendantDepth: descendants.length > 0 223 + ? Math.max(...descendants.map((d) => d.depth)) 224 + : 0, 225 + }, 226 + }; 227 + } catch (error) { 228 + console.error("Error fetching thread structure:", error); 229 + throw new Error("Failed to fetch thread structure"); 230 + } 231 + } 232 + }
+2 -2
data-plane/server/background.ts data-plane/background.ts
··· 1 1 import PQueue from "p-queue"; 2 - import { Database } from "./index.ts"; 2 + import { Database } from "./db/index.ts"; 3 3 import { Logger } from "@logtape/logtape"; 4 - import { env } from "../../utils/env.ts"; 4 + import { env } from "../utils/env.ts"; 5 5 6 6 // A simple queue for in-process, out-of-band/backgrounded work 7 7
+4 -5
data-plane/server/index.ts data-plane/db/index.ts
··· 1 1 import mongoose, { Connection } from "mongoose"; 2 - import { IdResolver, MemoryCache } from "@atproto/identity"; 2 + import { IdResolver, MemoryCache } from "@atp/identity"; 3 3 import { env } from "../../utils/env.ts"; 4 - import { DataPlaneClient, GetIdentityByDidResponse } from "../client/index.ts"; 5 4 import * as models from "./models.ts"; 6 - import { getResultFromDoc } from "./util.ts"; 5 + import { getResultFromDoc } from "../util.ts"; 7 6 import { getLogger } from "@logtape/logtape"; 8 7 9 8 const HOUR = 60 * 60 * 1000; 10 9 const DAY = HOUR * 24; 11 10 12 - export class Database implements DataPlaneClient { 11 + export class Database { 13 12 private connection!: Connection; 14 13 public models!: models.DatabaseModels; 15 14 public logger = getLogger(["appview", "database"]); ··· 199 198 // Implement DataPlaneClient interface 200 199 async getIdentityByDid( 201 200 { did }: { did: string }, 202 - ): Promise<GetIdentityByDidResponse> { 201 + ): Promise<{ did: string; handle?: string } | undefined> { 203 202 const doc = await this.idResolver.did.resolve(did); 204 203 if (!doc) { 205 204 throw new Error("DID not found");
+11 -11
data-plane/server/indexing/index.ts data-plane/indexing/index.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 2 import { AtpAgent, ComAtprotoSyncGetLatestCommit } from "@atproto/api"; 3 - import { DAY, HOUR } from "@atproto/common"; 4 - import { getPds, IdResolver } from "@atproto/identity"; 5 - import { ValidationError } from "@atproto/lexicon"; 3 + import { DAY, HOUR } from "@atp/common"; 4 + import { getPds, IdResolver } from "@atp/identity"; 5 + import { RepoRecord, ValidationError } from "@atp/lexicon"; 6 6 import { 7 7 getAndParseRecord, 8 8 readCarWithRoot, 9 9 VerifiedRepo, 10 10 verifyRepo, 11 11 WriteOpAction, 12 - } from "@atproto/repo"; 13 - import { AtUri } from "@atproto/syntax"; 14 - import { retryXrpc } from "../../../utils/retry.ts"; 12 + } from "@atp/repo"; 13 + import { AtUri } from "@atp/syntax"; 14 + import { retryXrpc } from "../../utils/retry.ts"; 15 15 import { BackgroundQueue } from "../background.ts"; 16 - import { Database } from "../index.ts"; 17 - import { ActorDocument } from "../models.ts"; 16 + import { Database } from "../db/index.ts"; 17 + import { ActorDocument } from "../db/models.ts"; 18 18 import * as Block from "./plugins/block.ts"; 19 19 import * as Generator from "./plugins/generator/index.ts"; 20 20 import * as Follow from "./plugins/follow.ts"; ··· 76 76 async indexRecord( 77 77 uri: AtUri, 78 78 cid: CID, 79 - obj: unknown, 79 + obj: RepoRecord, 80 80 action: WriteOpAction.Create | WriteOpAction.Update, 81 81 timestamp: string, 82 82 opts?: { disableNotifs?: boolean; disableLabels?: boolean }, ··· 97 97 } 98 98 99 99 async indexHandle(did: string, timestamp: string, force = false) { 100 - const actor = await this.db.models.Actor.findOne({ did }).lean(); 100 + const actor = await this.db.models.Actor.findOne({ did }); 101 101 if (!force && !needsHandleReindex(actor, timestamp)) { 102 102 return; 103 103 } ··· 187 187 if (op.op === "delete") { 188 188 await this.deleteRecord(uri); 189 189 } else { 190 - const parsed = await getAndParseRecord(blocks, cid); 190 + const parsed = getAndParseRecord(blocks, cid); 191 191 await this.indexRecord( 192 192 uri, 193 193 cid,
+6 -6
data-plane/server/indexing/plugins/audio.ts data-plane/indexing/plugins/audio.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Audio from "../../../../lex/types/so/sprk/sound/audio.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Audio from "../../../lex/types/so/sprk/sound/audio.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { AudioDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { AudioDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 - import { normalizeObject } from "../../../../utils/embed-normalizer.ts"; 9 + import { normalizeObject } from "../../../utils/embed-normalizer.ts"; 10 10 11 11 const lexId = lex.ids.SoSprkSoundAudio; 12 12 type IndexedAudio = AudioDocument;
+5 -5
data-plane/server/indexing/plugins/block.ts data-plane/indexing/plugins/block.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Block from "../../../../lex/types/app/bsky/graph/block.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Block from "../../../lex/types/app/bsky/graph/block.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { BlockDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { BlockDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 9 10 10 const lexId = lex.ids.AppBskyGraphBlock;
+5 -5
data-plane/server/indexing/plugins/follow.ts data-plane/indexing/plugins/follow.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Follow from "../../../../lex/types/app/bsky/graph/follow.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Follow from "../../../lex/types/app/bsky/graph/follow.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { FollowDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { FollowDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 9 10 10 const lexId = lex.ids.AppBskyGraphFollow;
+5 -5
data-plane/server/indexing/plugins/generator/bsky.ts data-plane/indexing/plugins/generator/bsky.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../../lex/lexicons.ts"; 4 - import * as FeedGenerator from "../../../../../lex/types/app/bsky/feed/generator.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../../lex/lexicons.ts"; 4 + import * as FeedGenerator from "../../../../lex/types/app/bsky/feed/generator.ts"; 5 5 import { BackgroundQueue } from "../../../background.ts"; 6 - import { Database } from "../../../index.ts"; 7 - import { BskyGeneratorDocument } from "../../../models.ts"; 6 + import { Database } from "../../../db/index.ts"; 7 + import { BskyGeneratorDocument } from "../../../db/models.ts"; 8 8 import { RecordProcessor } from "../../processor.ts"; 9 9 10 10 const lexId = lex.ids.AppBskyFeedGenerator;
data-plane/server/indexing/plugins/generator/index.ts data-plane/indexing/plugins/generator/index.ts
+5 -5
data-plane/server/indexing/plugins/generator/sprk.ts data-plane/indexing/plugins/generator/sprk.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../../lex/lexicons.ts"; 4 - import * as FeedGenerator from "../../../../../lex/types/so/sprk/feed/generator.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../../lex/lexicons.ts"; 4 + import * as FeedGenerator from "../../../../lex/types/so/sprk/feed/generator.ts"; 5 5 import { BackgroundQueue } from "../../../background.ts"; 6 - import { Database } from "../../../index.ts"; 7 - import { SprkGeneratorDocument } from "../../../models.ts"; 6 + import { Database } from "../../../db/index.ts"; 7 + import { SprkGeneratorDocument } from "../../../db/models.ts"; 8 8 import { RecordProcessor } from "../../processor.ts"; 9 9 10 10 const lexId = lex.ids.SoSprkFeedGenerator;
+5 -5
data-plane/server/indexing/plugins/like.ts data-plane/indexing/plugins/like.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Like from "../../../../lex/types/so/sprk/feed/like.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Like from "../../../lex/types/so/sprk/feed/like.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { LikeDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { LikeDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 9 10 10 const lexId = lex.ids.SoSprkFeedLike;
+6 -6
data-plane/server/indexing/plugins/music.ts data-plane/indexing/plugins/music.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Music from "../../../../lex/types/so/sprk/feed/music.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Music from "../../../lex/types/so/sprk/feed/music.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { MusicDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { MusicDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 - import { normalizeObject } from "../../../../utils/embed-normalizer.ts"; 9 + import { normalizeObject } from "../../../utils/embed-normalizer.ts"; 10 10 11 11 const lexId = lex.ids.SoSprkFeedMusic; 12 12 type IndexedMusic = MusicDocument;
+11 -11
data-plane/server/indexing/plugins/post.ts data-plane/indexing/plugins/post.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import { isMain as isEmbedImage } from "../../../../lex/types/so/sprk/embed/images.ts"; 5 - import { isMain as isEmbedVideo } from "../../../../lex/types/so/sprk/embed/video.ts"; 2 + import { AtUri } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import { isMain as isEmbedImage } from "../../../lex/types/so/sprk/embed/images.ts"; 5 + import { isMain as isEmbedVideo } from "../../../lex/types/so/sprk/embed/video.ts"; 6 6 import { 7 7 Record as PostRecord, 8 8 ReplyRef, 9 - } from "../../../../lex/types/so/sprk/feed/post.ts"; 10 - import { Record as GateRecord } from "../../../../lex/types/so/sprk/feed/threadgate.ts"; 9 + } from "../../../lex/types/so/sprk/feed/post.ts"; 10 + import { Record as GateRecord } from "../../../lex/types/so/sprk/feed/threadgate.ts"; 11 11 import { 12 12 isLink, 13 13 isMention, 14 - } from "../../../../lex/types/so/sprk/richtext/facet.ts"; 14 + } from "../../../lex/types/so/sprk/richtext/facet.ts"; 15 15 import { BackgroundQueue } from "../../background.ts"; 16 - import { Database } from "../../index.ts"; 17 - import { PostDocument } from "../../models.ts"; 16 + import { Database } from "../../db/index.ts"; 17 + import { PostDocument } from "../../db/models.ts"; 18 18 import { 19 19 getAncestorsAndSelf, 20 20 getDescendents, ··· 24 24 import { 25 25 normalizeEmbed, 26 26 normalizeObject, 27 - } from "../../../../utils/embed-normalizer.ts"; 28 - import { jsonToLex } from "@atproto/api"; 27 + } from "../../../utils/embed-normalizer.ts"; 28 + import { jsonToLex } from "@atp/lexicon"; 29 29 30 30 type PostAncestor = { 31 31 uri: string;
+6 -6
data-plane/server/indexing/plugins/profile.ts data-plane/indexing/plugins/profile.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Profile from "../../../../lex/types/so/sprk/actor/profile.ts"; 2 + import { AtUri } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Profile from "../../../lex/types/so/sprk/actor/profile.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { ProfileDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { ProfileDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 - import { normalizeProfile } from "../../../../utils/embed-normalizer.ts"; 9 + import { normalizeProfile } from "../../../utils/embed-normalizer.ts"; 10 10 11 11 const lexId = lex.ids.SoSprkActorProfile; 12 12 type IndexedProfile = ProfileDocument;
+6 -6
data-plane/server/indexing/plugins/repost.ts data-plane/indexing/plugins/repost.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Repost from "../../../../lex/types/app/bsky/feed/repost.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Repost from "../../../lex/types/so/sprk/feed/repost.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { RepostDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { RepostDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 9 10 - const lexId = lex.ids.AppBskyFeedRepost; 10 + const lexId = lex.ids.SoSprkFeedRepost; 11 11 type IndexedRepost = RepostDocument; 12 12 13 13 const insertFn = async (
+6 -6
data-plane/server/indexing/plugins/story.ts data-plane/indexing/plugins/story.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax"; 3 - import * as lex from "../../../../lex/lexicons.ts"; 4 - import * as Story from "../../../../lex/types/so/sprk/feed/story.ts"; 2 + import { AtUri, normalizeDatetimeAlways } from "@atp/syntax"; 3 + import * as lex from "../../../lex/lexicons.ts"; 4 + import * as Story from "../../../lex/types/so/sprk/feed/story.ts"; 5 5 import { BackgroundQueue } from "../../background.ts"; 6 - import { Database } from "../../index.ts"; 7 - import { StoryDocument } from "../../models.ts"; 6 + import { Database } from "../../db/index.ts"; 7 + import { StoryDocument } from "../../db/models.ts"; 8 8 import { RecordProcessor } from "../processor.ts"; 9 9 import { 10 10 normalizeEmbed, 11 11 normalizeObject, 12 - } from "../../../../utils/embed-normalizer.ts"; 12 + } from "../../../utils/embed-normalizer.ts"; 13 13 14 14 const lexId = lex.ids.SoSprkFeedStory; 15 15 type IndexedStory = StoryDocument;
+5 -6
data-plane/server/indexing/processor.ts data-plane/indexing/processor.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 - import { stringifyLex } from "@atproto/lexicon"; 3 - import { AtUri } from "@atproto/syntax"; 4 - import { lexicons } from "../../../lex/lexicons.ts"; 2 + import { jsonStringToLex, stringifyLex } from "@atp/lexicon"; 3 + import { AtUri } from "@atp/syntax"; 4 + import { lexicons } from "../../lex/lexicons.ts"; 5 5 import { BackgroundQueue } from "../background.ts"; 6 - import { Database } from "../index.ts"; 7 - import { jsonToLex } from "@atproto/api"; 6 + import { Database } from "../db/index.ts"; 8 7 9 8 // @NOTE re: insertions and deletions. Due to how record updates are handled, 10 9 // (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn). ··· 260 259 return this.handleNotifs({ deleted }); 261 260 } 262 261 263 - const record = jsonToLex(recordDoc.json); 262 + const record = jsonStringToLex(recordDoc.json); 264 263 if (!this.matchesSchema(record)) { 265 264 return this.handleNotifs({ deleted }); 266 265 }
+4 -4
data-plane/server/models.ts data-plane/db/models.ts
··· 86 86 rkey: string; 87 87 createdAt: string; 88 88 indexedAt: string; 89 - json: JSON; 90 - invalidReplyRoot?: boolean; 89 + json: string; 91 90 takenDown: boolean; 92 91 takedownRef: string; 92 + invalidReplyRoot?: boolean; 93 93 } 94 94 95 95 export const recordSchema = new Schema<RecordDocument>({ ··· 100 100 rkey: { type: String, required: true }, 101 101 createdAt: { type: String, required: true }, 102 102 indexedAt: { type: String, required: true }, 103 - json: { type: JSON, required: true }, 104 - invalidReplyRoot: { type: Boolean, required: false }, 103 + json: { type: String, required: true }, 105 104 takenDown: { type: Boolean, required: false }, 106 105 takedownRef: { type: String, required: false }, 106 + invalidReplyRoot: { type: Boolean, required: false }, 107 107 }); 108 108 109 109 export interface DuplicateRecordDocument extends Document {
+7 -15
data-plane/server/subscription.ts data-plane/subscription.ts
··· 1 - import { IdResolver } from "@atproto/identity"; 2 - import { WriteOpAction } from "@atproto/repo"; 3 - import { Event as FirehoseEvent, Firehose } from "@atproto/sync"; 4 - import { MemoryRunner } from "../../utils/memory-runner.ts"; 1 + import { IdResolver } from "@atp/identity"; 2 + import { WriteOpAction } from "@atp/repo"; 3 + import { Event as FirehoseEvent, Firehose, MemoryRunner } from "@atp/sync"; 5 4 import { BackgroundQueue } from "./background.ts"; 6 - import { Database } from "./index.ts"; 5 + import { Database } from "./db/index.ts"; 7 6 import { IndexingService } from "./indexing/index.ts"; 8 7 import { getLogger, Logger } from "@logtape/logtape"; 9 - import { env } from "../../utils/env.ts"; 8 + import { env } from "../utils/env.ts"; 10 9 11 10 export class RepoSubscription { 12 11 firehose: Firehose; ··· 164 163 const runner = new MemoryRunner({ 165 164 startCursor, 166 165 concurrency: env.RUNNER_CONCURRENCY, 167 - cursorSaveIntervalMs: 30000, // Save cursor every 30 seconds 166 + setCursorInterval: 30000, // Save cursor every 30 seconds 168 167 setCursor: async (cursor: number) => { 169 168 await db.saveCursorState(cursor); 170 169 logger.info("Cursor saved to database", { cursor }); ··· 214 213 } 215 214 }, 216 215 filterCollections: [ 217 - "so.sprk.feed.post", 218 - "so.sprk.feed.like", 219 - "so.sprk.feed.music", 220 - "so.sprk.feed.audio", 221 - "so.sprk.actor.profile", 222 - "so.sprk.feed.story", 223 - "so.sprk.graph.follow", 216 + "so.sprk.*", 224 217 "app.bsky.graph.follow", 225 218 "app.bsky.graph.block", 226 219 "app.bsky.feed.generator", 227 - "app.bsky.feed.repost", 228 220 ], 229 221 }); 230 222 return { firehose, runner };
+6 -8
data-plane/server/util.ts data-plane/util.ts
··· 1 1 import { 2 2 Record as PostRecord, 3 3 ReplyRef, 4 - } from "../../lex/types/so/sprk/feed/post.ts"; 5 - import { Database } from "./index.ts"; 6 - import { DidDocument } from "@atproto/identity"; 7 - import { Buffer } from "node:buffer"; 8 - import { Timestamp } from "npm:@bufbuild/protobuf@1.5.0"; 4 + } from "../lex/types/so/sprk/feed/post.ts"; 5 + import { Database } from "./db/index.ts"; 6 + import { DidDocument } from "@atp/identity"; 9 7 10 8 export const getDescendents = async ( 11 9 db: Database, ··· 161 159 return { 162 160 did: getDid(doc), 163 161 handle: getHandle(doc), 164 - keys: Buffer.from(JSON.stringify(keys)), 165 - services: Buffer.from(JSON.stringify(services)), 166 - updated: Timestamp.fromDate(new Date()), 162 + keys: new TextEncoder().encode(JSON.stringify(keys)), 163 + services: new TextEncoder().encode(JSON.stringify(services)), 164 + updated: new Date(), 167 165 }; 168 166 };
+24 -22
deno.json
··· 1 1 { 2 2 "tasks": { 3 3 "dev": "deno run -A --watch main.ts", 4 - "codegen": "deno run -A jsr:@sprk/lex-cli@^0.2.1 gen-server --yes ./lex ./lexicons", 4 + "codegen": "deno run -A jsr:@atp/lex-cli@^0.1.0-alpha.1 gen-server -o ./lex -i ./lexicons", 5 5 "start": "deno run -A --env-file main.ts", 6 6 "docker-dev": "docker compose -f compose.dev.yaml up --build --watch" 7 7 }, 8 8 "imports": { 9 - "@atproto/repo": "npm:@atproto/repo@^0.8.5", 10 - "@atproto/sync": "npm:@atproto/sync@^0.1.30", 11 - "@atproto/xrpc": "npm:@atproto/xrpc@^0.7.1", 12 - "@logtape/logtape": "jsr:@logtape/logtape@^1.0.4", 13 - "@logtape/pretty": "jsr:@logtape/pretty@^1.1.0-dev.333+b25e634b", 14 - "dotenv": "npm:dotenv@^17.2.1", 15 - "hono": "jsr:@hono/hono@^4.9.1", 9 + "@atp/common": "jsr:@atp/common@^0.1.0-alpha.4", 10 + "@atp/crypto": "jsr:@atp/crypto@^0.1.0-alpha.2", 11 + "@atp/identity": "jsr:@atp/identity@^0.1.0-alpha.1", 12 + "@atp/lexicon": "jsr:@atp/lexicon@^0.1.0-alpha.2", 13 + "@atp/repo": "jsr:@atp/repo@^0.1.0-alpha.2", 14 + "@atp/sync": "jsr:@atp/sync@^0.1.0-alpha.3", 15 + "@atp/syntax": "jsr:@atp/syntax@^0.1.0-alpha.1", 16 + "@atp/xrpc": "jsr:@atp/xrpc@^0.1.0-alpha.2", 17 + "@atp/xrpc-server": "jsr:@atp/xrpc-server@^0.1.0-alpha.2", 18 + "@atproto/identity": "npm:@atproto/identity@^0.4.9", 19 + "@atproto/repo": "npm:@atproto/repo@^0.8.10", 20 + "@atproto/sync": "npm:@atproto/sync@^0.1.35", 21 + "@logtape/logtape": "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9", 22 + "@logtape/pretty": "jsr:@logtape/pretty@^1.2.0-dev.344+834f24a9", 23 + "@std/assert": "jsr:@std/assert@^1.0.14", 24 + "dotenv": "npm:dotenv@^17.2.3", 25 + "hono": "jsr:@hono/hono@^4.9.9", 16 26 "@std/encoding": "jsr:@std/encoding@^1.0.10", 17 - "@atproto/api": "npm:@atproto/api@^0.14.22", 18 - "@atproto/common": "npm:@atproto/common@^0.4.11", 19 - "@atproto/crypto": "npm:@atproto/crypto@^0.4.4", 20 - "@atproto/identity": "npm:@atproto/identity@^0.4.8", 21 - "@atproto/lexicon": "npm:@atproto/lexicon@^0.4.12", 22 - "@atproto/syntax": "npm:@atproto/syntax@^0.3.4", 23 - "@sprk/xrpc-server": "jsr:@sprk/xrpc-server@^0.2.0", 24 - "jose": "npm:jose@^6.0.12", 25 - "mongoose": "npm:mongoose@^8.17.1", 26 - "multiformats": "npm:multiformats@^9.9.0", 27 - "p-queue": "npm:p-queue@^8.1.0" 27 + "@atproto/api": "npm:@atproto/api@^0.16.11", 28 + "jose": "npm:jose@^6.1.0", 29 + "mongoose": "npm:mongoose@^8.19.0", 30 + "multiformats": "npm:multiformats@^13.4.1", 31 + "p-queue": "npm:p-queue@^8.1.1" 28 32 }, 29 33 "compilerOptions": { 30 34 "jsx": "precompile", 31 35 "jsxImportSource": "hono/jsx" 32 36 }, 33 - "unstable": [ 34 - "sloppy-imports" 35 - ] 37 + "unstable": ["sloppy-imports"] 36 38 }
+273 -166
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 - "jsr:@hono/hono@^4.7.10": "4.9.1", 5 - "jsr:@hono/hono@^4.9.1": "4.9.1", 6 - "jsr:@logtape/logtape@^1.0.4": "1.0.4", 7 - "jsr:@logtape/logtape@^1.1.0-dev.333+b25e634b": "1.1.0-dev.333+b25e634b", 8 - "jsr:@logtape/pretty@^1.1.0-dev.333+b25e634b": "1.1.0-dev.333+b25e634b", 9 - "jsr:@sprk/xrpc-server@0.2": "0.2.0", 10 - "jsr:@std/assert@*": "1.0.14", 4 + "jsr:@atp/bytes@~0.1.0-alpha.1": "0.1.0-alpha.1", 5 + "jsr:@atp/common@~0.1.0-alpha.3": "0.1.0-alpha.4", 6 + "jsr:@atp/common@~0.1.0-alpha.4": "0.1.0-alpha.4", 7 + "jsr:@atp/crypto@~0.1.0-alpha.1": "0.1.0-alpha.2", 8 + "jsr:@atp/crypto@~0.1.0-alpha.2": "0.1.0-alpha.2", 9 + "jsr:@atp/identity@~0.1.0-alpha.1": "0.1.0-alpha.1", 10 + "jsr:@atp/lexicon@~0.1.0-alpha.1": "0.1.0-alpha.2", 11 + "jsr:@atp/lexicon@~0.1.0-alpha.2": "0.1.0-alpha.2", 12 + "jsr:@atp/repo@~0.1.0-alpha.2": "0.1.0-alpha.2", 13 + "jsr:@atp/sync@~0.1.0-alpha.3": "0.1.0-alpha.3", 14 + "jsr:@atp/syntax@~0.1.0-alpha.1": "0.1.0-alpha.1", 15 + "jsr:@atp/xrpc-server@~0.1.0-alpha.2": "0.1.0-alpha.2", 16 + "jsr:@atp/xrpc@~0.1.0-alpha.2": "0.1.0-alpha.2", 17 + "jsr:@hono/hono@^4.9.8": "4.9.9", 18 + "jsr:@hono/hono@^4.9.9": "4.9.9", 19 + "jsr:@logtape/file@^1.2.0-dev.344+834f24a9": "1.2.0-dev.344+834f24a9", 20 + "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9": "1.2.0-dev.344+834f24a9", 21 + "jsr:@logtape/pretty@^1.2.0-dev.344+834f24a9": "1.2.0-dev.344+834f24a9", 22 + "jsr:@noble/curves@^2.0.1": "2.0.1", 23 + "jsr:@noble/hashes@2": "2.0.1", 24 + "jsr:@noble/hashes@^2.0.1": "2.0.1", 11 25 "jsr:@std/assert@^1.0.14": "1.0.14", 12 - "jsr:@std/encoding@*": "1.0.10", 26 + "jsr:@std/bytes@^1.0.5": "1.0.6", 27 + "jsr:@std/cbor@~0.1.8": "0.1.8", 28 + "jsr:@std/crypto@^1.0.5": "1.0.5", 13 29 "jsr:@std/encoding@^1.0.10": "1.0.10", 30 + "jsr:@std/fs@^1.0.19": "1.0.19", 14 31 "jsr:@std/internal@^1.0.10": "1.0.10", 15 - "jsr:@std/text@*": "1.0.16", 16 - "jsr:@zod/zod@^4.0.17": "4.0.17", 17 - "npm:@atproto/api@~0.14.22": "0.14.22", 18 - "npm:@atproto/common@~0.4.11": "0.4.11", 19 - "npm:@atproto/crypto@~0.4.4": "0.4.4", 20 - "npm:@atproto/identity@~0.4.8": "0.4.8", 21 - "npm:@atproto/lexicon@~0.4.11": "0.4.12", 22 - "npm:@atproto/lexicon@~0.4.12": "0.4.12", 23 - "npm:@atproto/repo@~0.8.5": "0.8.5", 24 - "npm:@atproto/sync@~0.1.30": "0.1.30", 25 - "npm:@atproto/syntax@~0.3.4": "0.3.4", 26 - "npm:@atproto/xrpc@0.7": "0.7.1", 27 - "npm:@atproto/xrpc@~0.7.1": "0.7.1", 32 + "jsr:@std/streams@^1.0.9": "1.0.12", 33 + "jsr:@zod/zod@^4.1.11": "4.1.11", 34 + "npm:@atproto/api@~0.16.11": "0.16.11", 35 + "npm:@atproto/identity@~0.4.9": "0.4.9", 36 + "npm:@atproto/repo@~0.8.10": "0.8.10", 37 + "npm:@atproto/sync@~0.1.35": "0.1.35", 28 38 "npm:@bufbuild/protobuf@1.5.0": "1.5.0", 29 - "npm:@types/node@*": "24.2.0", 39 + "npm:@ipld/dag-cbor@^9.2.5": "9.2.5", 30 40 "npm:@types/node@24.0.7": "24.0.7", 31 - "npm:dotenv@*": "17.2.1", 32 - "npm:dotenv@^17.2.1": "17.2.1", 33 - "npm:http-errors@2": "2.0.0", 34 - "npm:jose@*": "6.0.12", 35 - "npm:jose@^6.0.12": "6.0.12", 41 + "npm:dotenv@^17.2.3": "17.2.3", 42 + "npm:jose@^6.1.0": "6.1.0", 36 43 "npm:lodash@*": "4.17.21", 37 - "npm:mongoose@^8.17.1": "8.17.1", 38 - "npm:multiformats@^9.9.0": "9.9.0", 39 - "npm:p-queue@^8.1.0": "8.1.0", 40 - "npm:rate-limiter-flexible@^2.4.1": "2.4.2", 41 - "npm:uint8arrays@*": "5.1.0", 42 - "npm:uint8arrays@3.0.0": "3.0.0", 43 - "npm:ws@^8.12.0": "8.18.3" 44 + "npm:mongoose@^8.19.0": "8.19.0", 45 + "npm:multiformats@^13.4.1": "13.4.1", 46 + "npm:p-queue@^8.1.1": "8.1.1", 47 + "npm:rate-limiter-flexible@^2.4.2": "2.4.2", 48 + "npm:zod@^4.1.11": "4.1.11" 44 49 }, 45 50 "jsr": { 46 - "@hono/hono@4.9.1": { 47 - "integrity": "d8ad3b296b65eab1a6b34f8701bcbd425246aa3f0e3024029ccb89cbb727fabd" 51 + "@atp/bytes@0.1.0-alpha.1": { 52 + "integrity": "51ab3c11252f29265b9ac382b6b2f7023643fe74682114300f4efceee87cfd5c", 53 + "dependencies": [ 54 + "npm:multiformats" 55 + ] 48 56 }, 49 - "@logtape/logtape@1.0.4": { 50 - "integrity": "6ada87764d995b1033c352a17fd9e20b217f3672083bc2d8debe356eac03fe10" 57 + "@atp/common@0.1.0-alpha.4": { 58 + "integrity": "b935ad78c94f1829a348139eb9fd2eabb6ddfed3dfd5a62a2af904712c1c1e52", 59 + "dependencies": [ 60 + "jsr:@atp/bytes", 61 + "jsr:@logtape/file", 62 + "jsr:@logtape/logtape", 63 + "jsr:@std/cbor", 64 + "jsr:@std/crypto", 65 + "jsr:@std/encoding", 66 + "jsr:@std/fs", 67 + "jsr:@zod/zod", 68 + "npm:@ipld/dag-cbor", 69 + "npm:multiformats" 70 + ] 51 71 }, 52 - "@logtape/logtape@1.1.0-dev.333+b25e634b": { 53 - "integrity": "482814e7de5a3470cd614182681bc3e64435b56f424de34badee259503a40273" 72 + "@atp/crypto@0.1.0-alpha.2": { 73 + "integrity": "594b0211ab82cc530bcd6f4a494ce192b0d5be60c01304d7096e030ef2baae11", 74 + "dependencies": [ 75 + "jsr:@atp/bytes", 76 + "jsr:@noble/curves", 77 + "jsr:@noble/hashes@^2.0.1" 78 + ] 54 79 }, 55 - "@logtape/pretty@1.1.0-dev.333+b25e634b": { 56 - "integrity": "f9579c0ca9658c6424758024c26a6bd841b60c57ca0fe69dd6fda0289b6f3afe", 80 + "@atp/identity@0.1.0-alpha.1": { 81 + "integrity": "a548f4715abeca8ed6f6f359cdaf277bf1a41cd455170d8e13a629bcf6351016", 57 82 "dependencies": [ 58 - "jsr:@logtape/logtape@^1.1.0-dev.333+b25e634b", 59 - "npm:@types/node@24.0.7" 83 + "jsr:@atp/common@~0.1.0-alpha.4", 84 + "jsr:@atp/crypto@~0.1.0-alpha.1" 85 + ] 86 + }, 87 + "@atp/lexicon@0.1.0-alpha.2": { 88 + "integrity": "2c66b1a958656f4e272c2082a1b2b4edb4b5d62104684c667af5485551ec17b1", 89 + "dependencies": [ 90 + "jsr:@atp/common@~0.1.0-alpha.3", 91 + "jsr:@atp/syntax", 92 + "npm:multiformats", 93 + "npm:zod" 94 + ] 95 + }, 96 + "@atp/repo@0.1.0-alpha.2": { 97 + "integrity": "6da50453bbd527a679237d15bc9569eb2195503189f9be9d3023060f3f89f44a", 98 + "dependencies": [ 99 + "jsr:@atp/bytes", 100 + "jsr:@atp/common@~0.1.0-alpha.4", 101 + "jsr:@atp/crypto@~0.1.0-alpha.2", 102 + "jsr:@atp/lexicon@~0.1.0-alpha.2", 103 + "jsr:@std/encoding", 104 + "npm:@ipld/dag-cbor", 105 + "npm:multiformats", 106 + "npm:zod" 107 + ] 108 + }, 109 + "@atp/sync@0.1.0-alpha.3": { 110 + "integrity": "c5d8dbed0d7e2a15013428e5eeb16c2624017756b82eb2c809a9e59623334e13", 111 + "dependencies": [ 112 + "jsr:@atp/common@~0.1.0-alpha.4", 113 + "jsr:@atp/identity", 114 + "jsr:@atp/lexicon@~0.1.0-alpha.2", 115 + "jsr:@atp/repo", 116 + "jsr:@atp/syntax", 117 + "jsr:@atp/xrpc-server", 118 + "npm:multiformats", 119 + "npm:p-queue" 120 + ] 121 + }, 122 + "@atp/syntax@0.1.0-alpha.1": { 123 + "integrity": "9e2055cace77cf63a8c52a4a94c39492215e7135101db7bc2289ebad9bec1991" 124 + }, 125 + "@atp/xrpc@0.1.0-alpha.2": { 126 + "integrity": "53a548b554430671eeef683ce48830599c12e48bb2f73ef9fa49d1cfe3aba1fb", 127 + "dependencies": [ 128 + "jsr:@atp/lexicon@~0.1.0-alpha.1", 129 + "jsr:@zod/zod" 60 130 ] 61 131 }, 62 - "@sprk/xrpc-server@0.2.0": { 63 - "integrity": "40bf683520379349caa968fa55bdcd62801097224ef4692e465281227c31177b", 132 + "@atp/xrpc-server@0.1.0-alpha.2": { 133 + "integrity": "fbf472cb725459ab844529c989ab61967a088956ceba03aba89f3784ee20e1e9", 64 134 "dependencies": [ 65 - "jsr:@hono/hono@^4.7.10", 66 - "jsr:@sprk/xrpc-server", 67 - "jsr:@std/assert@^1.0.14", 135 + "jsr:@atp/bytes", 136 + "jsr:@atp/common@~0.1.0-alpha.4", 137 + "jsr:@atp/crypto@~0.1.0-alpha.2", 138 + "jsr:@atp/lexicon@~0.1.0-alpha.2", 139 + "jsr:@atp/xrpc", 140 + "jsr:@hono/hono@^4.9.8", 141 + "jsr:@std/assert", 68 142 "jsr:@zod/zod", 69 - "npm:@atproto/common", 70 - "npm:@atproto/crypto", 71 - "npm:@atproto/lexicon@~0.4.11", 72 - "npm:@atproto/xrpc@0.7", 73 - "npm:http-errors", 74 - "npm:rate-limiter-flexible", 75 - "npm:uint8arrays@3.0.0", 76 - "npm:ws" 143 + "npm:rate-limiter-flexible" 144 + ] 145 + }, 146 + "@hono/hono@4.9.8": { 147 + "integrity": "908150f13e90181a051a3af3bf15203aff00190682afedfd38824d0cb9299a95" 148 + }, 149 + "@hono/hono@4.9.9": { 150 + "integrity": "1d716e97b71e91b852c70beb85c9d3b236393282c59d5e268b07cfd224a77318" 151 + }, 152 + "@logtape/file@1.2.0-dev.344+834f24a9": { 153 + "integrity": "4d674c368f8130dc1403c5c93a316726a65d6b17e36b094780f1b2ed301f5e1b", 154 + "dependencies": [ 155 + "jsr:@logtape/logtape" 156 + ] 157 + }, 158 + "@logtape/logtape@1.2.0-dev.344+834f24a9": { 159 + "integrity": "204222be0f94cd1b64a500e2dcdea22a1618a086fc531b054351e3cfa079c435" 160 + }, 161 + "@logtape/pretty@1.2.0-dev.344+834f24a9": { 162 + "integrity": "19aa16ee6409d7112b7cbcaadf10f0e379df03583426eda7efe2740e30a1c065", 163 + "dependencies": [ 164 + "jsr:@logtape/logtape", 165 + "npm:@types/node" 166 + ] 167 + }, 168 + "@noble/curves@2.0.1": { 169 + "integrity": "21ef41d207a203f60ba37a4fdcbc4f4a545b10c5dab7f293889f18292f81ab23", 170 + "dependencies": [ 171 + "jsr:@noble/hashes@2" 77 172 ] 173 + }, 174 + "@noble/hashes@2.0.1": { 175 + "integrity": "e0e908292a0bf91099cf8ba0720a1647cef82ab38b588815b5e9535b4ff4d7bb" 78 176 }, 79 177 "@std/assert@1.0.14": { 80 178 "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", ··· 82 180 "jsr:@std/internal" 83 181 ] 84 182 }, 183 + "@std/bytes@1.0.6": { 184 + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" 185 + }, 186 + "@std/cbor@0.1.8": { 187 + "integrity": "a0d1c520f8963358cc96defd8cbd1f9e81e40adc2bbfb301f122150f2024d93e", 188 + "dependencies": [ 189 + "jsr:@std/bytes", 190 + "jsr:@std/streams" 191 + ] 192 + }, 193 + "@std/crypto@1.0.5": { 194 + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" 195 + }, 85 196 "@std/encoding@1.0.10": { 86 197 "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 87 198 }, 199 + "@std/fs@1.0.19": { 200 + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06" 201 + }, 88 202 "@std/internal@1.0.10": { 89 203 "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 90 204 }, 91 - "@std/text@1.0.16": { 92 - "integrity": "ddb9853b75119a2473857d691cf1ec02ad90793a2e8b4a4ac49d7354281a0cf8" 205 + "@std/streams@1.0.12": { 206 + "integrity": "ae925fa1dc459b1abf5cbaa28cc5c7b0485853af3b2a384b0dc22d86e59dfbf4" 93 207 }, 94 - "@zod/zod@4.0.17": { 95 - "integrity": "4d9be90a1a3c16e09dad7ce25986379d7ab8ed5f5f843288509af6bf8def525f" 208 + "@zod/zod@4.1.11": { 209 + "integrity": "0d48947455491addca672d8ef766d86bc7bc3add07e78d049b8ffd643bb33a7a" 96 210 } 97 211 }, 98 212 "npm": { 99 - "@atproto/api@0.14.22": { 100 - "integrity": "sha512-ziXPau+sUdFovObSnsoN7JbOmUw1C5e5L28/yXf3P8vbEnSS3HVVGD1jYcscBYY34xQqi4bVDpwMYx/4yRsTuQ==", 213 + "@atproto/api@0.16.11": { 214 + "integrity": "sha512-1dhfQNHiclb102RW+Ea8Nft5olfqU0Ev/vlQaSX6mWNo1aP5zT+sPODJ8+BTUOYk3vcuvL7QMkqA/rLYy2PMyw==", 101 215 "dependencies": [ 102 216 "@atproto/common-web", 103 217 "@atproto/lexicon", 104 - "@atproto/syntax@0.4.0", 105 - "@atproto/xrpc@0.6.12", 218 + "@atproto/syntax", 219 + "@atproto/xrpc", 106 220 "await-lock", 107 221 "multiformats@9.9.0", 108 222 "tlds", 109 - "zod" 223 + "zod@3.25.76" 110 224 ] 111 225 }, 112 - "@atproto/common-web@0.4.2": { 113 - "integrity": "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==", 226 + "@atproto/common-web@0.4.3": { 227 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 114 228 "dependencies": [ 115 229 "graphemer", 116 230 "multiformats@9.9.0", 117 - "uint8arrays@3.0.0", 118 - "zod" 231 + "uint8arrays", 232 + "zod@3.25.76" 119 233 ] 120 234 }, 121 - "@atproto/common@0.4.11": { 122 - "integrity": "sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==", 235 + "@atproto/common@0.4.12": { 236 + "integrity": "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ==", 123 237 "dependencies": [ 124 238 "@atproto/common-web", 125 - "@ipld/dag-cbor", 239 + "@ipld/dag-cbor@7.0.3", 126 240 "cbor-x", 127 241 "iso-datestring-validator", 128 242 "multiformats@9.9.0", ··· 134 248 "dependencies": [ 135 249 "@noble/curves", 136 250 "@noble/hashes", 137 - "uint8arrays@3.0.0" 251 + "uint8arrays" 138 252 ] 139 253 }, 140 - "@atproto/identity@0.4.8": { 141 - "integrity": "sha512-Z0sLnJ87SeNdAifT+rqpgE1Rc3layMMW25gfWNo4u40RGuRODbdfAZlTwBSU2r+Vk45hU+iE+xeQspfednCEnA==", 254 + "@atproto/identity@0.4.9": { 255 + "integrity": "sha512-pRYCaeaEJMZ4vQlRQYYTrF3cMiRp21n/k/pUT1o7dgKby56zuLErDmFXkbKfKWPf7SgWRgamSaNmsGLqAOD7lQ==", 142 256 "dependencies": [ 143 257 "@atproto/common-web", 144 258 "@atproto/crypto" 145 259 ] 146 260 }, 147 - "@atproto/lexicon@0.4.12": { 148 - "integrity": "sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==", 261 + "@atproto/lexicon@0.5.1": { 262 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 149 263 "dependencies": [ 150 264 "@atproto/common-web", 151 - "@atproto/syntax@0.4.0", 265 + "@atproto/syntax", 152 266 "iso-datestring-validator", 153 267 "multiformats@9.9.0", 154 - "zod" 268 + "zod@3.25.76" 155 269 ] 156 270 }, 157 - "@atproto/repo@0.8.5": { 158 - "integrity": "sha512-QZ4UWBWDyPMXgPhktmaRYRyCXIw7lIEAyGtaFy7UmCPpJ5TtFKw3GhGrEiNz/fY3/6lrkdDj44/Tzkud/eP/VQ==", 271 + "@atproto/repo@0.8.10": { 272 + "integrity": "sha512-REs6TZGyxNaYsjqLf447u+gSdyzhvMkVbxMBiKt1ouEVRkiho1CY32+omn62UkpCuGK2y6SCf6x3sVMctgmX4g==", 159 273 "dependencies": [ 160 274 "@atproto/common", 161 275 "@atproto/common-web", 162 276 "@atproto/crypto", 163 277 "@atproto/lexicon", 164 - "@ipld/dag-cbor", 278 + "@ipld/dag-cbor@7.0.3", 165 279 "multiformats@9.9.0", 166 - "uint8arrays@3.0.0", 280 + "uint8arrays", 167 281 "varint", 168 - "zod" 282 + "zod@3.25.76" 169 283 ] 170 284 }, 171 - "@atproto/sync@0.1.30": { 172 - "integrity": "sha512-IbMT/4dklCKy0pVMlrJff4CTdaX/sWwcUrMIxv/kurCzpSQXaC0JtiA0DRfZCIc9n7FMSX+/96vfUNgZttEbOA==", 285 + "@atproto/sync@0.1.35": { 286 + "integrity": "sha512-MPvmTjJYCilZEQF1ds7itzF9tNEZtw4Ez0HeMO5E5GaPtTAccBU3AsTxwWST87EX5qsVxMlBTq2go6G6+Swd7Q==", 173 287 "dependencies": [ 174 288 "@atproto/common", 175 289 "@atproto/identity", 176 290 "@atproto/lexicon", 177 291 "@atproto/repo", 178 - "@atproto/syntax@0.4.0", 292 + "@atproto/syntax", 179 293 "@atproto/xrpc-server", 180 294 "multiformats@9.9.0", 181 295 "p-queue@6.6.2", 182 296 "ws" 183 297 ] 184 298 }, 185 - "@atproto/syntax@0.3.4": { 186 - "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==" 187 - }, 188 - "@atproto/syntax@0.4.0": { 189 - "integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==" 299 + "@atproto/syntax@0.4.1": { 300 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==" 190 301 }, 191 - "@atproto/xrpc-server@0.9.1": { 192 - "integrity": "sha512-AJfxsKrZgKL/5362Rc0oUEjlgpDCmY/soeyLHHjid8J6clbErAdJVCuFwW4T40aHGFY1J13a29ucwbSfOROx6w==", 302 + "@atproto/xrpc-server@0.9.5": { 303 + "integrity": "sha512-V0srjUgy6mQ5yf9+MSNBLs457m4qclEaWZsnqIE7RfYywvntexTAbMoo7J7ONfTNwdmA9Gw4oLak2z2cDAET4w==", 193 304 "dependencies": [ 194 305 "@atproto/common", 195 306 "@atproto/crypto", 196 307 "@atproto/lexicon", 197 - "@atproto/xrpc@0.7.1", 308 + "@atproto/xrpc", 198 309 "cbor-x", 199 310 "express", 200 311 "http-errors", 201 312 "mime-types", 202 313 "rate-limiter-flexible", 203 - "uint8arrays@3.0.0", 314 + "uint8arrays", 204 315 "ws", 205 - "zod" 316 + "zod@3.25.76" 206 317 ] 207 318 }, 208 - "@atproto/xrpc@0.6.12": { 209 - "integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==", 319 + "@atproto/xrpc@0.7.5": { 320 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 210 321 "dependencies": [ 211 322 "@atproto/lexicon", 212 - "zod" 213 - ] 214 - }, 215 - "@atproto/xrpc@0.7.1": { 216 - "integrity": "sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==", 217 - "dependencies": [ 218 - "@atproto/lexicon", 219 - "zod" 323 + "zod@3.25.76" 220 324 ] 221 325 }, 222 326 "@bufbuild/protobuf@1.5.0": { ··· 255 359 "@ipld/dag-cbor@7.0.3": { 256 360 "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", 257 361 "dependencies": [ 258 - "cborg", 362 + "cborg@1.10.2", 259 363 "multiformats@9.9.0" 260 364 ] 261 365 }, 262 - "@mongodb-js/saslprep@1.3.0": { 263 - "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", 366 + "@ipld/dag-cbor@9.2.5": { 367 + "integrity": "sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==", 368 + "dependencies": [ 369 + "cborg@4.2.15", 370 + "multiformats@13.4.1" 371 + ] 372 + }, 373 + "@mongodb-js/saslprep@1.3.1": { 374 + "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", 264 375 "dependencies": [ 265 376 "sparse-bitfield" 266 377 ] 267 378 }, 268 - "@noble/curves@1.9.6": { 269 - "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", 379 + "@noble/curves@1.9.7": { 380 + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 270 381 "dependencies": [ 271 382 "@noble/hashes" 272 383 ] ··· 277 388 "@types/node@24.0.7": { 278 389 "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", 279 390 "dependencies": [ 280 - "undici-types@7.8.0" 281 - ] 282 - }, 283 - "@types/node@24.2.0": { 284 - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", 285 - "dependencies": [ 286 - "undici-types@7.10.0" 391 + "undici-types" 287 392 ] 288 393 }, 289 394 "@types/webidl-conversions@7.0.3": { ··· 390 495 "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", 391 496 "bin": true 392 497 }, 498 + "cborg@4.2.15": { 499 + "integrity": "sha512-T+YVPemWyXcBVQdp0k61lQp2hJniRNmul0lAwTj2DTS/6dI4eCq/MRMucGqqvFqMBfmnD8tJ9aFtPu5dEGAbgw==", 500 + "bin": true 501 + }, 393 502 "content-disposition@0.5.4": { 394 503 "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 395 504 "dependencies": [ ··· 411 520 "ms@2.0.0" 412 521 ] 413 522 }, 414 - "debug@4.4.1": { 415 - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 523 + "debug@4.4.3": { 524 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 416 525 "dependencies": [ 417 526 "ms@2.1.3" 418 527 ] ··· 423 532 "destroy@1.2.0": { 424 533 "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 425 534 }, 426 - "detect-libc@2.0.4": { 427 - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==" 535 + "detect-libc@2.1.1": { 536 + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==" 428 537 }, 429 - "dotenv@17.2.1": { 430 - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==" 538 + "dotenv@17.2.3": { 539 + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==" 431 540 }, 432 541 "dunder-proto@1.0.1": { 433 542 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", ··· 601 710 "iso-datestring-validator@2.2.2": { 602 711 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 603 712 }, 604 - "jose@6.0.12": { 605 - "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==" 713 + "jose@6.1.0": { 714 + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==" 606 715 }, 607 716 "kareem@2.6.3": { 608 717 "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==" ··· 645 754 "whatwg-url" 646 755 ] 647 756 }, 648 - "mongodb@6.18.0": { 649 - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", 757 + "mongodb@6.20.0": { 758 + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", 650 759 "dependencies": [ 651 760 "@mongodb-js/saslprep", 652 761 "bson", 653 762 "mongodb-connection-string-url" 654 763 ] 655 764 }, 656 - "mongoose@8.17.1": { 657 - "integrity": "sha512-aodS4cacux5caoxB5ErEwRmrafIUsVRJxHnvP7URnSUnTenr32j1qBVV+KjYxryyLSisQkxglAFF69TNLeZTLg==", 765 + "mongoose@8.19.0": { 766 + "integrity": "sha512-Z4iRiBkC7aR7a/rxQxtUAUBasFdiXkBuv3EY4NwkRbs92xKA4pwzi1Q4D+odFBe+ChahMNAYg2JP+7tWzZM0sQ==", 658 767 "dependencies": [ 659 768 "bson", 660 769 "kareem", ··· 671 780 "mquery@5.0.0": { 672 781 "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", 673 782 "dependencies": [ 674 - "debug@4.4.1" 783 + "debug@4.4.3" 675 784 ] 676 785 }, 677 786 "ms@2.0.0": { ··· 680 789 "ms@2.1.3": { 681 790 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 682 791 }, 683 - "multiformats@13.4.0": { 684 - "integrity": "sha512-Mkb/QcclrJxKC+vrcIFl297h52QcKh2Az/9A5vbWytbQt4225UWWWmIuSsKksdww9NkIeYcA7DkfftyLuC/JSg==" 792 + "multiformats@13.4.1": { 793 + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==" 685 794 }, 686 795 "multiformats@9.9.0": { 687 796 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" ··· 718 827 "p-timeout@3.2.0" 719 828 ] 720 829 }, 721 - "p-queue@8.1.0": { 722 - "integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==", 830 + "p-queue@8.1.1": { 831 + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", 723 832 "dependencies": [ 724 833 "eventemitter3@5.0.1", 725 834 "p-timeout@6.1.4" ··· 928 1037 "real-require" 929 1038 ] 930 1039 }, 931 - "tlds@1.259.0": { 932 - "integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==", 1040 + "tlds@1.260.0": { 1041 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 933 1042 "bin": true 934 1043 }, 935 1044 "toidentifier@1.0.1": { ··· 954 1063 "multiformats@9.9.0" 955 1064 ] 956 1065 }, 957 - "uint8arrays@5.1.0": { 958 - "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", 959 - "dependencies": [ 960 - "multiformats@13.4.0" 961 - ] 962 - }, 963 - "undici-types@7.10.0": { 964 - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" 965 - }, 966 1066 "undici-types@7.8.0": { 967 1067 "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" 968 1068 }, ··· 993 1093 }, 994 1094 "zod@3.25.76": { 995 1095 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 1096 + }, 1097 + "zod@4.1.11": { 1098 + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==" 996 1099 } 997 1100 }, 998 1101 "workspace": { 999 1102 "dependencies": [ 1000 - "jsr:@hono/hono@^4.9.1", 1001 - "jsr:@logtape/logtape@^1.0.4", 1002 - "jsr:@logtape/pretty@^1.1.0-dev.333+b25e634b", 1003 - "jsr:@sprk/xrpc-server@0.2", 1103 + "jsr:@atp/common@~0.1.0-alpha.4", 1104 + "jsr:@atp/crypto@~0.1.0-alpha.2", 1105 + "jsr:@atp/identity@~0.1.0-alpha.1", 1106 + "jsr:@atp/lexicon@~0.1.0-alpha.2", 1107 + "jsr:@atp/repo@~0.1.0-alpha.2", 1108 + "jsr:@atp/sync@~0.1.0-alpha.3", 1109 + "jsr:@atp/syntax@~0.1.0-alpha.1", 1110 + "jsr:@atp/xrpc-server@~0.1.0-alpha.2", 1111 + "jsr:@atp/xrpc@~0.1.0-alpha.2", 1112 + "jsr:@hono/hono@^4.9.9", 1113 + "jsr:@logtape/logtape@^1.2.0-dev.344+834f24a9", 1114 + "jsr:@logtape/pretty@^1.2.0-dev.344+834f24a9", 1115 + "jsr:@std/assert@^1.0.14", 1004 1116 "jsr:@std/encoding@^1.0.10", 1005 - "npm:@atproto/api@~0.14.22", 1006 - "npm:@atproto/common@~0.4.11", 1007 - "npm:@atproto/crypto@~0.4.4", 1008 - "npm:@atproto/identity@~0.4.8", 1009 - "npm:@atproto/lexicon@~0.4.12", 1010 - "npm:@atproto/repo@~0.8.5", 1011 - "npm:@atproto/sync@~0.1.30", 1012 - "npm:@atproto/syntax@~0.3.4", 1013 - "npm:@atproto/xrpc@~0.7.1", 1014 - "npm:dotenv@^17.2.1", 1015 - "npm:jose@^6.0.12", 1016 - "npm:mongoose@^8.17.1", 1017 - "npm:multiformats@^9.9.0", 1018 - "npm:p-queue@^8.1.0" 1117 + "npm:@atproto/api@~0.16.11", 1118 + "npm:@atproto/identity@~0.4.9", 1119 + "npm:@atproto/repo@~0.8.10", 1120 + "npm:@atproto/sync@~0.1.35", 1121 + "npm:dotenv@^17.2.3", 1122 + "npm:jose@^6.1.0", 1123 + "npm:mongoose@^8.19.0", 1124 + "npm:multiformats@^13.4.1", 1125 + "npm:p-queue@^8.1.1" 1019 1126 ] 1020 1127 } 1021 1128 }
+204
hydration/actor.ts
··· 1 + import { DataPlane } from "../data-plane/index.ts"; 2 + import { Record as ProfileRecord } from "../lex/types/so/sprk/actor/profile.ts"; 3 + import { 4 + HydrationMap, 5 + parseRecord, 6 + parseString, 7 + safeTakedownRef, 8 + } from "./util.ts"; 9 + 10 + export type Actor = { 11 + did: string; 12 + handle?: string; 13 + profile?: ProfileRecord; 14 + profileCid?: string; 15 + profileTakedownRef?: string; 16 + indexedAt?: Date; 17 + createdAt?: Date; 18 + sortedAt?: Date; 19 + takedownRef?: string; 20 + upstreamStatus?: string; 21 + }; 22 + 23 + export type Actors = HydrationMap<Actor>; 24 + 25 + export type ProfileViewerState = { 26 + muted?: boolean; 27 + blockedBy?: string; 28 + blocking?: string; 29 + following?: string; 30 + followedBy?: string; 31 + }; 32 + 33 + export type ProfileViewerStates = HydrationMap<ProfileViewerState>; 34 + 35 + type ActivitySubscriptionState = { 36 + post: boolean; 37 + reply: boolean; 38 + }; 39 + 40 + export type ActivitySubscriptionStates = HydrationMap< 41 + ActivitySubscriptionState | undefined 42 + >; 43 + 44 + type KnownFollowersState = { 45 + count: number; 46 + followers: string[]; 47 + }; 48 + 49 + export type KnownFollowersStates = HydrationMap< 50 + KnownFollowersState | undefined 51 + >; 52 + 53 + export type ProfileAgg = { 54 + followers: number; 55 + follows: number; 56 + posts: number; 57 + feeds: number; 58 + }; 59 + 60 + export type ProfileAggs = HydrationMap<ProfileAgg>; 61 + 62 + export class ActorHydrator { 63 + constructor(public dataplane: DataPlane) {} 64 + 65 + async getRepoRevSafe(did: string | null): Promise<string | null> { 66 + if (!did) return null; 67 + try { 68 + const res = await this.dataplane.sync.latestRev(did); 69 + return parseString(res.rev) ?? null; 70 + } catch { 71 + return null; 72 + } 73 + } 74 + 75 + async getDids( 76 + handleOrDids: string[], 77 + ): Promise<(string | undefined)[]> { 78 + const handles = handleOrDids.filter((actor) => !actor.startsWith("did:")); 79 + const res = handles.length 80 + ? await this.dataplane.actors.getDidsByHandles(handles) 81 + : { dids: [] }; 82 + const didByHandle = handles.reduce( 83 + (acc, cur, i) => { 84 + const did = res.dids[i]; 85 + if (did && did.length > 0) { 86 + return acc.set(cur, did); 87 + } 88 + return acc; 89 + }, 90 + new Map() as Map<string, string>, 91 + ); 92 + return handleOrDids.map((id) => 93 + id.startsWith("did:") ? id : didByHandle.get(id) 94 + ); 95 + } 96 + 97 + async getDidsDefined(handleOrDids: string[]): Promise<string[]> { 98 + const res = await this.getDids(handleOrDids); 99 + return res.filter((did) => did !== undefined); 100 + } 101 + 102 + async getActors( 103 + dids: string[], 104 + opts: { 105 + includeTakedowns?: boolean; 106 + } = {}, 107 + ): Promise<Actors> { 108 + const { includeTakedowns = false } = opts; 109 + if (!dids.length) return new HydrationMap<Actor>(); 110 + const res = await this.dataplane.actors.getActors(dids); 111 + return dids.reduce((acc, did, i) => { 112 + const actor = res.actors[i]; 113 + const isNoHosted = actor.takenDown || 114 + (actor.upstreamStatus && actor.upstreamStatus !== "active"); 115 + if ( 116 + !actor.exists || 117 + (isNoHosted && !includeTakedowns) || 118 + !!actor.tombstonedAt 119 + ) { 120 + return acc.set(did, null); 121 + } 122 + 123 + const profile = actor.profile 124 + ? parseRecord<ProfileRecord>(actor.profile, includeTakedowns) 125 + : undefined; 126 + 127 + return acc.set(did, { 128 + did, 129 + handle: parseString(actor.handle), 130 + profile: profile?.record, 131 + profileCid: profile?.cid, 132 + sortedAt: profile?.sortedAt ?? new Date(0), 133 + profileTakedownRef: profile?.takedownRef, 134 + indexedAt: profile?.indexedAt, 135 + takedownRef: safeTakedownRef(actor), 136 + upstreamStatus: actor.upstreamStatus || undefined, 137 + createdAt: new Date(actor.createdAt ?? 0), 138 + }); 139 + }, new HydrationMap<Actor>()); 140 + } 141 + 142 + // "naive" because this method does not verify the existence of the list itself 143 + // a later check in the main hydrator will remove list uris that have been deleted or 144 + // repurposed to "curate lists" 145 + async getProfileViewerStatesNaive( 146 + dids: string[], 147 + viewer: string, 148 + ): Promise<ProfileViewerStates> { 149 + if (!dids.length) return new HydrationMap<ProfileViewerState>(); 150 + const res = await this.dataplane.relationships.getRelationships( 151 + viewer, 152 + dids, 153 + ); 154 + 155 + return dids.reduce((acc, did, i) => { 156 + const rels = res.relationships[i]; 157 + if (viewer === did) { 158 + // ignore self-follows, self-mutes, self-blocks, self-activity-subscriptions 159 + return acc.set(did, {}); 160 + } 161 + return acc.set(did, { 162 + muted: rels.muted ?? false, 163 + blockedBy: parseString(rels.blockedBy), 164 + blocking: parseString(rels.blocking), 165 + following: parseString(rels.following), 166 + followedBy: parseString(rels.followedBy), 167 + }); 168 + }, new HydrationMap<ProfileViewerState>()); 169 + } 170 + 171 + async getKnownFollowers( 172 + dids: string[], 173 + viewer: string | null, 174 + ): Promise<KnownFollowersStates> { 175 + if (!viewer) return new HydrationMap<KnownFollowersState | undefined>(); 176 + const { results: knownFollowersResults } = await this.dataplane 177 + .follows.getFollowsFollowing(viewer, dids); 178 + return dids.reduce((acc, did, i) => { 179 + const result = knownFollowersResults[i]?.dids; 180 + return acc.set( 181 + did, 182 + result && result.length > 0 183 + ? { 184 + count: result.length, 185 + followers: result.slice(0, 5), 186 + } 187 + : undefined, 188 + ); 189 + }, new HydrationMap<KnownFollowersState | undefined>()); 190 + } 191 + 192 + async getProfileAggregates(dids: string[]): Promise<ProfileAggs> { 193 + if (!dids.length) return new HydrationMap<ProfileAgg>(); 194 + const counts = await this.dataplane.interactions.getCountsForUsers(dids); 195 + return dids.reduce((acc, did, i) => { 196 + return acc.set(did, { 197 + followers: counts.followers[i] ?? 0, 198 + follows: counts.following[i] ?? 0, 199 + posts: counts.posts[i] ?? 0, 200 + feeds: counts.feeds[i] ?? 0, 201 + }); 202 + }, new HydrationMap<ProfileAgg>()); 203 + } 204 + }
+253
hydration/feed.ts
··· 1 + import { Record as FeedGenRecord } from "../lex/types/so/sprk/feed/generator.ts"; 2 + import { Record as BskyFeedGenRecord } from "../lex/types/app/bsky/feed/generator.ts"; 3 + import { Record as LikeRecord } from "../lex/types/so/sprk/feed/like.ts"; 4 + import { Record as PostRecord } from "../lex/types/so/sprk/feed/post.ts"; 5 + import { Record as RepostRecord } from "../lex/types/app/bsky/feed/repost.ts"; 6 + import { VideoMappingDocument } from "../data-plane/db/models.ts"; 7 + import { uriToDid as didFromUri } from "../utils/uris.ts"; 8 + import { 9 + HydrationMap, 10 + ItemRef, 11 + parseRecord, 12 + parseString, 13 + RecordInfo, 14 + split, 15 + } from "./util.ts"; 16 + import { DataPlane } from "../data-plane/index.ts"; 17 + 18 + export type Post = RecordInfo<PostRecord>; 19 + export type Posts = HydrationMap<Post>; 20 + 21 + export type VideoMapping = VideoMappingDocument; 22 + export type VideoMappings = HydrationMap<VideoMapping>; 23 + 24 + export type PostViewerState = { 25 + like?: string; 26 + repost?: string; 27 + threadMuted?: boolean; 28 + }; 29 + 30 + export type PostViewerStates = HydrationMap<PostViewerState>; 31 + 32 + export type ThreadContext = { 33 + // Whether the root author has liked the post. 34 + like?: string; 35 + }; 36 + 37 + export type ThreadContexts = HydrationMap<ThreadContext>; 38 + 39 + export type PostAgg = { 40 + likes: number; 41 + replies: number; 42 + reposts: number; 43 + }; 44 + 45 + export type PostAggs = HydrationMap<PostAgg>; 46 + 47 + export type Like = RecordInfo<LikeRecord>; 48 + export type Likes = HydrationMap<Like>; 49 + 50 + export type Repost = RecordInfo<RepostRecord>; 51 + export type Reposts = HydrationMap<Repost>; 52 + 53 + export type FeedGenAgg = { 54 + likes: number; 55 + }; 56 + 57 + export type FeedGenAggs = HydrationMap<FeedGenAgg>; 58 + 59 + export type FeedGen = RecordInfo<FeedGenRecord | BskyFeedGenRecord>; 60 + export type FeedGens = HydrationMap<FeedGen>; 61 + 62 + export type FeedGenViewerState = { 63 + like?: string; 64 + }; 65 + 66 + export type FeedGenViewerStates = HydrationMap<FeedGenViewerState>; 67 + 68 + export type ThreadRef = ItemRef & { threadRoot: string }; 69 + 70 + // @NOTE the feed item types in the protos for author feeds and timelines 71 + // technically have additional fields, not supported by the mock dataplane. 72 + export type FeedItem = { 73 + post: ItemRef; 74 + repost?: ItemRef; 75 + /** 76 + * If true, overrides the `reason` with `so.sprk.feed.defs#reasonPin`. Used 77 + * only in author feeds. 78 + */ 79 + authorPinned?: boolean; 80 + }; 81 + 82 + export class FeedHydrator { 83 + constructor(public dataplane: DataPlane) {} 84 + 85 + getVideoMappings( 86 + keys: string[], 87 + ): VideoMappings { 88 + if (!keys.length) return new HydrationMap<VideoMapping>(); 89 + 90 + // This would need to be implemented in the dataplane client 91 + // For now, return empty mappings 92 + return new HydrationMap<VideoMapping>(); 93 + } 94 + 95 + async getPosts( 96 + uris: string[], 97 + includeTakedowns = false, 98 + given = new HydrationMap<Post>(), 99 + ): Promise<Posts> { 100 + const [have, need] = split(uris, (uri) => given.has(uri)); 101 + const base = have.reduce( 102 + (acc, uri) => acc.set(uri, given.get(uri) ?? null), 103 + new HydrationMap<Post>(), 104 + ); 105 + if (!need.length) return base; 106 + const res = await this.dataplane.records.getPostRecords(need); 107 + 108 + return need.reduce((acc, uri, i) => { 109 + const record = parseRecord<PostRecord>(res.records[i], includeTakedowns); 110 + return acc.set( 111 + uri, 112 + record ? record : null, 113 + ); 114 + }, base); 115 + } 116 + 117 + async getPostViewerStates( 118 + refs: ThreadRef[], 119 + viewer: string, 120 + ): Promise<PostViewerStates> { 121 + if (!refs.length) return new HydrationMap<PostViewerState>(); 122 + const [likes, reposts] = await Promise.all([ 123 + await this.dataplane.likes.byActorAndSubjects(viewer, refs), 124 + await this.dataplane.reposts.byActorAndSubjects( 125 + viewer, 126 + refs, 127 + ), 128 + ]); 129 + return refs.reduce((acc, { uri }, i) => { 130 + return acc.set(uri, { 131 + like: parseString(likes.uris[i]), 132 + repost: parseString(reposts.uris[i]), 133 + }); 134 + }, new HydrationMap<PostViewerState>()); 135 + } 136 + 137 + async getThreadContexts(refs: ThreadRef[]): Promise<ThreadContexts> { 138 + if (!refs.length) return new HydrationMap<ThreadContext>(); 139 + 140 + const refsByRootAuthor = refs.reduce((acc, ref) => { 141 + const { threadRoot } = ref; 142 + const rootAuthor = didFromUri(threadRoot); 143 + const existingValue = acc.get(rootAuthor) ?? []; 144 + return acc.set(rootAuthor, [...existingValue, ref]); 145 + }, new Map<string, ThreadRef[]>()); 146 + const refsByRootAuthorEntries = Array.from(refsByRootAuthor.entries()); 147 + 148 + const likesPromises = refsByRootAuthorEntries.map( 149 + ([rootAuthor, refsForAuthor]) => 150 + this.dataplane.likes.byActorAndSubjects( 151 + rootAuthor, 152 + refsForAuthor.map(({ uri, cid }) => ({ uri, cid })), 153 + ), 154 + ); 155 + 156 + const rootAuthorsLikes = await Promise.all(likesPromises); 157 + 158 + const likesByUri = refsByRootAuthorEntries.reduce( 159 + (acc, [_rootAuthor, refsForAuthor], i) => { 160 + const likesForRootAuthor = rootAuthorsLikes[i]; 161 + refsForAuthor.forEach(({ uri }, j) => { 162 + acc.set(uri, likesForRootAuthor.uris[j]); 163 + }); 164 + return acc; 165 + }, 166 + new Map<string, string>(), 167 + ); 168 + 169 + return refs.reduce((acc, { uri }) => { 170 + return acc.set(uri, { 171 + like: parseString(likesByUri.get(uri)), 172 + }); 173 + }, new HydrationMap<ThreadContext>()); 174 + } 175 + 176 + async getPostAggregates( 177 + refs: ItemRef[], 178 + ): Promise<PostAggs> { 179 + if (!refs.length) return new HydrationMap<PostAgg>(); 180 + const counts = await this.dataplane.interactions.getInteractionCounts(refs); 181 + return refs.reduce((acc, { uri }, i) => { 182 + return acc.set(uri, { 183 + likes: counts.likes[i] ?? 0, 184 + reposts: counts.reposts[i] ?? 0, 185 + replies: counts.replies[i] ?? 0, 186 + }); 187 + }, new HydrationMap<PostAgg>()); 188 + } 189 + 190 + async getFeedGens( 191 + uris: string[], 192 + includeTakedowns = false, 193 + ): Promise<FeedGens> { 194 + if (!uris.length) return new HydrationMap<FeedGen>(); 195 + const res = await this.dataplane.records.getFeedGeneratorRecords(uris); 196 + return uris.reduce((acc, uri, i) => { 197 + const record = parseRecord<FeedGenRecord | BskyFeedGenRecord>( 198 + res.records[i], 199 + includeTakedowns, 200 + ); 201 + return acc.set(uri, record ?? null); 202 + }, new HydrationMap<FeedGen>()); 203 + } 204 + 205 + async getFeedGenViewerStates( 206 + uris: string[], 207 + viewer: string, 208 + ): Promise<FeedGenViewerStates> { 209 + if (!uris.length) return new HydrationMap<FeedGenViewerState>(); 210 + const likes = await this.dataplane.likes.byActorAndSubjects( 211 + viewer, 212 + uris.map((uri) => ({ uri })), 213 + ); 214 + return uris.reduce((acc, uri, i) => { 215 + return acc.set(uri, { 216 + like: parseString(likes.uris[i]), 217 + }); 218 + }, new HydrationMap<FeedGenViewerState>()); 219 + } 220 + 221 + async getFeedGenAggregates( 222 + refs: ItemRef[], 223 + ): Promise<FeedGenAggs> { 224 + if (!refs.length) return new HydrationMap<FeedGenAgg>(); 225 + const counts = await this.dataplane.interactions.getInteractionCounts(refs); 226 + return refs.reduce((acc, { uri }, i) => { 227 + return acc.set(uri, { 228 + likes: counts.likes[i] ?? 0, 229 + }); 230 + }, new HydrationMap<FeedGenAgg>()); 231 + } 232 + 233 + async getLikes(uris: string[], includeTakedowns = false): Promise<Likes> { 234 + if (!uris.length) return new HydrationMap<Like>(); 235 + const res = await this.dataplane.records.getLikeRecords(uris); 236 + return uris.reduce((acc, uri, i) => { 237 + const record = parseRecord<LikeRecord>(res.records[i], includeTakedowns); 238 + return acc.set(uri, record ?? null); 239 + }, new HydrationMap<Like>()); 240 + } 241 + 242 + async getReposts(uris: string[], includeTakedowns = false): Promise<Reposts> { 243 + if (!uris.length) return new HydrationMap<Repost>(); 244 + const res = await this.dataplane.records.getRepostRecords(uris); 245 + return uris.reduce((acc, uri, i) => { 246 + const record = parseRecord<RepostRecord>( 247 + res.records[i], 248 + includeTakedowns, 249 + ); 250 + return acc.set(uri, record ?? null); 251 + }, new HydrationMap<Repost>()); 252 + } 253 + }
+128
hydration/graph.ts
··· 1 + import { DataPlane } from "../data-plane/index.ts"; 2 + import { Record as BlockRecord } from "../lex/types/app/bsky/graph/block.ts"; 3 + import { Record as FollowRecord } from "../lex/types/app/bsky/graph/follow.ts"; 4 + import { HydrationMap, parseRecord, RecordInfo } from "./util.ts"; 5 + 6 + export type Follow = RecordInfo<FollowRecord>; 7 + export type Follows = HydrationMap<Follow>; 8 + 9 + export type FollowInfo = { 10 + uri: string; 11 + actorDid: string; 12 + subjectDid: string; 13 + }; 14 + 15 + export type Block = RecordInfo<BlockRecord>; 16 + 17 + export type RelationshipPair = [didA: string, didB: string]; 18 + 19 + const dedupePairs = (pairs: RelationshipPair[]): RelationshipPair[] => { 20 + const deduped = pairs.reduce((acc, pair) => { 21 + return acc.set(Blocks.key(...pair), pair); 22 + }, new Map<string, RelationshipPair>()); 23 + return [...deduped.values()]; 24 + }; 25 + 26 + export class Blocks { 27 + _blocks: Map<string, BlockEntry> = new Map(); // did:a,did:b -> block 28 + constructor() {} 29 + 30 + static key(didA: string, didB: string): string { 31 + return [didA, didB].sort().join(","); 32 + } 33 + 34 + set(didA: string, didB: string, block: BlockEntry): Blocks { 35 + const key = Blocks.key(didA, didB); 36 + this._blocks.set(key, block); 37 + return this; 38 + } 39 + 40 + get(didA: string, didB: string): BlockEntry | null { 41 + if (didA === didB) return null; // ignore self-blocks 42 + const key = Blocks.key(didA, didB); 43 + return this._blocks.get(key) ?? null; 44 + } 45 + 46 + merge(blocks: Blocks): Blocks { 47 + blocks._blocks.forEach((block, key) => { 48 + this._blocks.set(key, block); 49 + }); 50 + return this; 51 + } 52 + } 53 + 54 + // No "blocking" vs. "blocked" directionality: only suitable for bidirectional block checks 55 + export type BlockEntry = { 56 + blockUri: string | undefined; 57 + }; 58 + 59 + export class GraphHydrator { 60 + constructor(public dataplane: DataPlane) {} 61 + 62 + async getBidirectionalBlocks(pairs: RelationshipPair[]): Promise<Blocks> { 63 + if (!pairs.length) return new Blocks(); 64 + const deduped = dedupePairs(pairs).map(([a, b]) => ({ a, b })); 65 + const res = await this.dataplane.relationships.getBlockExistence(deduped); 66 + const blocks = new Blocks(); 67 + for (let i = 0; i < deduped.length; i++) { 68 + const pair = deduped[i]; 69 + const block = res.blocks[i]; 70 + blocks.set(pair.a, pair.b, { 71 + blockUri: block.blockedBy || block.blocking || undefined, 72 + }); 73 + } 74 + return blocks; 75 + } 76 + 77 + async getFollows(uris: string[], includeTakedowns = false): Promise<Follows> { 78 + if (!uris.length) return new HydrationMap<Follow>(); 79 + const res = await this.dataplane.records.getFollowRecords(uris); 80 + return uris.reduce((acc, uri, i) => { 81 + const record = parseRecord<FollowRecord>( 82 + res.records[i], 83 + includeTakedowns, 84 + ); 85 + return acc.set(uri, record ?? null); 86 + }, new HydrationMap<Follow>()); 87 + } 88 + 89 + async getBlocks( 90 + uris: string[], 91 + includeTakedowns = false, 92 + ): Promise<HydrationMap<Block>> { 93 + if (!uris.length) return new HydrationMap<Block>(); 94 + const res = await this.dataplane.records.getBlockRecords(uris); 95 + return uris.reduce((acc, uri, i) => { 96 + const record = parseRecord<BlockRecord>(res.records[i], includeTakedowns); 97 + return acc.set(uri, record ?? null); 98 + }, new HydrationMap<Block>()); 99 + } 100 + 101 + async getActorFollows(input: { 102 + did: string; 103 + cursor?: string; 104 + limit?: number; 105 + }): Promise<{ follows: FollowInfo[]; cursor: string | undefined }> { 106 + const { did, cursor, limit } = input; 107 + const res = await this.dataplane.follows.getFollows( 108 + did, 109 + limit, 110 + cursor, 111 + ); 112 + return { follows: res.follows, cursor: res.cursor }; 113 + } 114 + 115 + async getActorFollowers(input: { 116 + did: string; 117 + cursor?: string; 118 + limit?: number; 119 + }): Promise<{ followers: FollowInfo[]; cursor: string | undefined }> { 120 + const { did, cursor, limit } = input; 121 + const res = await this.dataplane.follows.getFollowers( 122 + did, 123 + limit, 124 + cursor, 125 + ); 126 + return { followers: res.followers, cursor: res.cursor }; 127 + } 128 + }
+748
hydration/index.ts
··· 1 + import { assert } from "@std/assert"; 2 + import { mapDefined } from "@atp/common"; 3 + import { AtUri } from "@atp/syntax"; 4 + import { DataPlane } from "../data-plane/index.ts"; 5 + import { ids } from "../lex/lexicons.ts"; 6 + import { Record as ProfileRecord } from "../lex/types/so/sprk/actor/profile.ts"; 7 + import { uriToDid as didFromUri } from "../utils/uris.ts"; 8 + import { 9 + ActivitySubscriptionStates, 10 + ActorHydrator, 11 + Actors, 12 + KnownFollowersStates, 13 + ProfileAggs, 14 + ProfileViewerStates, 15 + } from "./actor.ts"; 16 + import { 17 + FeedGenAggs, 18 + FeedGens, 19 + FeedGenViewerStates, 20 + FeedHydrator, 21 + FeedItem, 22 + Likes, 23 + Post, 24 + PostAggs, 25 + Posts, 26 + PostViewerStates, 27 + Reposts, 28 + ThreadContexts, 29 + ThreadRef, 30 + VideoMappings, 31 + } from "./feed.ts"; 32 + import { 33 + BlockEntry, 34 + Follows, 35 + GraphHydrator, 36 + RelationshipPair, 37 + } from "./graph.ts"; 38 + import { 39 + HydrationMap, 40 + ItemRef, 41 + mergeManyMaps, 42 + mergeMaps, 43 + RecordInfo, 44 + } from "./util.ts"; 45 + import { getLogger } from "@logtape/logtape"; 46 + 47 + export class HydrateCtx { 48 + viewer: string | null; 49 + includeTakedowns?: boolean; 50 + includeActorTakedowns?: boolean; 51 + include3pBlocks?: boolean; 52 + 53 + constructor(private vals: HydrateCtxVals) { 54 + this.viewer = this.vals.viewer !== null 55 + ? serviceRefToDid(this.vals.viewer) 56 + : null; 57 + this.includeTakedowns = this.vals.includeTakedowns; 58 + this.includeActorTakedowns = this.vals.includeActorTakedowns; 59 + this.include3pBlocks = this.vals.include3pBlocks; 60 + } 61 + // Convenience with use with dataplane.getActors cache control 62 + get skipCacheForViewer() { 63 + if (!this.viewer) return undefined; 64 + return [this.viewer]; 65 + } 66 + copy<V extends Partial<HydrateCtxVals>>(vals?: V): HydrateCtx & V { 67 + return new HydrateCtx({ ...this.vals, ...vals }) as HydrateCtx & V; 68 + } 69 + } 70 + 71 + export type HydrateCtxVals = { 72 + viewer: string | null; 73 + includeTakedowns?: boolean; 74 + includeActorTakedowns?: boolean; 75 + include3pBlocks?: boolean; 76 + }; 77 + 78 + export type HydrationState = { 79 + ctx?: HydrateCtx; 80 + actors?: Actors; 81 + profileViewers?: ProfileViewerStates; 82 + profileAggs?: ProfileAggs; 83 + posts?: Posts; 84 + postAggs?: PostAggs; 85 + postViewers?: PostViewerStates; 86 + threadContexts?: ThreadContexts; 87 + postBlocks?: PostBlocks; 88 + reposts?: Reposts; 89 + follows?: Follows; 90 + followBlocks?: FollowBlocks; 91 + likes?: Likes; 92 + likeBlocks?: LikeBlocks; 93 + feedgens?: FeedGens; 94 + feedgenViewers?: FeedGenViewerStates; 95 + feedgenAggs?: FeedGenAggs; 96 + knownFollowers?: KnownFollowersStates; 97 + activitySubscriptions?: ActivitySubscriptionStates; 98 + bidirectionalBlocks?: BidirectionalBlocks; 99 + videoMappings?: VideoMappings; 100 + }; 101 + 102 + export type PostBlock = { embed: boolean; parent: boolean; root: boolean }; 103 + export type PostBlocks = HydrationMap<PostBlock>; 104 + type PostBlockPairs = { 105 + embed?: RelationshipPair; 106 + parent?: RelationshipPair; 107 + root?: RelationshipPair; 108 + }; 109 + 110 + export type LikeBlock = boolean; 111 + export type LikeBlocks = HydrationMap<LikeBlock>; 112 + 113 + export type FollowBlock = boolean; 114 + export type FollowBlocks = HydrationMap<FollowBlock>; 115 + 116 + export type BidirectionalBlocks = HydrationMap<HydrationMap<boolean>>; 117 + 118 + const hydrationLogger = getLogger(["appview", "hydrator"]); 119 + 120 + export class Hydrator { 121 + actor: ActorHydrator; 122 + feed: FeedHydrator; 123 + graph: GraphHydrator; 124 + 125 + constructor( 126 + public dataplane: DataPlane, 127 + ) { 128 + this.actor = new ActorHydrator(dataplane); 129 + this.feed = new FeedHydrator(dataplane); 130 + this.graph = new GraphHydrator(dataplane); 131 + } 132 + 133 + // so.sprk.actor.defs#profileView 134 + // - profile viewer 135 + // Note: builds on the naive profile viewer hydrator and removes references to lists that have been deleted 136 + async hydrateProfileViewers( 137 + dids: string[], 138 + ctx: HydrateCtx, 139 + ): Promise<HydrationState> { 140 + const viewer = ctx.viewer; 141 + if (!viewer) return {}; 142 + const profileViewers = await this.actor.getProfileViewerStatesNaive( 143 + dids, 144 + viewer, 145 + ); 146 + 147 + return { 148 + profileViewers, 149 + ctx, 150 + }; 151 + } 152 + 153 + // so.sprk.actor.defs#profileView 154 + // - profile 155 + // - list basic 156 + async hydrateProfiles( 157 + dids: string[], 158 + ctx: HydrateCtx, 159 + ): Promise<HydrationState> { 160 + const includeTakedowns = ctx.includeTakedowns || ctx.includeActorTakedowns; 161 + const [actors, profileViewersState] = await Promise.all([ 162 + this.actor.getActors(dids, { 163 + includeTakedowns, 164 + }), 165 + this.hydrateProfileViewers(dids, ctx), 166 + ]); 167 + return mergeStates(profileViewersState ?? {}, { 168 + actors, 169 + ctx, 170 + }); 171 + } 172 + 173 + // so.sprk.actor.defs#profileViewBasic 174 + // - profile basic 175 + // - profile 176 + // - list basic 177 + hydrateProfilesBasic( 178 + dids: string[], 179 + ctx: HydrateCtx, 180 + ): Promise<HydrationState> { 181 + return this.hydrateProfiles(dids, ctx); 182 + } 183 + 184 + // so.sprk.actor.defs#profileViewDetailed 185 + // - profile detailed 186 + // - profile 187 + // - list basic 188 + // - starterpack 189 + // - profile 190 + // - list basic 191 + // - labels 192 + async hydrateProfilesDetailed( 193 + dids: string[], 194 + ctx: HydrateCtx, 195 + ): Promise<HydrationState> { 196 + let knownFollowers: KnownFollowersStates = new HydrationMap(); 197 + try { 198 + knownFollowers = await this.actor.getKnownFollowers(dids, ctx.viewer); 199 + } catch (err) { 200 + hydrationLogger.error( 201 + "Failed to get known followers for profiles", 202 + { err }, 203 + ); 204 + } 205 + 206 + const subjectsToKnownFollowersMap = Array.from( 207 + knownFollowers.keys(), 208 + ).reduce((acc, did) => { 209 + const known = knownFollowers.get(did); 210 + if (known) { 211 + acc.set(did, known.followers); 212 + } 213 + return acc; 214 + }, new Map<string, string[]>()); 215 + const allKnownFollowerDids = Array.from(knownFollowers.values()) 216 + .filter(Boolean) 217 + .flatMap((f) => f!.followers); 218 + const allDids = Array.from(new Set(dids.concat(allKnownFollowerDids))); 219 + const [state, profileAggs, bidirectionalBlocks] = await Promise.all([ 220 + this.hydrateProfiles(allDids, ctx), 221 + this.actor.getProfileAggregates(dids), 222 + this.hydrateBidirectionalBlocks(subjectsToKnownFollowersMap), 223 + ]); 224 + return mergeManyStates(state, { 225 + profileAggs, 226 + knownFollowers, 227 + ctx, 228 + bidirectionalBlocks, 229 + }); 230 + } 231 + 232 + // so.sprk.feed.defs#postView 233 + // - post 234 + // - profile 235 + // - list basic 236 + // - list 237 + // - profile 238 + // - list basic 239 + // - feedgen 240 + // - profile 241 + // - list basic 242 + // - mod service 243 + // - profile 244 + // - list basic 245 + async hydratePosts( 246 + refs: ItemRef[], 247 + ctx: HydrateCtx, 248 + state: HydrationState = {}, 249 + ): Promise<HydrationState> { 250 + const uris = refs.map((ref) => ref.uri); 251 + 252 + state.posts ??= new HydrationMap<Post>(); 253 + const addPostsToHydrationState = (posts: Posts) => { 254 + posts.forEach((post, uri) => { 255 + state.posts ??= new HydrationMap<Post>(); 256 + state.posts.set(uri, post); 257 + }); 258 + }; 259 + 260 + // layer 0: the posts in the thread 261 + const postsLayer0 = await this.feed.getPosts( 262 + uris, 263 + ctx.includeTakedowns, 264 + state.posts, 265 + ); 266 + addPostsToHydrationState(postsLayer0); 267 + 268 + const additionalRootUris = rootUrisFromPosts(postsLayer0); // supports computing threadgates 269 + const threadRootUris = new Set<string>(); 270 + for (const [uri, post] of postsLayer0) { 271 + if (post) { 272 + threadRootUris.add(rootUriFromPost(post) ?? uri); 273 + } 274 + } 275 + 276 + // fetch additional root URIs for threadgates 277 + const postsLayer1 = await this.feed.getPosts( 278 + additionalRootUris, 279 + ctx.includeTakedowns, 280 + state.posts, 281 + ); 282 + addPostsToHydrationState(postsLayer1); 283 + 284 + const posts = mergeManyMaps(postsLayer0, postsLayer1) ?? postsLayer0; 285 + const allPostUris = [...posts.keys()]; 286 + const allRefs = refs; 287 + const threadRefs = allRefs.map((ref) => ({ 288 + ...ref, 289 + threadRoot: posts.get(ref.uri)?.record.reply?.root.uri ?? ref.uri, 290 + })); 291 + 292 + const [ 293 + postAggs, 294 + postViewers, 295 + postBlocks, 296 + profileState, 297 + feedGenState, 298 + ] = await Promise.all([ 299 + this.feed.getPostAggregates(allRefs), 300 + ctx.viewer 301 + ? this.feed.getPostViewerStates(threadRefs, ctx.viewer) 302 + : undefined, 303 + this.hydratePostBlocks(posts), 304 + this.hydrateProfiles(allPostUris.map(didFromUri), ctx), 305 + this.hydrateFeedGens([], ctx), 306 + ]); 307 + // combine all hydration state 308 + return mergeManyStates( 309 + profileState, 310 + feedGenState, 311 + { 312 + posts, 313 + postAggs, 314 + postViewers, 315 + postBlocks, 316 + ctx, 317 + }, 318 + ); 319 + } 320 + 321 + private async hydratePostBlocks( 322 + posts: Posts, 323 + ): Promise<PostBlocks> { 324 + const postBlocks = new HydrationMap<PostBlock>(); 325 + const postBlocksPairs = new Map<string, PostBlockPairs>(); 326 + const relationships: RelationshipPair[] = []; 327 + for (const [uri, item] of posts) { 328 + if (!item) continue; 329 + const post = item.record; 330 + const creator = didFromUri(uri); 331 + const postBlockPairs: PostBlockPairs = {}; 332 + postBlocksPairs.set(uri, postBlockPairs); 333 + // 3p block for replies 334 + const parentUri = post.reply?.parent.uri; 335 + const parentDid = parentUri && didFromUri(parentUri); 336 + if (parentDid && parentDid !== creator) { 337 + const pair: RelationshipPair = [creator, parentDid]; 338 + relationships.push(pair); 339 + postBlockPairs.parent = pair; 340 + } 341 + const rootUri = post.reply?.root.uri; 342 + const rootDid = rootUri && didFromUri(rootUri); 343 + if (rootDid && rootDid !== creator) { 344 + const pair: RelationshipPair = [creator, rootDid]; 345 + relationships.push(pair); 346 + postBlockPairs.root = pair; 347 + } 348 + // No embed blocking - nested record logic removed 349 + } 350 + // replace embed/parent/root pairs with block state 351 + const blocks = await this.hydrateBidirectionalBlocks( 352 + pairsToMap(relationships), 353 + ); 354 + for (const [uri, { embed, parent, root }] of postBlocksPairs) { 355 + postBlocks.set(uri, { 356 + embed: !!embed && !!isBlocked(blocks, embed), 357 + parent: !!parent && !!isBlocked(blocks, parent), 358 + root: !!root && !!isBlocked(blocks, root), 359 + }); 360 + } 361 + return postBlocks; 362 + } 363 + 364 + // so.sprk.feed.defs#feedViewPost 365 + // - post (+ replies w/ reply parent author) 366 + // - profile 367 + // - list basic 368 + // - list 369 + // - profile 370 + // - list basic 371 + // - feedgen 372 + // - profile 373 + // - list basic 374 + // - repost 375 + // - profile 376 + // - list basic 377 + // - post 378 + // - ... 379 + async hydrateFeedItems( 380 + items: FeedItem[], 381 + ctx: HydrateCtx, 382 + ): Promise<HydrationState> { 383 + // get posts, collect reply refs 384 + const posts = await this.feed.getPosts( 385 + items.map((item) => item.post.uri), 386 + ctx.includeTakedowns, 387 + ); 388 + const rootUris: string[] = []; 389 + const parentUris: string[] = []; 390 + const postAndReplyRefs: ItemRef[] = []; 391 + posts.forEach((post, uri) => { 392 + if (!post) return; 393 + postAndReplyRefs.push({ uri, cid: post.cid }); 394 + if (post.record.reply) { 395 + rootUris.push(post.record.reply.root.uri); 396 + parentUris.push(post.record.reply.parent.uri); 397 + postAndReplyRefs.push(post.record.reply.root, post.record.reply.parent); 398 + } 399 + }); 400 + // get replies, collect reply parent authors 401 + const replies = await this.feed.getPosts( 402 + [...rootUris, ...parentUris], 403 + ctx.includeTakedowns, 404 + ); 405 + const replyParentAuthors: string[] = []; 406 + parentUris.forEach((uri) => { 407 + const parent = replies.get(uri); 408 + if (!parent?.record.reply) return; 409 + replyParentAuthors.push(didFromUri(parent.record.reply.parent.uri)); 410 + }); 411 + // hydrate state for all posts, reposts, authors of reposts + reply parent authors 412 + const repostUris = mapDefined(items, (item) => item.repost?.uri); 413 + const [postState, repostProfileState, reposts] = await Promise.all([ 414 + this.hydratePosts(postAndReplyRefs, ctx, { 415 + posts: posts.merge(replies), // avoids refetches of posts 416 + }), 417 + this.hydrateProfiles( 418 + [...repostUris.map(didFromUri), ...replyParentAuthors], 419 + ctx, 420 + ), 421 + this.feed.getReposts(repostUris, ctx.includeTakedowns), 422 + ]); 423 + return mergeManyStates(postState, repostProfileState, { 424 + reposts, 425 + ctx, 426 + }); 427 + } 428 + 429 + // so.sprk.feed.defs#threadViewPost 430 + // - post 431 + // - profile 432 + // - list basic 433 + // - list 434 + // - profile 435 + // - list basic 436 + // - feedgen 437 + // - profile 438 + // - list basic 439 + async hydrateThreadPosts( 440 + refs: ItemRef[], 441 + ctx: HydrateCtx, 442 + ): Promise<HydrationState> { 443 + const postsState = await this.hydratePosts(refs, ctx); 444 + 445 + const { posts } = postsState; 446 + const postsList = posts ? Array.from(posts.entries()) : []; 447 + 448 + const isDefined = ( 449 + entry: [string, Post | null], 450 + ): entry is [string, Post] => { 451 + const [, post] = entry; 452 + return !!post; 453 + }; 454 + 455 + const threadRefs: ThreadRef[] = postsList 456 + .filter(isDefined) 457 + .map(([uri, post]) => ({ 458 + uri, 459 + cid: post.cid, 460 + threadRoot: post.record.reply?.root.uri ?? uri, 461 + })); 462 + 463 + const threadContexts = await this.feed.getThreadContexts(threadRefs); 464 + 465 + return mergeStates(postsState, { threadContexts }); 466 + } 467 + 468 + // so.sprk.feed.defs#generatorView 469 + // - feedgen 470 + // - profile 471 + // - list basic 472 + async hydrateFeedGens( 473 + uris: string[], // @TODO any way to get refs here? 474 + ctx: HydrateCtx, 475 + ): Promise<HydrationState> { 476 + const [feedgens, feedgenAggs, feedgenViewers, profileState] = await Promise 477 + .all([ 478 + this.feed.getFeedGens(uris, ctx.includeTakedowns), 479 + this.feed.getFeedGenAggregates( 480 + uris.map((uri) => ({ uri })), 481 + ), 482 + ctx.viewer 483 + ? this.feed.getFeedGenViewerStates(uris, ctx.viewer) 484 + : undefined, 485 + this.hydrateProfiles(uris.map(didFromUri), ctx), 486 + ]); 487 + return mergeStates(profileState, { 488 + feedgens, 489 + feedgenAggs, 490 + feedgenViewers, 491 + ctx, 492 + }); 493 + } 494 + 495 + // so.sprk.feed.getLikes#like 496 + // - like 497 + // - profile 498 + // - list basic 499 + async hydrateLikes( 500 + authorDid: string, 501 + uris: string[], 502 + ctx: HydrateCtx, 503 + ): Promise<HydrationState> { 504 + const [likes, profileState] = await Promise.all([ 505 + this.feed.getLikes(uris, ctx.includeTakedowns), 506 + this.hydrateProfiles(uris.map(didFromUri), ctx), 507 + ]); 508 + 509 + const pairs: RelationshipPair[] = []; 510 + for (const [uri, like] of likes) { 511 + if (like) { 512 + pairs.push([authorDid, didFromUri(uri)]); 513 + } 514 + } 515 + const blocks = await this.hydrateBidirectionalBlocks( 516 + pairsToMap(pairs), 517 + ); 518 + const likeBlocks = new HydrationMap<LikeBlock>(); 519 + for (const [uri, like] of likes) { 520 + if (like) { 521 + likeBlocks.set(uri, isBlocked(blocks, [authorDid, didFromUri(uri)])); 522 + } else { 523 + likeBlocks.set(uri, null); 524 + } 525 + } 526 + 527 + return mergeStates(profileState, { likes, likeBlocks, ctx }); 528 + } 529 + 530 + // so.sprk.feed.getRepostedBy#repostedBy 531 + // - repost 532 + // - profile 533 + // - list basic 534 + async hydrateReposts(uris: string[], ctx: HydrateCtx) { 535 + const [reposts, profileState] = await Promise.all([ 536 + this.feed.getReposts(uris, ctx.includeTakedowns), 537 + this.hydrateProfiles(uris.map(didFromUri), ctx), 538 + ]); 539 + return mergeStates(profileState, { reposts, ctx }); 540 + } 541 + 542 + // provides partial hydration state within getFollows / getFollowers, mainly for applying rules 543 + async hydrateFollows( 544 + uris: string[], 545 + ): Promise<HydrationState> { 546 + const follows = await this.graph.getFollows(uris); 547 + const pairs: RelationshipPair[] = []; 548 + for (const [uri, follow] of follows) { 549 + if (follow) { 550 + pairs.push([didFromUri(uri), follow.record.subject]); 551 + } 552 + } 553 + const blocks = await this.hydrateBidirectionalBlocks( 554 + pairsToMap(pairs), 555 + ); 556 + const followBlocks = new HydrationMap<FollowBlock>(); 557 + for (const [uri, follow] of follows) { 558 + if (follow) { 559 + followBlocks.set( 560 + uri, 561 + isBlocked(blocks, [didFromUri(uri), follow.record.subject]), 562 + ); 563 + } else { 564 + followBlocks.set(uri, null); 565 + } 566 + } 567 + return { follows, followBlocks }; 568 + } 569 + 570 + async hydrateBidirectionalBlocks( 571 + didMap: Map<string, string[]>, 572 + ): Promise<BidirectionalBlocks> { 573 + const pairs: RelationshipPair[] = []; 574 + for (const [source, targets] of didMap) { 575 + for (const target of targets) { 576 + pairs.push([source, target]); 577 + } 578 + } 579 + 580 + const blocks = await this.graph.getBidirectionalBlocks(pairs); 581 + 582 + const result: BidirectionalBlocks = new HydrationMap< 583 + HydrationMap<boolean> 584 + >(); 585 + for (const [source, targets] of didMap) { 586 + const didBlocks = new HydrationMap<boolean>(); 587 + for (const target of targets) { 588 + const block = blocks.get(source, target); 589 + 590 + const blockEntry: BlockEntry = { 591 + blockUri: block?.blockUri, 592 + }; 593 + 594 + didBlocks.set( 595 + target, 596 + !!blockEntry.blockUri, 597 + ); 598 + } 599 + result.set(source, didBlocks); 600 + } 601 + 602 + return result; 603 + } 604 + 605 + // ad-hoc record hydration 606 + // in com.atproto.repo.getRecord 607 + async getRecord(uri: string, includeTakedowns = false) { 608 + const parsed = new AtUri(uri); 609 + const collection = parsed.collection; 610 + if (collection === ids.SoSprkFeedPost) { 611 + return ( 612 + (await this.feed.getPosts([uri], includeTakedowns)).get(uri) ?? 613 + undefined 614 + ); 615 + } else if (collection === ids.AppBskyFeedRepost) { 616 + return ( 617 + (await this.feed.getReposts([uri], includeTakedowns)).get(uri) ?? 618 + undefined 619 + ); 620 + } else if (collection === ids.SoSprkFeedLike) { 621 + return ( 622 + (await this.feed.getLikes([uri], includeTakedowns)).get(uri) ?? 623 + undefined 624 + ); 625 + } else if (collection === ids.AppBskyGraphFollow) { 626 + return ( 627 + (await this.graph.getFollows([uri], includeTakedowns)).get(uri) ?? 628 + undefined 629 + ); 630 + } else if (collection === ids.AppBskyGraphBlock) { 631 + return ( 632 + (await this.graph.getBlocks([uri], includeTakedowns)).get(uri) ?? 633 + undefined 634 + ); 635 + } else if ( 636 + collection === ids.AppBskyFeedGenerator || 637 + collection === ids.SoSprkFeedGenerator 638 + ) { 639 + return ( 640 + (await this.feed.getFeedGens([uri], includeTakedowns)).get(uri) ?? 641 + undefined 642 + ); 643 + } else if (collection === ids.SoSprkActorProfile) { 644 + const did = parsed.hostname; 645 + const actor = ( 646 + await this.actor.getActors([did], { includeTakedowns }) 647 + ).get(did); 648 + if (!actor?.profile || !actor?.profileCid) return undefined; 649 + const recordInfo: RecordInfo<ProfileRecord> = { 650 + record: actor.profile, 651 + cid: actor.profileCid, 652 + sortedAt: actor.sortedAt ?? new Date(0), 653 + indexedAt: actor.indexedAt ?? new Date(0), 654 + takedownRef: actor.profileTakedownRef, 655 + }; 656 + 657 + return recordInfo; 658 + } 659 + } 660 + 661 + createContext = (vals: HydrateCtxVals) => { 662 + return new HydrateCtx({ 663 + viewer: vals.viewer, 664 + includeTakedowns: vals.includeTakedowns, 665 + include3pBlocks: vals.include3pBlocks, 666 + }); 667 + }; 668 + 669 + async resolveUri(uriStr: string) { 670 + const uri = new AtUri(uriStr); 671 + const [did] = await this.actor.getDids([uri.host]); 672 + if (!did) return uriStr; 673 + uri.host = did; 674 + return uri.toString(); 675 + } 676 + } 677 + 678 + // service refs may look like "did:plc:example#service_id". we want to extract the did part "did:plc:example". 679 + const serviceRefToDid = (serviceRef: string) => { 680 + const idx = serviceRef.indexOf("#"); 681 + return idx !== -1 ? serviceRef.slice(0, idx) : serviceRef; 682 + }; 683 + 684 + const rootUrisFromPosts = (posts: Posts): string[] => { 685 + const uris: string[] = []; 686 + for (const item of posts.values()) { 687 + const rootUri = item && rootUriFromPost(item); 688 + if (rootUri) { 689 + uris.push(rootUri); 690 + } 691 + } 692 + return uris; 693 + }; 694 + 695 + const rootUriFromPost = (post: Post): string | undefined => { 696 + return post.record.reply?.root.uri; 697 + }; 698 + 699 + const isBlocked = (blocks: BidirectionalBlocks, [a, b]: RelationshipPair) => { 700 + return blocks.get(a)?.get(b) ?? false; 701 + }; 702 + 703 + const pairsToMap = (pairs: RelationshipPair[]): Map<string, string[]> => { 704 + const map = new Map<string, string[]>(); 705 + for (const [a, b] of pairs) { 706 + const list = map.get(a) ?? []; 707 + list.push(b); 708 + map.set(a, list); 709 + } 710 + return map; 711 + }; 712 + 713 + export const mergeStates = ( 714 + stateA: HydrationState, 715 + stateB: HydrationState, 716 + ): HydrationState => { 717 + assert( 718 + !stateA.ctx?.viewer || 719 + !stateB.ctx?.viewer || 720 + stateA.ctx?.viewer === stateB.ctx?.viewer, 721 + "incompatible viewers", 722 + ); 723 + return { 724 + ctx: stateA.ctx ?? stateB.ctx, 725 + actors: mergeMaps(stateA.actors, stateB.actors), 726 + profileAggs: mergeMaps(stateA.profileAggs, stateB.profileAggs), 727 + profileViewers: mergeMaps(stateA.profileViewers, stateB.profileViewers), 728 + posts: mergeMaps(stateA.posts, stateB.posts), 729 + postAggs: mergeMaps(stateA.postAggs, stateB.postAggs), 730 + postViewers: mergeMaps(stateA.postViewers, stateB.postViewers), 731 + threadContexts: mergeMaps(stateA.threadContexts, stateB.threadContexts), 732 + postBlocks: mergeMaps(stateA.postBlocks, stateB.postBlocks), 733 + reposts: mergeMaps(stateA.reposts, stateB.reposts), 734 + follows: mergeMaps(stateA.follows, stateB.follows), 735 + followBlocks: mergeMaps(stateA.followBlocks, stateB.followBlocks), 736 + likes: mergeMaps(stateA.likes, stateB.likes), 737 + likeBlocks: mergeMaps(stateA.likeBlocks, stateB.likeBlocks), 738 + feedgens: mergeMaps(stateA.feedgens, stateB.feedgens), 739 + feedgenAggs: mergeMaps(stateA.feedgenAggs, stateB.feedgenAggs), 740 + feedgenViewers: mergeMaps(stateA.feedgenViewers, stateB.feedgenViewers), 741 + knownFollowers: mergeMaps(stateA.knownFollowers, stateB.knownFollowers), 742 + videoMappings: mergeMaps(stateA.videoMappings, stateB.videoMappings), 743 + }; 744 + }; 745 + 746 + export const mergeManyStates = (...states: HydrationState[]) => { 747 + return states.reduce(mergeStates, {} as HydrationState); 748 + };
+146
hydration/util.ts
··· 1 + import { CID } from "multiformats/cid"; 2 + 3 + import { AtUri } from "@atp/syntax"; 4 + import { lexicons } from "../lex/lexicons.ts"; 5 + import { Record } from "../data-plane/routes/records.ts"; 6 + import { jsonStringToLex, RepoRecord } from "@atp/lexicon"; 7 + 8 + export class HydrationMap<T> extends Map<string, T | null> implements Merges { 9 + merge(map: HydrationMap<T>): this { 10 + map.forEach((val, key) => { 11 + this.set(key, val); 12 + }); 13 + return this; 14 + } 15 + } 16 + 17 + export interface Merges { 18 + merge<T extends this>(map: T): this; 19 + } 20 + 21 + type UnknownRecord = { $type: string; [x: string]: unknown }; 22 + 23 + export type RecordInfo<T extends UnknownRecord> = { 24 + record: T; 25 + cid: string; 26 + sortedAt: Date; 27 + indexedAt: Date; 28 + takedownRef: string | undefined; 29 + }; 30 + 31 + export const mergeMaps = <V, M extends HydrationMap<V>>( 32 + mapA?: M, 33 + mapB?: M, 34 + ): M | undefined => { 35 + if (!mapA) return mapB; 36 + if (!mapB) return mapA; 37 + return mapA.merge(mapB); 38 + }; 39 + 40 + export const mergeNestedMaps = <V, M extends HydrationMap<HydrationMap<V>>>( 41 + mapA?: M, 42 + mapB?: M, 43 + ): M | undefined => { 44 + if (!mapA) return mapB; 45 + if (!mapB) return mapA; 46 + 47 + for (const [key, map] of mapB) { 48 + const merged = mergeMaps(mapA.get(key) ?? undefined, map ?? undefined); 49 + mapA.set(key, merged ?? null); 50 + } 51 + 52 + return mapA; 53 + }; 54 + 55 + export const mergeManyMaps = <T>(...maps: HydrationMap<T>[]) => { 56 + return maps.reduce(mergeMaps, undefined as HydrationMap<T> | undefined); 57 + }; 58 + 59 + export type ItemRef = { uri: string; cid?: string }; 60 + 61 + export const parseRecord = <T extends UnknownRecord>( 62 + entry: Record, 63 + includeTakedowns: boolean, 64 + ): RecordInfo<T> | undefined => { 65 + if (!includeTakedowns && entry.takenDown) { 66 + return undefined; 67 + } 68 + const record = jsonStringToLex(entry.record); 69 + const cid = entry.cid; 70 + const sortedAt = new Date(entry.sortedAt ?? 0); 71 + const indexedAt = new Date(entry.indexedAt ?? 0); 72 + if (!record || !cid) return; 73 + if (!isValidRecord(entry.record)) { 74 + return; 75 + } 76 + return { 77 + record: record as T, 78 + cid, 79 + sortedAt, 80 + indexedAt, 81 + takedownRef: safeTakedownRef(entry), 82 + }; 83 + }; 84 + 85 + const isValidRecord = (record: string) => { 86 + const lex = jsonStringToLex(record); 87 + const lexRecord = lex as RepoRecord; 88 + if (typeof lexRecord["$type"] !== "string") { 89 + return false; 90 + } 91 + try { 92 + lexicons.assertValidRecord(lexRecord["$type"], lexRecord); 93 + return true; 94 + } catch { 95 + return false; 96 + } 97 + }; 98 + 99 + export const parseString = (str: string | undefined): string | undefined => { 100 + return str && str.length > 0 ? str : undefined; 101 + }; 102 + 103 + export const parseCid = (cidStr: string | undefined): CID | undefined => { 104 + if (!cidStr || cidStr.length === 0) return; 105 + try { 106 + return CID.parse(cidStr); 107 + } catch { 108 + return; 109 + } 110 + }; 111 + 112 + export const urisByCollection = (uris: string[]): Map<string, string[]> => { 113 + const result = new Map<string, string[]>(); 114 + for (const uri of uris) { 115 + const collection = new AtUri(uri).collection; 116 + const items = result.get(collection) ?? []; 117 + items.push(uri); 118 + result.set(collection, items); 119 + } 120 + return result; 121 + }; 122 + 123 + export const split = <T>( 124 + items: T[], 125 + predicate: (item: T) => boolean, 126 + ): [T[], T[]] => { 127 + const yes: T[] = []; 128 + const no: T[] = []; 129 + for (const item of items) { 130 + if (predicate(item)) { 131 + yes.push(item); 132 + } else { 133 + no.push(item); 134 + } 135 + } 136 + return [yes, no]; 137 + }; 138 + 139 + export const safeTakedownRef = (obj?: { 140 + takenDown: boolean; 141 + takedownRef?: string | undefined; 142 + }): string | undefined => { 143 + if (!obj) return; 144 + if (obj.takedownRef) return obj.takedownRef; 145 + if (obj.takenDown) return "SPRK-TAKEDOWN-UNKNOWN"; 146 + };
+260 -260
lex/index.ts
··· 8 8 type Options as XrpcOptions, 9 9 Server as XrpcServer, 10 10 type StreamConfigOrHandler, 11 - } from "@sprk/xrpc-server"; 11 + } from "@atp/xrpc-server"; 12 12 import { schemas } from "./lexicons.ts"; 13 13 import * as ToolsOzoneSignatureFindCorrelation from "./types/tools/ozone/signature/findCorrelation.ts"; 14 14 import * as ToolsOzoneSignatureSearchAccounts from "./types/tools/ozone/signature/searchAccounts.ts"; ··· 402 402 ToolsOzoneSignatureFindCorrelation.HandlerOutput 403 403 >, 404 404 ) { 405 - const nsid = "tools.ozone.signature.findCorrelation"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 405 + const nsid = "tools.ozone.signature.findCorrelation"; // @ts-ignore - dynamically generated 406 406 return this._server.xrpc.method(nsid, cfg); 407 407 } 408 408 ··· 414 414 ToolsOzoneSignatureSearchAccounts.HandlerOutput 415 415 >, 416 416 ) { 417 - const nsid = "tools.ozone.signature.searchAccounts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 417 + const nsid = "tools.ozone.signature.searchAccounts"; // @ts-ignore - dynamically generated 418 418 return this._server.xrpc.method(nsid, cfg); 419 419 } 420 420 ··· 426 426 ToolsOzoneSignatureFindRelatedAccounts.HandlerOutput 427 427 >, 428 428 ) { 429 - const nsid = "tools.ozone.signature.findRelatedAccounts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 429 + const nsid = "tools.ozone.signature.findRelatedAccounts"; // @ts-ignore - dynamically generated 430 430 return this._server.xrpc.method(nsid, cfg); 431 431 } 432 432 } ··· 446 446 ToolsOzoneServerGetConfig.HandlerOutput 447 447 >, 448 448 ) { 449 - const nsid = "tools.ozone.server.getConfig"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 449 + const nsid = "tools.ozone.server.getConfig"; // @ts-ignore - dynamically generated 450 450 return this._server.xrpc.method(nsid, cfg); 451 451 } 452 452 } ··· 466 466 ToolsOzoneTeamListMembers.HandlerOutput 467 467 >, 468 468 ) { 469 - const nsid = "tools.ozone.team.listMembers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 469 + const nsid = "tools.ozone.team.listMembers"; // @ts-ignore - dynamically generated 470 470 return this._server.xrpc.method(nsid, cfg); 471 471 } 472 472 ··· 478 478 ToolsOzoneTeamDeleteMember.HandlerOutput 479 479 >, 480 480 ) { 481 - const nsid = "tools.ozone.team.deleteMember"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 481 + const nsid = "tools.ozone.team.deleteMember"; // @ts-ignore - dynamically generated 482 482 return this._server.xrpc.method(nsid, cfg); 483 483 } 484 484 ··· 490 490 ToolsOzoneTeamUpdateMember.HandlerOutput 491 491 >, 492 492 ) { 493 - const nsid = "tools.ozone.team.updateMember"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 493 + const nsid = "tools.ozone.team.updateMember"; // @ts-ignore - dynamically generated 494 494 return this._server.xrpc.method(nsid, cfg); 495 495 } 496 496 ··· 502 502 ToolsOzoneTeamAddMember.HandlerOutput 503 503 >, 504 504 ) { 505 - const nsid = "tools.ozone.team.addMember"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 505 + const nsid = "tools.ozone.team.addMember"; // @ts-ignore - dynamically generated 506 506 return this._server.xrpc.method(nsid, cfg); 507 507 } 508 508 } ··· 522 522 ToolsOzoneCommunicationUpdateTemplate.HandlerOutput 523 523 >, 524 524 ) { 525 - const nsid = "tools.ozone.communication.updateTemplate"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 525 + const nsid = "tools.ozone.communication.updateTemplate"; // @ts-ignore - dynamically generated 526 526 return this._server.xrpc.method(nsid, cfg); 527 527 } 528 528 ··· 534 534 ToolsOzoneCommunicationCreateTemplate.HandlerOutput 535 535 >, 536 536 ) { 537 - const nsid = "tools.ozone.communication.createTemplate"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 537 + const nsid = "tools.ozone.communication.createTemplate"; // @ts-ignore - dynamically generated 538 538 return this._server.xrpc.method(nsid, cfg); 539 539 } 540 540 ··· 546 546 ToolsOzoneCommunicationListTemplates.HandlerOutput 547 547 >, 548 548 ) { 549 - const nsid = "tools.ozone.communication.listTemplates"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 549 + const nsid = "tools.ozone.communication.listTemplates"; // @ts-ignore - dynamically generated 550 550 return this._server.xrpc.method(nsid, cfg); 551 551 } 552 552 ··· 558 558 ToolsOzoneCommunicationDeleteTemplate.HandlerOutput 559 559 >, 560 560 ) { 561 - const nsid = "tools.ozone.communication.deleteTemplate"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 561 + const nsid = "tools.ozone.communication.deleteTemplate"; // @ts-ignore - dynamically generated 562 562 return this._server.xrpc.method(nsid, cfg); 563 563 } 564 564 } ··· 578 578 ToolsOzoneSetAddValues.HandlerOutput 579 579 >, 580 580 ) { 581 - const nsid = "tools.ozone.set.addValues"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 581 + const nsid = "tools.ozone.set.addValues"; // @ts-ignore - dynamically generated 582 582 return this._server.xrpc.method(nsid, cfg); 583 583 } 584 584 ··· 590 590 ToolsOzoneSetGetValues.HandlerOutput 591 591 >, 592 592 ) { 593 - const nsid = "tools.ozone.set.getValues"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 593 + const nsid = "tools.ozone.set.getValues"; // @ts-ignore - dynamically generated 594 594 return this._server.xrpc.method(nsid, cfg); 595 595 } 596 596 ··· 602 602 ToolsOzoneSetDeleteSet.HandlerOutput 603 603 >, 604 604 ) { 605 - const nsid = "tools.ozone.set.deleteSet"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 605 + const nsid = "tools.ozone.set.deleteSet"; // @ts-ignore - dynamically generated 606 606 return this._server.xrpc.method(nsid, cfg); 607 607 } 608 608 ··· 614 614 ToolsOzoneSetUpsertSet.HandlerOutput 615 615 >, 616 616 ) { 617 - const nsid = "tools.ozone.set.upsertSet"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 617 + const nsid = "tools.ozone.set.upsertSet"; // @ts-ignore - dynamically generated 618 618 return this._server.xrpc.method(nsid, cfg); 619 619 } 620 620 ··· 626 626 ToolsOzoneSetDeleteValues.HandlerOutput 627 627 >, 628 628 ) { 629 - const nsid = "tools.ozone.set.deleteValues"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 629 + const nsid = "tools.ozone.set.deleteValues"; // @ts-ignore - dynamically generated 630 630 return this._server.xrpc.method(nsid, cfg); 631 631 } 632 632 ··· 638 638 ToolsOzoneSetQuerySets.HandlerOutput 639 639 >, 640 640 ) { 641 - const nsid = "tools.ozone.set.querySets"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 641 + const nsid = "tools.ozone.set.querySets"; // @ts-ignore - dynamically generated 642 642 return this._server.xrpc.method(nsid, cfg); 643 643 } 644 644 } ··· 658 658 ToolsOzoneSettingListOptions.HandlerOutput 659 659 >, 660 660 ) { 661 - const nsid = "tools.ozone.setting.listOptions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 661 + const nsid = "tools.ozone.setting.listOptions"; // @ts-ignore - dynamically generated 662 662 return this._server.xrpc.method(nsid, cfg); 663 663 } 664 664 ··· 670 670 ToolsOzoneSettingRemoveOptions.HandlerOutput 671 671 >, 672 672 ) { 673 - const nsid = "tools.ozone.setting.removeOptions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 673 + const nsid = "tools.ozone.setting.removeOptions"; // @ts-ignore - dynamically generated 674 674 return this._server.xrpc.method(nsid, cfg); 675 675 } 676 676 ··· 682 682 ToolsOzoneSettingUpsertOption.HandlerOutput 683 683 >, 684 684 ) { 685 - const nsid = "tools.ozone.setting.upsertOption"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 685 + const nsid = "tools.ozone.setting.upsertOption"; // @ts-ignore - dynamically generated 686 686 return this._server.xrpc.method(nsid, cfg); 687 687 } 688 688 } ··· 702 702 ToolsOzoneModerationGetReporterStats.HandlerOutput 703 703 >, 704 704 ) { 705 - const nsid = "tools.ozone.moderation.getReporterStats"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 705 + const nsid = "tools.ozone.moderation.getReporterStats"; // @ts-ignore - dynamically generated 706 706 return this._server.xrpc.method(nsid, cfg); 707 707 } 708 708 ··· 714 714 ToolsOzoneModerationQueryStatuses.HandlerOutput 715 715 >, 716 716 ) { 717 - const nsid = "tools.ozone.moderation.queryStatuses"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 717 + const nsid = "tools.ozone.moderation.queryStatuses"; // @ts-ignore - dynamically generated 718 718 return this._server.xrpc.method(nsid, cfg); 719 719 } 720 720 ··· 726 726 ToolsOzoneModerationGetRepo.HandlerOutput 727 727 >, 728 728 ) { 729 - const nsid = "tools.ozone.moderation.getRepo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 729 + const nsid = "tools.ozone.moderation.getRepo"; // @ts-ignore - dynamically generated 730 730 return this._server.xrpc.method(nsid, cfg); 731 731 } 732 732 ··· 738 738 ToolsOzoneModerationGetRecords.HandlerOutput 739 739 >, 740 740 ) { 741 - const nsid = "tools.ozone.moderation.getRecords"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 741 + const nsid = "tools.ozone.moderation.getRecords"; // @ts-ignore - dynamically generated 742 742 return this._server.xrpc.method(nsid, cfg); 743 743 } 744 744 ··· 750 750 ToolsOzoneModerationGetEvent.HandlerOutput 751 751 >, 752 752 ) { 753 - const nsid = "tools.ozone.moderation.getEvent"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 753 + const nsid = "tools.ozone.moderation.getEvent"; // @ts-ignore - dynamically generated 754 754 return this._server.xrpc.method(nsid, cfg); 755 755 } 756 756 ··· 762 762 ToolsOzoneModerationQueryEvents.HandlerOutput 763 763 >, 764 764 ) { 765 - const nsid = "tools.ozone.moderation.queryEvents"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 765 + const nsid = "tools.ozone.moderation.queryEvents"; // @ts-ignore - dynamically generated 766 766 return this._server.xrpc.method(nsid, cfg); 767 767 } 768 768 ··· 774 774 ToolsOzoneModerationGetRecord.HandlerOutput 775 775 >, 776 776 ) { 777 - const nsid = "tools.ozone.moderation.getRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 777 + const nsid = "tools.ozone.moderation.getRecord"; // @ts-ignore - dynamically generated 778 778 return this._server.xrpc.method(nsid, cfg); 779 779 } 780 780 ··· 786 786 ToolsOzoneModerationEmitEvent.HandlerOutput 787 787 >, 788 788 ) { 789 - const nsid = "tools.ozone.moderation.emitEvent"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 789 + const nsid = "tools.ozone.moderation.emitEvent"; // @ts-ignore - dynamically generated 790 790 return this._server.xrpc.method(nsid, cfg); 791 791 } 792 792 ··· 798 798 ToolsOzoneModerationSearchRepos.HandlerOutput 799 799 >, 800 800 ) { 801 - const nsid = "tools.ozone.moderation.searchRepos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 801 + const nsid = "tools.ozone.moderation.searchRepos"; // @ts-ignore - dynamically generated 802 802 return this._server.xrpc.method(nsid, cfg); 803 803 } 804 804 ··· 810 810 ToolsOzoneModerationGetRepos.HandlerOutput 811 811 >, 812 812 ) { 813 - const nsid = "tools.ozone.moderation.getRepos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 813 + const nsid = "tools.ozone.moderation.getRepos"; // @ts-ignore - dynamically generated 814 814 return this._server.xrpc.method(nsid, cfg); 815 815 } 816 816 } ··· 866 866 AppBskyVideoUploadVideo.HandlerOutput 867 867 >, 868 868 ) { 869 - const nsid = "app.bsky.video.uploadVideo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 869 + const nsid = "app.bsky.video.uploadVideo"; // @ts-ignore - dynamically generated 870 870 return this._server.xrpc.method(nsid, cfg); 871 871 } 872 872 ··· 878 878 AppBskyVideoGetJobStatus.HandlerOutput 879 879 >, 880 880 ) { 881 - const nsid = "app.bsky.video.getJobStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 881 + const nsid = "app.bsky.video.getJobStatus"; // @ts-ignore - dynamically generated 882 882 return this._server.xrpc.method(nsid, cfg); 883 883 } 884 884 ··· 890 890 AppBskyVideoGetUploadLimits.HandlerOutput 891 891 >, 892 892 ) { 893 - const nsid = "app.bsky.video.getUploadLimits"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 893 + const nsid = "app.bsky.video.getUploadLimits"; // @ts-ignore - dynamically generated 894 894 return this._server.xrpc.method(nsid, cfg); 895 895 } 896 896 } ··· 918 918 AppBskyNotificationRegisterPush.HandlerOutput 919 919 >, 920 920 ) { 921 - const nsid = "app.bsky.notification.registerPush"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 921 + const nsid = "app.bsky.notification.registerPush"; // @ts-ignore - dynamically generated 922 922 return this._server.xrpc.method(nsid, cfg); 923 923 } 924 924 ··· 930 930 AppBskyNotificationPutPreferences.HandlerOutput 931 931 >, 932 932 ) { 933 - const nsid = "app.bsky.notification.putPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 933 + const nsid = "app.bsky.notification.putPreferences"; // @ts-ignore - dynamically generated 934 934 return this._server.xrpc.method(nsid, cfg); 935 935 } 936 936 ··· 942 942 AppBskyNotificationUpdateSeen.HandlerOutput 943 943 >, 944 944 ) { 945 - const nsid = "app.bsky.notification.updateSeen"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 945 + const nsid = "app.bsky.notification.updateSeen"; // @ts-ignore - dynamically generated 946 946 return this._server.xrpc.method(nsid, cfg); 947 947 } 948 948 ··· 954 954 AppBskyNotificationListNotifications.HandlerOutput 955 955 >, 956 956 ) { 957 - const nsid = "app.bsky.notification.listNotifications"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 957 + const nsid = "app.bsky.notification.listNotifications"; // @ts-ignore - dynamically generated 958 958 return this._server.xrpc.method(nsid, cfg); 959 959 } 960 960 ··· 966 966 AppBskyNotificationGetUnreadCount.HandlerOutput 967 967 >, 968 968 ) { 969 - const nsid = "app.bsky.notification.getUnreadCount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 969 + const nsid = "app.bsky.notification.getUnreadCount"; // @ts-ignore - dynamically generated 970 970 return this._server.xrpc.method(nsid, cfg); 971 971 } 972 972 } ··· 986 986 AppBskyUnspeccedSearchStarterPacksSkeleton.HandlerOutput 987 987 >, 988 988 ) { 989 - const nsid = "app.bsky.unspecced.searchStarterPacksSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 989 + const nsid = "app.bsky.unspecced.searchStarterPacksSkeleton"; // @ts-ignore - dynamically generated 990 990 return this._server.xrpc.method(nsid, cfg); 991 991 } 992 992 ··· 998 998 AppBskyUnspeccedSearchActorsSkeleton.HandlerOutput 999 999 >, 1000 1000 ) { 1001 - const nsid = "app.bsky.unspecced.searchActorsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1001 + const nsid = "app.bsky.unspecced.searchActorsSkeleton"; // @ts-ignore - dynamically generated 1002 1002 return this._server.xrpc.method(nsid, cfg); 1003 1003 } 1004 1004 ··· 1010 1010 AppBskyUnspeccedGetSuggestionsSkeleton.HandlerOutput 1011 1011 >, 1012 1012 ) { 1013 - const nsid = "app.bsky.unspecced.getSuggestionsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1013 + const nsid = "app.bsky.unspecced.getSuggestionsSkeleton"; // @ts-ignore - dynamically generated 1014 1014 return this._server.xrpc.method(nsid, cfg); 1015 1015 } 1016 1016 ··· 1022 1022 AppBskyUnspeccedSearchPostsSkeleton.HandlerOutput 1023 1023 >, 1024 1024 ) { 1025 - const nsid = "app.bsky.unspecced.searchPostsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1025 + const nsid = "app.bsky.unspecced.searchPostsSkeleton"; // @ts-ignore - dynamically generated 1026 1026 return this._server.xrpc.method(nsid, cfg); 1027 1027 } 1028 1028 ··· 1034 1034 AppBskyUnspeccedGetPopularFeedGenerators.HandlerOutput 1035 1035 >, 1036 1036 ) { 1037 - const nsid = "app.bsky.unspecced.getPopularFeedGenerators"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1037 + const nsid = "app.bsky.unspecced.getPopularFeedGenerators"; // @ts-ignore - dynamically generated 1038 1038 return this._server.xrpc.method(nsid, cfg); 1039 1039 } 1040 1040 ··· 1046 1046 AppBskyUnspeccedGetTrendingTopics.HandlerOutput 1047 1047 >, 1048 1048 ) { 1049 - const nsid = "app.bsky.unspecced.getTrendingTopics"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1049 + const nsid = "app.bsky.unspecced.getTrendingTopics"; // @ts-ignore - dynamically generated 1050 1050 return this._server.xrpc.method(nsid, cfg); 1051 1051 } 1052 1052 ··· 1058 1058 AppBskyUnspeccedGetTaggedSuggestions.HandlerOutput 1059 1059 >, 1060 1060 ) { 1061 - const nsid = "app.bsky.unspecced.getTaggedSuggestions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1061 + const nsid = "app.bsky.unspecced.getTaggedSuggestions"; // @ts-ignore - dynamically generated 1062 1062 return this._server.xrpc.method(nsid, cfg); 1063 1063 } 1064 1064 ··· 1070 1070 AppBskyUnspeccedGetConfig.HandlerOutput 1071 1071 >, 1072 1072 ) { 1073 - const nsid = "app.bsky.unspecced.getConfig"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1073 + const nsid = "app.bsky.unspecced.getConfig"; // @ts-ignore - dynamically generated 1074 1074 return this._server.xrpc.method(nsid, cfg); 1075 1075 } 1076 1076 } ··· 1090 1090 AppBskyGraphGetStarterPacks.HandlerOutput 1091 1091 >, 1092 1092 ) { 1093 - const nsid = "app.bsky.graph.getStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1093 + const nsid = "app.bsky.graph.getStarterPacks"; // @ts-ignore - dynamically generated 1094 1094 return this._server.xrpc.method(nsid, cfg); 1095 1095 } 1096 1096 ··· 1102 1102 AppBskyGraphGetSuggestedFollowsByActor.HandlerOutput 1103 1103 >, 1104 1104 ) { 1105 - const nsid = "app.bsky.graph.getSuggestedFollowsByActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1105 + const nsid = "app.bsky.graph.getSuggestedFollowsByActor"; // @ts-ignore - dynamically generated 1106 1106 return this._server.xrpc.method(nsid, cfg); 1107 1107 } 1108 1108 ··· 1114 1114 AppBskyGraphUnmuteActorList.HandlerOutput 1115 1115 >, 1116 1116 ) { 1117 - const nsid = "app.bsky.graph.unmuteActorList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1117 + const nsid = "app.bsky.graph.unmuteActorList"; // @ts-ignore - dynamically generated 1118 1118 return this._server.xrpc.method(nsid, cfg); 1119 1119 } 1120 1120 ··· 1126 1126 AppBskyGraphGetListBlocks.HandlerOutput 1127 1127 >, 1128 1128 ) { 1129 - const nsid = "app.bsky.graph.getListBlocks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1129 + const nsid = "app.bsky.graph.getListBlocks"; // @ts-ignore - dynamically generated 1130 1130 return this._server.xrpc.method(nsid, cfg); 1131 1131 } 1132 1132 ··· 1138 1138 AppBskyGraphGetStarterPack.HandlerOutput 1139 1139 >, 1140 1140 ) { 1141 - const nsid = "app.bsky.graph.getStarterPack"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1141 + const nsid = "app.bsky.graph.getStarterPack"; // @ts-ignore - dynamically generated 1142 1142 return this._server.xrpc.method(nsid, cfg); 1143 1143 } 1144 1144 ··· 1150 1150 AppBskyGraphMuteActorList.HandlerOutput 1151 1151 >, 1152 1152 ) { 1153 - const nsid = "app.bsky.graph.muteActorList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1153 + const nsid = "app.bsky.graph.muteActorList"; // @ts-ignore - dynamically generated 1154 1154 return this._server.xrpc.method(nsid, cfg); 1155 1155 } 1156 1156 ··· 1162 1162 AppBskyGraphMuteThread.HandlerOutput 1163 1163 >, 1164 1164 ) { 1165 - const nsid = "app.bsky.graph.muteThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1165 + const nsid = "app.bsky.graph.muteThread"; // @ts-ignore - dynamically generated 1166 1166 return this._server.xrpc.method(nsid, cfg); 1167 1167 } 1168 1168 ··· 1174 1174 AppBskyGraphSearchStarterPacks.HandlerOutput 1175 1175 >, 1176 1176 ) { 1177 - const nsid = "app.bsky.graph.searchStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1177 + const nsid = "app.bsky.graph.searchStarterPacks"; // @ts-ignore - dynamically generated 1178 1178 return this._server.xrpc.method(nsid, cfg); 1179 1179 } 1180 1180 ··· 1186 1186 AppBskyGraphGetActorStarterPacks.HandlerOutput 1187 1187 >, 1188 1188 ) { 1189 - const nsid = "app.bsky.graph.getActorStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1189 + const nsid = "app.bsky.graph.getActorStarterPacks"; // @ts-ignore - dynamically generated 1190 1190 return this._server.xrpc.method(nsid, cfg); 1191 1191 } 1192 1192 ··· 1198 1198 AppBskyGraphGetLists.HandlerOutput 1199 1199 >, 1200 1200 ) { 1201 - const nsid = "app.bsky.graph.getLists"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1201 + const nsid = "app.bsky.graph.getLists"; // @ts-ignore - dynamically generated 1202 1202 return this._server.xrpc.method(nsid, cfg); 1203 1203 } 1204 1204 ··· 1210 1210 AppBskyGraphGetFollowers.HandlerOutput 1211 1211 >, 1212 1212 ) { 1213 - const nsid = "app.bsky.graph.getFollowers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1213 + const nsid = "app.bsky.graph.getFollowers"; // @ts-ignore - dynamically generated 1214 1214 return this._server.xrpc.method(nsid, cfg); 1215 1215 } 1216 1216 ··· 1222 1222 AppBskyGraphUnmuteThread.HandlerOutput 1223 1223 >, 1224 1224 ) { 1225 - const nsid = "app.bsky.graph.unmuteThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1225 + const nsid = "app.bsky.graph.unmuteThread"; // @ts-ignore - dynamically generated 1226 1226 return this._server.xrpc.method(nsid, cfg); 1227 1227 } 1228 1228 ··· 1234 1234 AppBskyGraphMuteActor.HandlerOutput 1235 1235 >, 1236 1236 ) { 1237 - const nsid = "app.bsky.graph.muteActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1237 + const nsid = "app.bsky.graph.muteActor"; // @ts-ignore - dynamically generated 1238 1238 return this._server.xrpc.method(nsid, cfg); 1239 1239 } 1240 1240 ··· 1246 1246 AppBskyGraphGetMutes.HandlerOutput 1247 1247 >, 1248 1248 ) { 1249 - const nsid = "app.bsky.graph.getMutes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1249 + const nsid = "app.bsky.graph.getMutes"; // @ts-ignore - dynamically generated 1250 1250 return this._server.xrpc.method(nsid, cfg); 1251 1251 } 1252 1252 ··· 1258 1258 AppBskyGraphGetKnownFollowers.HandlerOutput 1259 1259 >, 1260 1260 ) { 1261 - const nsid = "app.bsky.graph.getKnownFollowers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1261 + const nsid = "app.bsky.graph.getKnownFollowers"; // @ts-ignore - dynamically generated 1262 1262 return this._server.xrpc.method(nsid, cfg); 1263 1263 } 1264 1264 ··· 1270 1270 AppBskyGraphGetListMutes.HandlerOutput 1271 1271 >, 1272 1272 ) { 1273 - const nsid = "app.bsky.graph.getListMutes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1273 + const nsid = "app.bsky.graph.getListMutes"; // @ts-ignore - dynamically generated 1274 1274 return this._server.xrpc.method(nsid, cfg); 1275 1275 } 1276 1276 ··· 1282 1282 AppBskyGraphGetFollows.HandlerOutput 1283 1283 >, 1284 1284 ) { 1285 - const nsid = "app.bsky.graph.getFollows"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1285 + const nsid = "app.bsky.graph.getFollows"; // @ts-ignore - dynamically generated 1286 1286 return this._server.xrpc.method(nsid, cfg); 1287 1287 } 1288 1288 ··· 1294 1294 AppBskyGraphGetBlocks.HandlerOutput 1295 1295 >, 1296 1296 ) { 1297 - const nsid = "app.bsky.graph.getBlocks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1297 + const nsid = "app.bsky.graph.getBlocks"; // @ts-ignore - dynamically generated 1298 1298 return this._server.xrpc.method(nsid, cfg); 1299 1299 } 1300 1300 ··· 1306 1306 AppBskyGraphGetRelationships.HandlerOutput 1307 1307 >, 1308 1308 ) { 1309 - const nsid = "app.bsky.graph.getRelationships"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1309 + const nsid = "app.bsky.graph.getRelationships"; // @ts-ignore - dynamically generated 1310 1310 return this._server.xrpc.method(nsid, cfg); 1311 1311 } 1312 1312 ··· 1318 1318 AppBskyGraphUnmuteActor.HandlerOutput 1319 1319 >, 1320 1320 ) { 1321 - const nsid = "app.bsky.graph.unmuteActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1321 + const nsid = "app.bsky.graph.unmuteActor"; // @ts-ignore - dynamically generated 1322 1322 return this._server.xrpc.method(nsid, cfg); 1323 1323 } 1324 1324 ··· 1330 1330 AppBskyGraphGetList.HandlerOutput 1331 1331 >, 1332 1332 ) { 1333 - const nsid = "app.bsky.graph.getList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1333 + const nsid = "app.bsky.graph.getList"; // @ts-ignore - dynamically generated 1334 1334 return this._server.xrpc.method(nsid, cfg); 1335 1335 } 1336 1336 } ··· 1350 1350 AppBskyFeedSendInteractions.HandlerOutput 1351 1351 >, 1352 1352 ) { 1353 - const nsid = "app.bsky.feed.sendInteractions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1353 + const nsid = "app.bsky.feed.sendInteractions"; // @ts-ignore - dynamically generated 1354 1354 return this._server.xrpc.method(nsid, cfg); 1355 1355 } 1356 1356 ··· 1362 1362 AppBskyFeedGetFeedGenerators.HandlerOutput 1363 1363 >, 1364 1364 ) { 1365 - const nsid = "app.bsky.feed.getFeedGenerators"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1365 + const nsid = "app.bsky.feed.getFeedGenerators"; // @ts-ignore - dynamically generated 1366 1366 return this._server.xrpc.method(nsid, cfg); 1367 1367 } 1368 1368 ··· 1374 1374 AppBskyFeedGetTimeline.HandlerOutput 1375 1375 >, 1376 1376 ) { 1377 - const nsid = "app.bsky.feed.getTimeline"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1377 + const nsid = "app.bsky.feed.getTimeline"; // @ts-ignore - dynamically generated 1378 1378 return this._server.xrpc.method(nsid, cfg); 1379 1379 } 1380 1380 ··· 1386 1386 AppBskyFeedGetFeedGenerator.HandlerOutput 1387 1387 >, 1388 1388 ) { 1389 - const nsid = "app.bsky.feed.getFeedGenerator"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1389 + const nsid = "app.bsky.feed.getFeedGenerator"; // @ts-ignore - dynamically generated 1390 1390 return this._server.xrpc.method(nsid, cfg); 1391 1391 } 1392 1392 ··· 1398 1398 AppBskyFeedGetAuthorFeed.HandlerOutput 1399 1399 >, 1400 1400 ) { 1401 - const nsid = "app.bsky.feed.getAuthorFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1401 + const nsid = "app.bsky.feed.getAuthorFeed"; // @ts-ignore - dynamically generated 1402 1402 return this._server.xrpc.method(nsid, cfg); 1403 1403 } 1404 1404 ··· 1410 1410 AppBskyFeedGetLikes.HandlerOutput 1411 1411 >, 1412 1412 ) { 1413 - const nsid = "app.bsky.feed.getLikes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1413 + const nsid = "app.bsky.feed.getLikes"; // @ts-ignore - dynamically generated 1414 1414 return this._server.xrpc.method(nsid, cfg); 1415 1415 } 1416 1416 ··· 1422 1422 AppBskyFeedGetPostThread.HandlerOutput 1423 1423 >, 1424 1424 ) { 1425 - const nsid = "app.bsky.feed.getPostThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1425 + const nsid = "app.bsky.feed.getPostThread"; // @ts-ignore - dynamically generated 1426 1426 return this._server.xrpc.method(nsid, cfg); 1427 1427 } 1428 1428 ··· 1434 1434 AppBskyFeedGetActorLikes.HandlerOutput 1435 1435 >, 1436 1436 ) { 1437 - const nsid = "app.bsky.feed.getActorLikes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1437 + const nsid = "app.bsky.feed.getActorLikes"; // @ts-ignore - dynamically generated 1438 1438 return this._server.xrpc.method(nsid, cfg); 1439 1439 } 1440 1440 ··· 1446 1446 AppBskyFeedGetRepostedBy.HandlerOutput 1447 1447 >, 1448 1448 ) { 1449 - const nsid = "app.bsky.feed.getRepostedBy"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1449 + const nsid = "app.bsky.feed.getRepostedBy"; // @ts-ignore - dynamically generated 1450 1450 return this._server.xrpc.method(nsid, cfg); 1451 1451 } 1452 1452 ··· 1458 1458 AppBskyFeedDescribeFeedGenerator.HandlerOutput 1459 1459 >, 1460 1460 ) { 1461 - const nsid = "app.bsky.feed.describeFeedGenerator"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1461 + const nsid = "app.bsky.feed.describeFeedGenerator"; // @ts-ignore - dynamically generated 1462 1462 return this._server.xrpc.method(nsid, cfg); 1463 1463 } 1464 1464 ··· 1470 1470 AppBskyFeedSearchPosts.HandlerOutput 1471 1471 >, 1472 1472 ) { 1473 - const nsid = "app.bsky.feed.searchPosts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1473 + const nsid = "app.bsky.feed.searchPosts"; // @ts-ignore - dynamically generated 1474 1474 return this._server.xrpc.method(nsid, cfg); 1475 1475 } 1476 1476 ··· 1482 1482 AppBskyFeedGetPosts.HandlerOutput 1483 1483 >, 1484 1484 ) { 1485 - const nsid = "app.bsky.feed.getPosts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1485 + const nsid = "app.bsky.feed.getPosts"; // @ts-ignore - dynamically generated 1486 1486 return this._server.xrpc.method(nsid, cfg); 1487 1487 } 1488 1488 ··· 1494 1494 AppBskyFeedGetFeed.HandlerOutput 1495 1495 >, 1496 1496 ) { 1497 - const nsid = "app.bsky.feed.getFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1497 + const nsid = "app.bsky.feed.getFeed"; // @ts-ignore - dynamically generated 1498 1498 return this._server.xrpc.method(nsid, cfg); 1499 1499 } 1500 1500 ··· 1506 1506 AppBskyFeedGetQuotes.HandlerOutput 1507 1507 >, 1508 1508 ) { 1509 - const nsid = "app.bsky.feed.getQuotes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1509 + const nsid = "app.bsky.feed.getQuotes"; // @ts-ignore - dynamically generated 1510 1510 return this._server.xrpc.method(nsid, cfg); 1511 1511 } 1512 1512 ··· 1518 1518 AppBskyFeedGetFeedSkeleton.HandlerOutput 1519 1519 >, 1520 1520 ) { 1521 - const nsid = "app.bsky.feed.getFeedSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1521 + const nsid = "app.bsky.feed.getFeedSkeleton"; // @ts-ignore - dynamically generated 1522 1522 return this._server.xrpc.method(nsid, cfg); 1523 1523 } 1524 1524 ··· 1530 1530 AppBskyFeedGetListFeed.HandlerOutput 1531 1531 >, 1532 1532 ) { 1533 - const nsid = "app.bsky.feed.getListFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1533 + const nsid = "app.bsky.feed.getListFeed"; // @ts-ignore - dynamically generated 1534 1534 return this._server.xrpc.method(nsid, cfg); 1535 1535 } 1536 1536 ··· 1542 1542 AppBskyFeedGetSuggestedFeeds.HandlerOutput 1543 1543 >, 1544 1544 ) { 1545 - const nsid = "app.bsky.feed.getSuggestedFeeds"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1545 + const nsid = "app.bsky.feed.getSuggestedFeeds"; // @ts-ignore - dynamically generated 1546 1546 return this._server.xrpc.method(nsid, cfg); 1547 1547 } 1548 1548 ··· 1554 1554 AppBskyFeedGetActorFeeds.HandlerOutput 1555 1555 >, 1556 1556 ) { 1557 - const nsid = "app.bsky.feed.getActorFeeds"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1557 + const nsid = "app.bsky.feed.getActorFeeds"; // @ts-ignore - dynamically generated 1558 1558 return this._server.xrpc.method(nsid, cfg); 1559 1559 } 1560 1560 } ··· 1582 1582 AppBskyActorSearchActorsTypeahead.HandlerOutput 1583 1583 >, 1584 1584 ) { 1585 - const nsid = "app.bsky.actor.searchActorsTypeahead"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1585 + const nsid = "app.bsky.actor.searchActorsTypeahead"; // @ts-ignore - dynamically generated 1586 1586 return this._server.xrpc.method(nsid, cfg); 1587 1587 } 1588 1588 ··· 1594 1594 AppBskyActorPutPreferences.HandlerOutput 1595 1595 >, 1596 1596 ) { 1597 - const nsid = "app.bsky.actor.putPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1597 + const nsid = "app.bsky.actor.putPreferences"; // @ts-ignore - dynamically generated 1598 1598 return this._server.xrpc.method(nsid, cfg); 1599 1599 } 1600 1600 ··· 1606 1606 AppBskyActorGetProfile.HandlerOutput 1607 1607 >, 1608 1608 ) { 1609 - const nsid = "app.bsky.actor.getProfile"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1609 + const nsid = "app.bsky.actor.getProfile"; // @ts-ignore - dynamically generated 1610 1610 return this._server.xrpc.method(nsid, cfg); 1611 1611 } 1612 1612 ··· 1618 1618 AppBskyActorGetSuggestions.HandlerOutput 1619 1619 >, 1620 1620 ) { 1621 - const nsid = "app.bsky.actor.getSuggestions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1621 + const nsid = "app.bsky.actor.getSuggestions"; // @ts-ignore - dynamically generated 1622 1622 return this._server.xrpc.method(nsid, cfg); 1623 1623 } 1624 1624 ··· 1630 1630 AppBskyActorSearchActors.HandlerOutput 1631 1631 >, 1632 1632 ) { 1633 - const nsid = "app.bsky.actor.searchActors"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1633 + const nsid = "app.bsky.actor.searchActors"; // @ts-ignore - dynamically generated 1634 1634 return this._server.xrpc.method(nsid, cfg); 1635 1635 } 1636 1636 ··· 1642 1642 AppBskyActorGetProfiles.HandlerOutput 1643 1643 >, 1644 1644 ) { 1645 - const nsid = "app.bsky.actor.getProfiles"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1645 + const nsid = "app.bsky.actor.getProfiles"; // @ts-ignore - dynamically generated 1646 1646 return this._server.xrpc.method(nsid, cfg); 1647 1647 } 1648 1648 ··· 1654 1654 AppBskyActorGetPreferences.HandlerOutput 1655 1655 >, 1656 1656 ) { 1657 - const nsid = "app.bsky.actor.getPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1657 + const nsid = "app.bsky.actor.getPreferences"; // @ts-ignore - dynamically generated 1658 1658 return this._server.xrpc.method(nsid, cfg); 1659 1659 } 1660 1660 } ··· 1674 1674 AppBskyLabelerGetServices.HandlerOutput 1675 1675 >, 1676 1676 ) { 1677 - const nsid = "app.bsky.labeler.getServices"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1677 + const nsid = "app.bsky.labeler.getServices"; // @ts-ignore - dynamically generated 1678 1678 return this._server.xrpc.method(nsid, cfg); 1679 1679 } 1680 1680 } ··· 1718 1718 ChatBskyConvoListConvos.HandlerOutput 1719 1719 >, 1720 1720 ) { 1721 - const nsid = "chat.bsky.convo.listConvos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1721 + const nsid = "chat.bsky.convo.listConvos"; // @ts-ignore - dynamically generated 1722 1722 return this._server.xrpc.method(nsid, cfg); 1723 1723 } 1724 1724 ··· 1730 1730 ChatBskyConvoUnmuteConvo.HandlerOutput 1731 1731 >, 1732 1732 ) { 1733 - const nsid = "chat.bsky.convo.unmuteConvo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1733 + const nsid = "chat.bsky.convo.unmuteConvo"; // @ts-ignore - dynamically generated 1734 1734 return this._server.xrpc.method(nsid, cfg); 1735 1735 } 1736 1736 ··· 1742 1742 ChatBskyConvoGetConvoAvailability.HandlerOutput 1743 1743 >, 1744 1744 ) { 1745 - const nsid = "chat.bsky.convo.getConvoAvailability"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1745 + const nsid = "chat.bsky.convo.getConvoAvailability"; // @ts-ignore - dynamically generated 1746 1746 return this._server.xrpc.method(nsid, cfg); 1747 1747 } 1748 1748 ··· 1754 1754 ChatBskyConvoGetLog.HandlerOutput 1755 1755 >, 1756 1756 ) { 1757 - const nsid = "chat.bsky.convo.getLog"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1757 + const nsid = "chat.bsky.convo.getLog"; // @ts-ignore - dynamically generated 1758 1758 return this._server.xrpc.method(nsid, cfg); 1759 1759 } 1760 1760 ··· 1766 1766 ChatBskyConvoSendMessage.HandlerOutput 1767 1767 >, 1768 1768 ) { 1769 - const nsid = "chat.bsky.convo.sendMessage"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1769 + const nsid = "chat.bsky.convo.sendMessage"; // @ts-ignore - dynamically generated 1770 1770 return this._server.xrpc.method(nsid, cfg); 1771 1771 } 1772 1772 ··· 1778 1778 ChatBskyConvoLeaveConvo.HandlerOutput 1779 1779 >, 1780 1780 ) { 1781 - const nsid = "chat.bsky.convo.leaveConvo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1781 + const nsid = "chat.bsky.convo.leaveConvo"; // @ts-ignore - dynamically generated 1782 1782 return this._server.xrpc.method(nsid, cfg); 1783 1783 } 1784 1784 ··· 1790 1790 ChatBskyConvoAcceptConvo.HandlerOutput 1791 1791 >, 1792 1792 ) { 1793 - const nsid = "chat.bsky.convo.acceptConvo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1793 + const nsid = "chat.bsky.convo.acceptConvo"; // @ts-ignore - dynamically generated 1794 1794 return this._server.xrpc.method(nsid, cfg); 1795 1795 } 1796 1796 ··· 1802 1802 ChatBskyConvoMuteConvo.HandlerOutput 1803 1803 >, 1804 1804 ) { 1805 - const nsid = "chat.bsky.convo.muteConvo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1805 + const nsid = "chat.bsky.convo.muteConvo"; // @ts-ignore - dynamically generated 1806 1806 return this._server.xrpc.method(nsid, cfg); 1807 1807 } 1808 1808 ··· 1814 1814 ChatBskyConvoDeleteMessageForSelf.HandlerOutput 1815 1815 >, 1816 1816 ) { 1817 - const nsid = "chat.bsky.convo.deleteMessageForSelf"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1817 + const nsid = "chat.bsky.convo.deleteMessageForSelf"; // @ts-ignore - dynamically generated 1818 1818 return this._server.xrpc.method(nsid, cfg); 1819 1819 } 1820 1820 ··· 1826 1826 ChatBskyConvoUpdateRead.HandlerOutput 1827 1827 >, 1828 1828 ) { 1829 - const nsid = "chat.bsky.convo.updateRead"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1829 + const nsid = "chat.bsky.convo.updateRead"; // @ts-ignore - dynamically generated 1830 1830 return this._server.xrpc.method(nsid, cfg); 1831 1831 } 1832 1832 ··· 1838 1838 ChatBskyConvoUpdateAllRead.HandlerOutput 1839 1839 >, 1840 1840 ) { 1841 - const nsid = "chat.bsky.convo.updateAllRead"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1841 + const nsid = "chat.bsky.convo.updateAllRead"; // @ts-ignore - dynamically generated 1842 1842 return this._server.xrpc.method(nsid, cfg); 1843 1843 } 1844 1844 ··· 1850 1850 ChatBskyConvoGetConvo.HandlerOutput 1851 1851 >, 1852 1852 ) { 1853 - const nsid = "chat.bsky.convo.getConvo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1853 + const nsid = "chat.bsky.convo.getConvo"; // @ts-ignore - dynamically generated 1854 1854 return this._server.xrpc.method(nsid, cfg); 1855 1855 } 1856 1856 ··· 1862 1862 ChatBskyConvoGetMessages.HandlerOutput 1863 1863 >, 1864 1864 ) { 1865 - const nsid = "chat.bsky.convo.getMessages"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1865 + const nsid = "chat.bsky.convo.getMessages"; // @ts-ignore - dynamically generated 1866 1866 return this._server.xrpc.method(nsid, cfg); 1867 1867 } 1868 1868 ··· 1874 1874 ChatBskyConvoGetConvoForMembers.HandlerOutput 1875 1875 >, 1876 1876 ) { 1877 - const nsid = "chat.bsky.convo.getConvoForMembers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1877 + const nsid = "chat.bsky.convo.getConvoForMembers"; // @ts-ignore - dynamically generated 1878 1878 return this._server.xrpc.method(nsid, cfg); 1879 1879 } 1880 1880 ··· 1886 1886 ChatBskyConvoSendMessageBatch.HandlerOutput 1887 1887 >, 1888 1888 ) { 1889 - const nsid = "chat.bsky.convo.sendMessageBatch"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1889 + const nsid = "chat.bsky.convo.sendMessageBatch"; // @ts-ignore - dynamically generated 1890 1890 return this._server.xrpc.method(nsid, cfg); 1891 1891 } 1892 1892 } ··· 1906 1906 ChatBskyActorExportAccountData.HandlerOutput 1907 1907 >, 1908 1908 ) { 1909 - const nsid = "chat.bsky.actor.exportAccountData"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1909 + const nsid = "chat.bsky.actor.exportAccountData"; // @ts-ignore - dynamically generated 1910 1910 return this._server.xrpc.method(nsid, cfg); 1911 1911 } 1912 1912 ··· 1918 1918 ChatBskyActorDeleteAccount.HandlerOutput 1919 1919 >, 1920 1920 ) { 1921 - const nsid = "chat.bsky.actor.deleteAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1921 + const nsid = "chat.bsky.actor.deleteAccount"; // @ts-ignore - dynamically generated 1922 1922 return this._server.xrpc.method(nsid, cfg); 1923 1923 } 1924 1924 } ··· 1938 1938 ChatBskyModerationGetActorMetadata.HandlerOutput 1939 1939 >, 1940 1940 ) { 1941 - const nsid = "chat.bsky.moderation.getActorMetadata"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1941 + const nsid = "chat.bsky.moderation.getActorMetadata"; // @ts-ignore - dynamically generated 1942 1942 return this._server.xrpc.method(nsid, cfg); 1943 1943 } 1944 1944 ··· 1950 1950 ChatBskyModerationGetMessageContext.HandlerOutput 1951 1951 >, 1952 1952 ) { 1953 - const nsid = "chat.bsky.moderation.getMessageContext"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1953 + const nsid = "chat.bsky.moderation.getMessageContext"; // @ts-ignore - dynamically generated 1954 1954 return this._server.xrpc.method(nsid, cfg); 1955 1955 } 1956 1956 ··· 1962 1962 ChatBskyModerationUpdateActorAccess.HandlerOutput 1963 1963 >, 1964 1964 ) { 1965 - const nsid = "chat.bsky.moderation.updateActorAccess"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 1965 + const nsid = "chat.bsky.moderation.updateActorAccess"; // @ts-ignore - dynamically generated 1966 1966 return this._server.xrpc.method(nsid, cfg); 1967 1967 } 1968 1968 } ··· 2020 2020 SoSprkVideoUploadVideo.HandlerOutput 2021 2021 >, 2022 2022 ) { 2023 - const nsid = "so.sprk.video.uploadVideo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2023 + const nsid = "so.sprk.video.uploadVideo"; // @ts-ignore - dynamically generated 2024 2024 return this._server.xrpc.method(nsid, cfg); 2025 2025 } 2026 2026 ··· 2032 2032 SoSprkVideoGetJobStatus.HandlerOutput 2033 2033 >, 2034 2034 ) { 2035 - const nsid = "so.sprk.video.getJobStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2035 + const nsid = "so.sprk.video.getJobStatus"; // @ts-ignore - dynamically generated 2036 2036 return this._server.xrpc.method(nsid, cfg); 2037 2037 } 2038 2038 ··· 2044 2044 SoSprkVideoGetUploadLimits.HandlerOutput 2045 2045 >, 2046 2046 ) { 2047 - const nsid = "so.sprk.video.getUploadLimits"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2047 + const nsid = "so.sprk.video.getUploadLimits"; // @ts-ignore - dynamically generated 2048 2048 return this._server.xrpc.method(nsid, cfg); 2049 2049 } 2050 2050 } ··· 2072 2072 SoSprkNotificationRegisterPush.HandlerOutput 2073 2073 >, 2074 2074 ) { 2075 - const nsid = "so.sprk.notification.registerPush"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2075 + const nsid = "so.sprk.notification.registerPush"; // @ts-ignore - dynamically generated 2076 2076 return this._server.xrpc.method(nsid, cfg); 2077 2077 } 2078 2078 ··· 2084 2084 SoSprkNotificationPutPreferences.HandlerOutput 2085 2085 >, 2086 2086 ) { 2087 - const nsid = "so.sprk.notification.putPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2087 + const nsid = "so.sprk.notification.putPreferences"; // @ts-ignore - dynamically generated 2088 2088 return this._server.xrpc.method(nsid, cfg); 2089 2089 } 2090 2090 ··· 2096 2096 SoSprkNotificationUpdateSeen.HandlerOutput 2097 2097 >, 2098 2098 ) { 2099 - const nsid = "so.sprk.notification.updateSeen"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2099 + const nsid = "so.sprk.notification.updateSeen"; // @ts-ignore - dynamically generated 2100 2100 return this._server.xrpc.method(nsid, cfg); 2101 2101 } 2102 2102 ··· 2108 2108 SoSprkNotificationListNotifications.HandlerOutput 2109 2109 >, 2110 2110 ) { 2111 - const nsid = "so.sprk.notification.listNotifications"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2111 + const nsid = "so.sprk.notification.listNotifications"; // @ts-ignore - dynamically generated 2112 2112 return this._server.xrpc.method(nsid, cfg); 2113 2113 } 2114 2114 ··· 2120 2120 SoSprkNotificationGetUnreadCount.HandlerOutput 2121 2121 >, 2122 2122 ) { 2123 - const nsid = "so.sprk.notification.getUnreadCount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2123 + const nsid = "so.sprk.notification.getUnreadCount"; // @ts-ignore - dynamically generated 2124 2124 return this._server.xrpc.method(nsid, cfg); 2125 2125 } 2126 2126 } ··· 2140 2140 SoSprkUnspeccedSearchStarterPacksSkeleton.HandlerOutput 2141 2141 >, 2142 2142 ) { 2143 - const nsid = "so.sprk.unspecced.searchStarterPacksSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2143 + const nsid = "so.sprk.unspecced.searchStarterPacksSkeleton"; // @ts-ignore - dynamically generated 2144 2144 return this._server.xrpc.method(nsid, cfg); 2145 2145 } 2146 2146 ··· 2152 2152 SoSprkUnspeccedSearchActorsSkeleton.HandlerOutput 2153 2153 >, 2154 2154 ) { 2155 - const nsid = "so.sprk.unspecced.searchActorsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2155 + const nsid = "so.sprk.unspecced.searchActorsSkeleton"; // @ts-ignore - dynamically generated 2156 2156 return this._server.xrpc.method(nsid, cfg); 2157 2157 } 2158 2158 ··· 2164 2164 SoSprkUnspeccedGetSuggestionsSkeleton.HandlerOutput 2165 2165 >, 2166 2166 ) { 2167 - const nsid = "so.sprk.unspecced.getSuggestionsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2167 + const nsid = "so.sprk.unspecced.getSuggestionsSkeleton"; // @ts-ignore - dynamically generated 2168 2168 return this._server.xrpc.method(nsid, cfg); 2169 2169 } 2170 2170 ··· 2176 2176 SoSprkUnspeccedSearchPostsSkeleton.HandlerOutput 2177 2177 >, 2178 2178 ) { 2179 - const nsid = "so.sprk.unspecced.searchPostsSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2179 + const nsid = "so.sprk.unspecced.searchPostsSkeleton"; // @ts-ignore - dynamically generated 2180 2180 return this._server.xrpc.method(nsid, cfg); 2181 2181 } 2182 2182 ··· 2188 2188 SoSprkUnspeccedGetPopularFeedGenerators.HandlerOutput 2189 2189 >, 2190 2190 ) { 2191 - const nsid = "so.sprk.unspecced.getPopularFeedGenerators"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2191 + const nsid = "so.sprk.unspecced.getPopularFeedGenerators"; // @ts-ignore - dynamically generated 2192 2192 return this._server.xrpc.method(nsid, cfg); 2193 2193 } 2194 2194 ··· 2200 2200 SoSprkUnspeccedGetTrendingTopics.HandlerOutput 2201 2201 >, 2202 2202 ) { 2203 - const nsid = "so.sprk.unspecced.getTrendingTopics"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2203 + const nsid = "so.sprk.unspecced.getTrendingTopics"; // @ts-ignore - dynamically generated 2204 2204 return this._server.xrpc.method(nsid, cfg); 2205 2205 } 2206 2206 ··· 2212 2212 SoSprkUnspeccedGetTaggedSuggestions.HandlerOutput 2213 2213 >, 2214 2214 ) { 2215 - const nsid = "so.sprk.unspecced.getTaggedSuggestions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2215 + const nsid = "so.sprk.unspecced.getTaggedSuggestions"; // @ts-ignore - dynamically generated 2216 2216 return this._server.xrpc.method(nsid, cfg); 2217 2217 } 2218 2218 ··· 2224 2224 SoSprkUnspeccedGetConfig.HandlerOutput 2225 2225 >, 2226 2226 ) { 2227 - const nsid = "so.sprk.unspecced.getConfig"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2227 + const nsid = "so.sprk.unspecced.getConfig"; // @ts-ignore - dynamically generated 2228 2228 return this._server.xrpc.method(nsid, cfg); 2229 2229 } 2230 2230 } ··· 2244 2244 SoSprkGraphGetStarterPacks.HandlerOutput 2245 2245 >, 2246 2246 ) { 2247 - const nsid = "so.sprk.graph.getStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2247 + const nsid = "so.sprk.graph.getStarterPacks"; // @ts-ignore - dynamically generated 2248 2248 return this._server.xrpc.method(nsid, cfg); 2249 2249 } 2250 2250 ··· 2256 2256 SoSprkGraphGetSuggestedFollowsByActor.HandlerOutput 2257 2257 >, 2258 2258 ) { 2259 - const nsid = "so.sprk.graph.getSuggestedFollowsByActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2259 + const nsid = "so.sprk.graph.getSuggestedFollowsByActor"; // @ts-ignore - dynamically generated 2260 2260 return this._server.xrpc.method(nsid, cfg); 2261 2261 } 2262 2262 ··· 2268 2268 SoSprkGraphUnmuteActorList.HandlerOutput 2269 2269 >, 2270 2270 ) { 2271 - const nsid = "so.sprk.graph.unmuteActorList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2271 + const nsid = "so.sprk.graph.unmuteActorList"; // @ts-ignore - dynamically generated 2272 2272 return this._server.xrpc.method(nsid, cfg); 2273 2273 } 2274 2274 ··· 2280 2280 SoSprkGraphGetListBlocks.HandlerOutput 2281 2281 >, 2282 2282 ) { 2283 - const nsid = "so.sprk.graph.getListBlocks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2283 + const nsid = "so.sprk.graph.getListBlocks"; // @ts-ignore - dynamically generated 2284 2284 return this._server.xrpc.method(nsid, cfg); 2285 2285 } 2286 2286 ··· 2292 2292 SoSprkGraphGetStarterPack.HandlerOutput 2293 2293 >, 2294 2294 ) { 2295 - const nsid = "so.sprk.graph.getStarterPack"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2295 + const nsid = "so.sprk.graph.getStarterPack"; // @ts-ignore - dynamically generated 2296 2296 return this._server.xrpc.method(nsid, cfg); 2297 2297 } 2298 2298 ··· 2304 2304 SoSprkGraphMuteActorList.HandlerOutput 2305 2305 >, 2306 2306 ) { 2307 - const nsid = "so.sprk.graph.muteActorList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2307 + const nsid = "so.sprk.graph.muteActorList"; // @ts-ignore - dynamically generated 2308 2308 return this._server.xrpc.method(nsid, cfg); 2309 2309 } 2310 2310 ··· 2316 2316 SoSprkGraphMuteThread.HandlerOutput 2317 2317 >, 2318 2318 ) { 2319 - const nsid = "so.sprk.graph.muteThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2319 + const nsid = "so.sprk.graph.muteThread"; // @ts-ignore - dynamically generated 2320 2320 return this._server.xrpc.method(nsid, cfg); 2321 2321 } 2322 2322 ··· 2328 2328 SoSprkGraphSearchStarterPacks.HandlerOutput 2329 2329 >, 2330 2330 ) { 2331 - const nsid = "so.sprk.graph.searchStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2331 + const nsid = "so.sprk.graph.searchStarterPacks"; // @ts-ignore - dynamically generated 2332 2332 return this._server.xrpc.method(nsid, cfg); 2333 2333 } 2334 2334 ··· 2340 2340 SoSprkGraphGetActorStarterPacks.HandlerOutput 2341 2341 >, 2342 2342 ) { 2343 - const nsid = "so.sprk.graph.getActorStarterPacks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2343 + const nsid = "so.sprk.graph.getActorStarterPacks"; // @ts-ignore - dynamically generated 2344 2344 return this._server.xrpc.method(nsid, cfg); 2345 2345 } 2346 2346 ··· 2352 2352 SoSprkGraphGetLists.HandlerOutput 2353 2353 >, 2354 2354 ) { 2355 - const nsid = "so.sprk.graph.getLists"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2355 + const nsid = "so.sprk.graph.getLists"; // @ts-ignore - dynamically generated 2356 2356 return this._server.xrpc.method(nsid, cfg); 2357 2357 } 2358 2358 ··· 2364 2364 SoSprkGraphGetFollowers.HandlerOutput 2365 2365 >, 2366 2366 ) { 2367 - const nsid = "so.sprk.graph.getFollowers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2367 + const nsid = "so.sprk.graph.getFollowers"; // @ts-ignore - dynamically generated 2368 2368 return this._server.xrpc.method(nsid, cfg); 2369 2369 } 2370 2370 ··· 2376 2376 SoSprkGraphUnmuteThread.HandlerOutput 2377 2377 >, 2378 2378 ) { 2379 - const nsid = "so.sprk.graph.unmuteThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2379 + const nsid = "so.sprk.graph.unmuteThread"; // @ts-ignore - dynamically generated 2380 2380 return this._server.xrpc.method(nsid, cfg); 2381 2381 } 2382 2382 ··· 2388 2388 SoSprkGraphMuteActor.HandlerOutput 2389 2389 >, 2390 2390 ) { 2391 - const nsid = "so.sprk.graph.muteActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2391 + const nsid = "so.sprk.graph.muteActor"; // @ts-ignore - dynamically generated 2392 2392 return this._server.xrpc.method(nsid, cfg); 2393 2393 } 2394 2394 ··· 2400 2400 SoSprkGraphGetMutes.HandlerOutput 2401 2401 >, 2402 2402 ) { 2403 - const nsid = "so.sprk.graph.getMutes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2403 + const nsid = "so.sprk.graph.getMutes"; // @ts-ignore - dynamically generated 2404 2404 return this._server.xrpc.method(nsid, cfg); 2405 2405 } 2406 2406 ··· 2412 2412 SoSprkGraphGetKnownFollowers.HandlerOutput 2413 2413 >, 2414 2414 ) { 2415 - const nsid = "so.sprk.graph.getKnownFollowers"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2415 + const nsid = "so.sprk.graph.getKnownFollowers"; // @ts-ignore - dynamically generated 2416 2416 return this._server.xrpc.method(nsid, cfg); 2417 2417 } 2418 2418 ··· 2424 2424 SoSprkGraphGetListMutes.HandlerOutput 2425 2425 >, 2426 2426 ) { 2427 - const nsid = "so.sprk.graph.getListMutes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2427 + const nsid = "so.sprk.graph.getListMutes"; // @ts-ignore - dynamically generated 2428 2428 return this._server.xrpc.method(nsid, cfg); 2429 2429 } 2430 2430 ··· 2436 2436 SoSprkGraphGetFollows.HandlerOutput 2437 2437 >, 2438 2438 ) { 2439 - const nsid = "so.sprk.graph.getFollows"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2439 + const nsid = "so.sprk.graph.getFollows"; // @ts-ignore - dynamically generated 2440 2440 return this._server.xrpc.method(nsid, cfg); 2441 2441 } 2442 2442 ··· 2448 2448 SoSprkGraphGetBlocks.HandlerOutput 2449 2449 >, 2450 2450 ) { 2451 - const nsid = "so.sprk.graph.getBlocks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2451 + const nsid = "so.sprk.graph.getBlocks"; // @ts-ignore - dynamically generated 2452 2452 return this._server.xrpc.method(nsid, cfg); 2453 2453 } 2454 2454 ··· 2460 2460 SoSprkGraphGetRelationships.HandlerOutput 2461 2461 >, 2462 2462 ) { 2463 - const nsid = "so.sprk.graph.getRelationships"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2463 + const nsid = "so.sprk.graph.getRelationships"; // @ts-ignore - dynamically generated 2464 2464 return this._server.xrpc.method(nsid, cfg); 2465 2465 } 2466 2466 ··· 2472 2472 SoSprkGraphUnmuteActor.HandlerOutput 2473 2473 >, 2474 2474 ) { 2475 - const nsid = "so.sprk.graph.unmuteActor"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2475 + const nsid = "so.sprk.graph.unmuteActor"; // @ts-ignore - dynamically generated 2476 2476 return this._server.xrpc.method(nsid, cfg); 2477 2477 } 2478 2478 ··· 2484 2484 SoSprkGraphGetList.HandlerOutput 2485 2485 >, 2486 2486 ) { 2487 - const nsid = "so.sprk.graph.getList"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2487 + const nsid = "so.sprk.graph.getList"; // @ts-ignore - dynamically generated 2488 2488 return this._server.xrpc.method(nsid, cfg); 2489 2489 } 2490 2490 } ··· 2504 2504 SoSprkFeedSendInteractions.HandlerOutput 2505 2505 >, 2506 2506 ) { 2507 - const nsid = "so.sprk.feed.sendInteractions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2507 + const nsid = "so.sprk.feed.sendInteractions"; // @ts-ignore - dynamically generated 2508 2508 return this._server.xrpc.method(nsid, cfg); 2509 2509 } 2510 2510 ··· 2516 2516 SoSprkFeedGetFeedGenerators.HandlerOutput 2517 2517 >, 2518 2518 ) { 2519 - const nsid = "so.sprk.feed.getFeedGenerators"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2519 + const nsid = "so.sprk.feed.getFeedGenerators"; // @ts-ignore - dynamically generated 2520 2520 return this._server.xrpc.method(nsid, cfg); 2521 2521 } 2522 2522 ··· 2528 2528 SoSprkFeedGetTimeline.HandlerOutput 2529 2529 >, 2530 2530 ) { 2531 - const nsid = "so.sprk.feed.getTimeline"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2531 + const nsid = "so.sprk.feed.getTimeline"; // @ts-ignore - dynamically generated 2532 2532 return this._server.xrpc.method(nsid, cfg); 2533 2533 } 2534 2534 ··· 2540 2540 SoSprkFeedGetFeedGenerator.HandlerOutput 2541 2541 >, 2542 2542 ) { 2543 - const nsid = "so.sprk.feed.getFeedGenerator"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2543 + const nsid = "so.sprk.feed.getFeedGenerator"; // @ts-ignore - dynamically generated 2544 2544 return this._server.xrpc.method(nsid, cfg); 2545 2545 } 2546 2546 ··· 2552 2552 SoSprkFeedGetAuthorFeed.HandlerOutput 2553 2553 >, 2554 2554 ) { 2555 - const nsid = "so.sprk.feed.getAuthorFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2555 + const nsid = "so.sprk.feed.getAuthorFeed"; // @ts-ignore - dynamically generated 2556 2556 return this._server.xrpc.method(nsid, cfg); 2557 2557 } 2558 2558 ··· 2564 2564 SoSprkFeedGetLikes.HandlerOutput 2565 2565 >, 2566 2566 ) { 2567 - const nsid = "so.sprk.feed.getLikes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2567 + const nsid = "so.sprk.feed.getLikes"; // @ts-ignore - dynamically generated 2568 2568 return this._server.xrpc.method(nsid, cfg); 2569 2569 } 2570 2570 ··· 2576 2576 SoSprkFeedGetPostThread.HandlerOutput 2577 2577 >, 2578 2578 ) { 2579 - const nsid = "so.sprk.feed.getPostThread"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2579 + const nsid = "so.sprk.feed.getPostThread"; // @ts-ignore - dynamically generated 2580 2580 return this._server.xrpc.method(nsid, cfg); 2581 2581 } 2582 2582 ··· 2588 2588 SoSprkFeedGetActorLikes.HandlerOutput 2589 2589 >, 2590 2590 ) { 2591 - const nsid = "so.sprk.feed.getActorLikes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2591 + const nsid = "so.sprk.feed.getActorLikes"; // @ts-ignore - dynamically generated 2592 2592 return this._server.xrpc.method(nsid, cfg); 2593 2593 } 2594 2594 ··· 2600 2600 SoSprkFeedGetRepostedBy.HandlerOutput 2601 2601 >, 2602 2602 ) { 2603 - const nsid = "so.sprk.feed.getRepostedBy"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2603 + const nsid = "so.sprk.feed.getRepostedBy"; // @ts-ignore - dynamically generated 2604 2604 return this._server.xrpc.method(nsid, cfg); 2605 2605 } 2606 2606 ··· 2612 2612 SoSprkFeedDescribeFeedGenerator.HandlerOutput 2613 2613 >, 2614 2614 ) { 2615 - const nsid = "so.sprk.feed.describeFeedGenerator"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2615 + const nsid = "so.sprk.feed.describeFeedGenerator"; // @ts-ignore - dynamically generated 2616 2616 return this._server.xrpc.method(nsid, cfg); 2617 2617 } 2618 2618 ··· 2624 2624 SoSprkFeedSearchPosts.HandlerOutput 2625 2625 >, 2626 2626 ) { 2627 - const nsid = "so.sprk.feed.searchPosts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2627 + const nsid = "so.sprk.feed.searchPosts"; // @ts-ignore - dynamically generated 2628 2628 return this._server.xrpc.method(nsid, cfg); 2629 2629 } 2630 2630 ··· 2636 2636 SoSprkFeedGetPosts.HandlerOutput 2637 2637 >, 2638 2638 ) { 2639 - const nsid = "so.sprk.feed.getPosts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2639 + const nsid = "so.sprk.feed.getPosts"; // @ts-ignore - dynamically generated 2640 2640 return this._server.xrpc.method(nsid, cfg); 2641 2641 } 2642 2642 ··· 2648 2648 SoSprkFeedGetFeed.HandlerOutput 2649 2649 >, 2650 2650 ) { 2651 - const nsid = "so.sprk.feed.getFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2651 + const nsid = "so.sprk.feed.getFeed"; // @ts-ignore - dynamically generated 2652 2652 return this._server.xrpc.method(nsid, cfg); 2653 2653 } 2654 2654 ··· 2660 2660 SoSprkFeedGetStories.HandlerOutput 2661 2661 >, 2662 2662 ) { 2663 - const nsid = "so.sprk.feed.getStories"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2663 + const nsid = "so.sprk.feed.getStories"; // @ts-ignore - dynamically generated 2664 2664 return this._server.xrpc.method(nsid, cfg); 2665 2665 } 2666 2666 ··· 2672 2672 SoSprkFeedGetQuotes.HandlerOutput 2673 2673 >, 2674 2674 ) { 2675 - const nsid = "so.sprk.feed.getQuotes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2675 + const nsid = "so.sprk.feed.getQuotes"; // @ts-ignore - dynamically generated 2676 2676 return this._server.xrpc.method(nsid, cfg); 2677 2677 } 2678 2678 ··· 2684 2684 SoSprkFeedGetStoriesTimeline.HandlerOutput 2685 2685 >, 2686 2686 ) { 2687 - const nsid = "so.sprk.feed.getStoriesTimeline"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2687 + const nsid = "so.sprk.feed.getStoriesTimeline"; // @ts-ignore - dynamically generated 2688 2688 return this._server.xrpc.method(nsid, cfg); 2689 2689 } 2690 2690 ··· 2696 2696 SoSprkFeedGetFeedSkeleton.HandlerOutput 2697 2697 >, 2698 2698 ) { 2699 - const nsid = "so.sprk.feed.getFeedSkeleton"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2699 + const nsid = "so.sprk.feed.getFeedSkeleton"; // @ts-ignore - dynamically generated 2700 2700 return this._server.xrpc.method(nsid, cfg); 2701 2701 } 2702 2702 ··· 2708 2708 SoSprkFeedGetListFeed.HandlerOutput 2709 2709 >, 2710 2710 ) { 2711 - const nsid = "so.sprk.feed.getListFeed"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2711 + const nsid = "so.sprk.feed.getListFeed"; // @ts-ignore - dynamically generated 2712 2712 return this._server.xrpc.method(nsid, cfg); 2713 2713 } 2714 2714 ··· 2720 2720 SoSprkFeedGetSuggestedFeeds.HandlerOutput 2721 2721 >, 2722 2722 ) { 2723 - const nsid = "so.sprk.feed.getSuggestedFeeds"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2723 + const nsid = "so.sprk.feed.getSuggestedFeeds"; // @ts-ignore - dynamically generated 2724 2724 return this._server.xrpc.method(nsid, cfg); 2725 2725 } 2726 2726 ··· 2732 2732 SoSprkFeedGetActorFeeds.HandlerOutput 2733 2733 >, 2734 2734 ) { 2735 - const nsid = "so.sprk.feed.getActorFeeds"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2735 + const nsid = "so.sprk.feed.getActorFeeds"; // @ts-ignore - dynamically generated 2736 2736 return this._server.xrpc.method(nsid, cfg); 2737 2737 } 2738 2738 } ··· 2760 2760 SoSprkSoundGetActorAudios.HandlerOutput 2761 2761 >, 2762 2762 ) { 2763 - const nsid = "so.sprk.sound.getActorAudios"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2763 + const nsid = "so.sprk.sound.getActorAudios"; // @ts-ignore - dynamically generated 2764 2764 return this._server.xrpc.method(nsid, cfg); 2765 2765 } 2766 2766 ··· 2772 2772 SoSprkSoundGetAudioPosts.HandlerOutput 2773 2773 >, 2774 2774 ) { 2775 - const nsid = "so.sprk.sound.getAudioPosts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2775 + const nsid = "so.sprk.sound.getAudioPosts"; // @ts-ignore - dynamically generated 2776 2776 return this._server.xrpc.method(nsid, cfg); 2777 2777 } 2778 2778 ··· 2784 2784 SoSprkSoundGetAudios.HandlerOutput 2785 2785 >, 2786 2786 ) { 2787 - const nsid = "so.sprk.sound.getAudios"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2787 + const nsid = "so.sprk.sound.getAudios"; // @ts-ignore - dynamically generated 2788 2788 return this._server.xrpc.method(nsid, cfg); 2789 2789 } 2790 2790 ··· 2796 2796 SoSprkSoundGetTrendingAudios.HandlerOutput 2797 2797 >, 2798 2798 ) { 2799 - const nsid = "so.sprk.sound.getTrendingAudios"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2799 + const nsid = "so.sprk.sound.getTrendingAudios"; // @ts-ignore - dynamically generated 2800 2800 return this._server.xrpc.method(nsid, cfg); 2801 2801 } 2802 2802 } ··· 2816 2816 SoSprkActorSearchActorsTypeahead.HandlerOutput 2817 2817 >, 2818 2818 ) { 2819 - const nsid = "so.sprk.actor.searchActorsTypeahead"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2819 + const nsid = "so.sprk.actor.searchActorsTypeahead"; // @ts-ignore - dynamically generated 2820 2820 return this._server.xrpc.method(nsid, cfg); 2821 2821 } 2822 2822 ··· 2828 2828 SoSprkActorPutPreferences.HandlerOutput 2829 2829 >, 2830 2830 ) { 2831 - const nsid = "so.sprk.actor.putPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2831 + const nsid = "so.sprk.actor.putPreferences"; // @ts-ignore - dynamically generated 2832 2832 return this._server.xrpc.method(nsid, cfg); 2833 2833 } 2834 2834 ··· 2840 2840 SoSprkActorGetProfile.HandlerOutput 2841 2841 >, 2842 2842 ) { 2843 - const nsid = "so.sprk.actor.getProfile"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2843 + const nsid = "so.sprk.actor.getProfile"; // @ts-ignore - dynamically generated 2844 2844 return this._server.xrpc.method(nsid, cfg); 2845 2845 } 2846 2846 ··· 2852 2852 SoSprkActorGetSuggestions.HandlerOutput 2853 2853 >, 2854 2854 ) { 2855 - const nsid = "so.sprk.actor.getSuggestions"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2855 + const nsid = "so.sprk.actor.getSuggestions"; // @ts-ignore - dynamically generated 2856 2856 return this._server.xrpc.method(nsid, cfg); 2857 2857 } 2858 2858 ··· 2864 2864 SoSprkActorSearchActors.HandlerOutput 2865 2865 >, 2866 2866 ) { 2867 - const nsid = "so.sprk.actor.searchActors"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2867 + const nsid = "so.sprk.actor.searchActors"; // @ts-ignore - dynamically generated 2868 2868 return this._server.xrpc.method(nsid, cfg); 2869 2869 } 2870 2870 ··· 2876 2876 SoSprkActorGetProfiles.HandlerOutput 2877 2877 >, 2878 2878 ) { 2879 - const nsid = "so.sprk.actor.getProfiles"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2879 + const nsid = "so.sprk.actor.getProfiles"; // @ts-ignore - dynamically generated 2880 2880 return this._server.xrpc.method(nsid, cfg); 2881 2881 } 2882 2882 ··· 2888 2888 SoSprkActorGetPreferences.HandlerOutput 2889 2889 >, 2890 2890 ) { 2891 - const nsid = "so.sprk.actor.getPreferences"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2891 + const nsid = "so.sprk.actor.getPreferences"; // @ts-ignore - dynamically generated 2892 2892 return this._server.xrpc.method(nsid, cfg); 2893 2893 } 2894 2894 } ··· 2908 2908 SoSprkLabelerGetServices.HandlerOutput 2909 2909 >, 2910 2910 ) { 2911 - const nsid = "so.sprk.labeler.getServices"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2911 + const nsid = "so.sprk.labeler.getServices"; // @ts-ignore - dynamically generated 2912 2912 return this._server.xrpc.method(nsid, cfg); 2913 2913 } 2914 2914 } ··· 2964 2964 ComAtprotoTempAddReservedHandle.HandlerOutput 2965 2965 >, 2966 2966 ) { 2967 - const nsid = "com.atproto.temp.addReservedHandle"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2967 + const nsid = "com.atproto.temp.addReservedHandle"; // @ts-ignore - dynamically generated 2968 2968 return this._server.xrpc.method(nsid, cfg); 2969 2969 } 2970 2970 ··· 2976 2976 ComAtprotoTempCheckSignupQueue.HandlerOutput 2977 2977 >, 2978 2978 ) { 2979 - const nsid = "com.atproto.temp.checkSignupQueue"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2979 + const nsid = "com.atproto.temp.checkSignupQueue"; // @ts-ignore - dynamically generated 2980 2980 return this._server.xrpc.method(nsid, cfg); 2981 2981 } 2982 2982 ··· 2988 2988 ComAtprotoTempRequestPhoneVerification.HandlerOutput 2989 2989 >, 2990 2990 ) { 2991 - const nsid = "com.atproto.temp.requestPhoneVerification"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 2991 + const nsid = "com.atproto.temp.requestPhoneVerification"; // @ts-ignore - dynamically generated 2992 2992 return this._server.xrpc.method(nsid, cfg); 2993 2993 } 2994 2994 ··· 3000 3000 ComAtprotoTempFetchLabels.HandlerOutput 3001 3001 >, 3002 3002 ) { 3003 - const nsid = "com.atproto.temp.fetchLabels"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3003 + const nsid = "com.atproto.temp.fetchLabels"; // @ts-ignore - dynamically generated 3004 3004 return this._server.xrpc.method(nsid, cfg); 3005 3005 } 3006 3006 } ··· 3020 3020 ComAtprotoIdentityUpdateHandle.HandlerOutput 3021 3021 >, 3022 3022 ) { 3023 - const nsid = "com.atproto.identity.updateHandle"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3023 + const nsid = "com.atproto.identity.updateHandle"; // @ts-ignore - dynamically generated 3024 3024 return this._server.xrpc.method(nsid, cfg); 3025 3025 } 3026 3026 ··· 3032 3032 ComAtprotoIdentitySignPlcOperation.HandlerOutput 3033 3033 >, 3034 3034 ) { 3035 - const nsid = "com.atproto.identity.signPlcOperation"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3035 + const nsid = "com.atproto.identity.signPlcOperation"; // @ts-ignore - dynamically generated 3036 3036 return this._server.xrpc.method(nsid, cfg); 3037 3037 } 3038 3038 ··· 3044 3044 ComAtprotoIdentitySubmitPlcOperation.HandlerOutput 3045 3045 >, 3046 3046 ) { 3047 - const nsid = "com.atproto.identity.submitPlcOperation"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3047 + const nsid = "com.atproto.identity.submitPlcOperation"; // @ts-ignore - dynamically generated 3048 3048 return this._server.xrpc.method(nsid, cfg); 3049 3049 } 3050 3050 ··· 3056 3056 ComAtprotoIdentityResolveHandle.HandlerOutput 3057 3057 >, 3058 3058 ) { 3059 - const nsid = "com.atproto.identity.resolveHandle"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3059 + const nsid = "com.atproto.identity.resolveHandle"; // @ts-ignore - dynamically generated 3060 3060 return this._server.xrpc.method(nsid, cfg); 3061 3061 } 3062 3062 ··· 3068 3068 ComAtprotoIdentityRequestPlcOperationSignature.HandlerOutput 3069 3069 >, 3070 3070 ) { 3071 - const nsid = "com.atproto.identity.requestPlcOperationSignature"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3071 + const nsid = "com.atproto.identity.requestPlcOperationSignature"; // @ts-ignore - dynamically generated 3072 3072 return this._server.xrpc.method(nsid, cfg); 3073 3073 } 3074 3074 ··· 3080 3080 ComAtprotoIdentityGetRecommendedDidCredentials.HandlerOutput 3081 3081 >, 3082 3082 ) { 3083 - const nsid = "com.atproto.identity.getRecommendedDidCredentials"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3083 + const nsid = "com.atproto.identity.getRecommendedDidCredentials"; // @ts-ignore - dynamically generated 3084 3084 return this._server.xrpc.method(nsid, cfg); 3085 3085 } 3086 3086 } ··· 3100 3100 ComAtprotoAdminUpdateAccountEmail.HandlerOutput 3101 3101 >, 3102 3102 ) { 3103 - const nsid = "com.atproto.admin.updateAccountEmail"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3103 + const nsid = "com.atproto.admin.updateAccountEmail"; // @ts-ignore - dynamically generated 3104 3104 return this._server.xrpc.method(nsid, cfg); 3105 3105 } 3106 3106 ··· 3112 3112 ComAtprotoAdminGetAccountInfo.HandlerOutput 3113 3113 >, 3114 3114 ) { 3115 - const nsid = "com.atproto.admin.getAccountInfo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3115 + const nsid = "com.atproto.admin.getAccountInfo"; // @ts-ignore - dynamically generated 3116 3116 return this._server.xrpc.method(nsid, cfg); 3117 3117 } 3118 3118 ··· 3124 3124 ComAtprotoAdminGetSubjectStatus.HandlerOutput 3125 3125 >, 3126 3126 ) { 3127 - const nsid = "com.atproto.admin.getSubjectStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3127 + const nsid = "com.atproto.admin.getSubjectStatus"; // @ts-ignore - dynamically generated 3128 3128 return this._server.xrpc.method(nsid, cfg); 3129 3129 } 3130 3130 ··· 3136 3136 ComAtprotoAdminSearchAccounts.HandlerOutput 3137 3137 >, 3138 3138 ) { 3139 - const nsid = "com.atproto.admin.searchAccounts"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3139 + const nsid = "com.atproto.admin.searchAccounts"; // @ts-ignore - dynamically generated 3140 3140 return this._server.xrpc.method(nsid, cfg); 3141 3141 } 3142 3142 ··· 3148 3148 ComAtprotoAdminUpdateAccountPassword.HandlerOutput 3149 3149 >, 3150 3150 ) { 3151 - const nsid = "com.atproto.admin.updateAccountPassword"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3151 + const nsid = "com.atproto.admin.updateAccountPassword"; // @ts-ignore - dynamically generated 3152 3152 return this._server.xrpc.method(nsid, cfg); 3153 3153 } 3154 3154 ··· 3160 3160 ComAtprotoAdminUpdateAccountHandle.HandlerOutput 3161 3161 >, 3162 3162 ) { 3163 - const nsid = "com.atproto.admin.updateAccountHandle"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3163 + const nsid = "com.atproto.admin.updateAccountHandle"; // @ts-ignore - dynamically generated 3164 3164 return this._server.xrpc.method(nsid, cfg); 3165 3165 } 3166 3166 ··· 3172 3172 ComAtprotoAdminGetInviteCodes.HandlerOutput 3173 3173 >, 3174 3174 ) { 3175 - const nsid = "com.atproto.admin.getInviteCodes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3175 + const nsid = "com.atproto.admin.getInviteCodes"; // @ts-ignore - dynamically generated 3176 3176 return this._server.xrpc.method(nsid, cfg); 3177 3177 } 3178 3178 ··· 3184 3184 ComAtprotoAdminEnableAccountInvites.HandlerOutput 3185 3185 >, 3186 3186 ) { 3187 - const nsid = "com.atproto.admin.enableAccountInvites"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3187 + const nsid = "com.atproto.admin.enableAccountInvites"; // @ts-ignore - dynamically generated 3188 3188 return this._server.xrpc.method(nsid, cfg); 3189 3189 } 3190 3190 ··· 3196 3196 ComAtprotoAdminDisableAccountInvites.HandlerOutput 3197 3197 >, 3198 3198 ) { 3199 - const nsid = "com.atproto.admin.disableAccountInvites"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3199 + const nsid = "com.atproto.admin.disableAccountInvites"; // @ts-ignore - dynamically generated 3200 3200 return this._server.xrpc.method(nsid, cfg); 3201 3201 } 3202 3202 ··· 3208 3208 ComAtprotoAdminDisableInviteCodes.HandlerOutput 3209 3209 >, 3210 3210 ) { 3211 - const nsid = "com.atproto.admin.disableInviteCodes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3211 + const nsid = "com.atproto.admin.disableInviteCodes"; // @ts-ignore - dynamically generated 3212 3212 return this._server.xrpc.method(nsid, cfg); 3213 3213 } 3214 3214 ··· 3220 3220 ComAtprotoAdminUpdateSubjectStatus.HandlerOutput 3221 3221 >, 3222 3222 ) { 3223 - const nsid = "com.atproto.admin.updateSubjectStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3223 + const nsid = "com.atproto.admin.updateSubjectStatus"; // @ts-ignore - dynamically generated 3224 3224 return this._server.xrpc.method(nsid, cfg); 3225 3225 } 3226 3226 ··· 3232 3232 ComAtprotoAdminSendEmail.HandlerOutput 3233 3233 >, 3234 3234 ) { 3235 - const nsid = "com.atproto.admin.sendEmail"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3235 + const nsid = "com.atproto.admin.sendEmail"; // @ts-ignore - dynamically generated 3236 3236 return this._server.xrpc.method(nsid, cfg); 3237 3237 } 3238 3238 ··· 3244 3244 ComAtprotoAdminGetAccountInfos.HandlerOutput 3245 3245 >, 3246 3246 ) { 3247 - const nsid = "com.atproto.admin.getAccountInfos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3247 + const nsid = "com.atproto.admin.getAccountInfos"; // @ts-ignore - dynamically generated 3248 3248 return this._server.xrpc.method(nsid, cfg); 3249 3249 } 3250 3250 ··· 3256 3256 ComAtprotoAdminDeleteAccount.HandlerOutput 3257 3257 >, 3258 3258 ) { 3259 - const nsid = "com.atproto.admin.deleteAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3259 + const nsid = "com.atproto.admin.deleteAccount"; // @ts-ignore - dynamically generated 3260 3260 return this._server.xrpc.method(nsid, cfg); 3261 3261 } 3262 3262 } ··· 3275 3275 ComAtprotoLabelSubscribeLabels.HandlerOutput 3276 3276 >, 3277 3277 ) { 3278 - const nsid = "com.atproto.label.subscribeLabels"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3278 + const nsid = "com.atproto.label.subscribeLabels"; // @ts-ignore - dynamically generated 3279 3279 return this._server.xrpc.streamMethod(nsid, cfg); 3280 3280 } 3281 3281 ··· 3287 3287 ComAtprotoLabelQueryLabels.HandlerOutput 3288 3288 >, 3289 3289 ) { 3290 - const nsid = "com.atproto.label.queryLabels"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3290 + const nsid = "com.atproto.label.queryLabels"; // @ts-ignore - dynamically generated 3291 3291 return this._server.xrpc.method(nsid, cfg); 3292 3292 } 3293 3293 } ··· 3307 3307 ComAtprotoServerRequestEmailConfirmation.HandlerOutput 3308 3308 >, 3309 3309 ) { 3310 - const nsid = "com.atproto.server.requestEmailConfirmation"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3310 + const nsid = "com.atproto.server.requestEmailConfirmation"; // @ts-ignore - dynamically generated 3311 3311 return this._server.xrpc.method(nsid, cfg); 3312 3312 } 3313 3313 ··· 3319 3319 ComAtprotoServerReserveSigningKey.HandlerOutput 3320 3320 >, 3321 3321 ) { 3322 - const nsid = "com.atproto.server.reserveSigningKey"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3322 + const nsid = "com.atproto.server.reserveSigningKey"; // @ts-ignore - dynamically generated 3323 3323 return this._server.xrpc.method(nsid, cfg); 3324 3324 } 3325 3325 ··· 3331 3331 ComAtprotoServerGetServiceAuth.HandlerOutput 3332 3332 >, 3333 3333 ) { 3334 - const nsid = "com.atproto.server.getServiceAuth"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3334 + const nsid = "com.atproto.server.getServiceAuth"; // @ts-ignore - dynamically generated 3335 3335 return this._server.xrpc.method(nsid, cfg); 3336 3336 } 3337 3337 ··· 3343 3343 ComAtprotoServerGetAccountInviteCodes.HandlerOutput 3344 3344 >, 3345 3345 ) { 3346 - const nsid = "com.atproto.server.getAccountInviteCodes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3346 + const nsid = "com.atproto.server.getAccountInviteCodes"; // @ts-ignore - dynamically generated 3347 3347 return this._server.xrpc.method(nsid, cfg); 3348 3348 } 3349 3349 ··· 3355 3355 ComAtprotoServerCreateSession.HandlerOutput 3356 3356 >, 3357 3357 ) { 3358 - const nsid = "com.atproto.server.createSession"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3358 + const nsid = "com.atproto.server.createSession"; // @ts-ignore - dynamically generated 3359 3359 return this._server.xrpc.method(nsid, cfg); 3360 3360 } 3361 3361 ··· 3367 3367 ComAtprotoServerListAppPasswords.HandlerOutput 3368 3368 >, 3369 3369 ) { 3370 - const nsid = "com.atproto.server.listAppPasswords"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3370 + const nsid = "com.atproto.server.listAppPasswords"; // @ts-ignore - dynamically generated 3371 3371 return this._server.xrpc.method(nsid, cfg); 3372 3372 } 3373 3373 ··· 3379 3379 ComAtprotoServerCreateInviteCodes.HandlerOutput 3380 3380 >, 3381 3381 ) { 3382 - const nsid = "com.atproto.server.createInviteCodes"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3382 + const nsid = "com.atproto.server.createInviteCodes"; // @ts-ignore - dynamically generated 3383 3383 return this._server.xrpc.method(nsid, cfg); 3384 3384 } 3385 3385 ··· 3391 3391 ComAtprotoServerDeleteSession.HandlerOutput 3392 3392 >, 3393 3393 ) { 3394 - const nsid = "com.atproto.server.deleteSession"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3394 + const nsid = "com.atproto.server.deleteSession"; // @ts-ignore - dynamically generated 3395 3395 return this._server.xrpc.method(nsid, cfg); 3396 3396 } 3397 3397 ··· 3403 3403 ComAtprotoServerRevokeAppPassword.HandlerOutput 3404 3404 >, 3405 3405 ) { 3406 - const nsid = "com.atproto.server.revokeAppPassword"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3406 + const nsid = "com.atproto.server.revokeAppPassword"; // @ts-ignore - dynamically generated 3407 3407 return this._server.xrpc.method(nsid, cfg); 3408 3408 } 3409 3409 ··· 3415 3415 ComAtprotoServerCreateAppPassword.HandlerOutput 3416 3416 >, 3417 3417 ) { 3418 - const nsid = "com.atproto.server.createAppPassword"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3418 + const nsid = "com.atproto.server.createAppPassword"; // @ts-ignore - dynamically generated 3419 3419 return this._server.xrpc.method(nsid, cfg); 3420 3420 } 3421 3421 ··· 3427 3427 ComAtprotoServerActivateAccount.HandlerOutput 3428 3428 >, 3429 3429 ) { 3430 - const nsid = "com.atproto.server.activateAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3430 + const nsid = "com.atproto.server.activateAccount"; // @ts-ignore - dynamically generated 3431 3431 return this._server.xrpc.method(nsid, cfg); 3432 3432 } 3433 3433 ··· 3439 3439 ComAtprotoServerDescribeServer.HandlerOutput 3440 3440 >, 3441 3441 ) { 3442 - const nsid = "com.atproto.server.describeServer"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3442 + const nsid = "com.atproto.server.describeServer"; // @ts-ignore - dynamically generated 3443 3443 return this._server.xrpc.method(nsid, cfg); 3444 3444 } 3445 3445 ··· 3451 3451 ComAtprotoServerConfirmEmail.HandlerOutput 3452 3452 >, 3453 3453 ) { 3454 - const nsid = "com.atproto.server.confirmEmail"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3454 + const nsid = "com.atproto.server.confirmEmail"; // @ts-ignore - dynamically generated 3455 3455 return this._server.xrpc.method(nsid, cfg); 3456 3456 } 3457 3457 ··· 3463 3463 ComAtprotoServerGetSession.HandlerOutput 3464 3464 >, 3465 3465 ) { 3466 - const nsid = "com.atproto.server.getSession"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3466 + const nsid = "com.atproto.server.getSession"; // @ts-ignore - dynamically generated 3467 3467 return this._server.xrpc.method(nsid, cfg); 3468 3468 } 3469 3469 ··· 3475 3475 ComAtprotoServerRefreshSession.HandlerOutput 3476 3476 >, 3477 3477 ) { 3478 - const nsid = "com.atproto.server.refreshSession"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3478 + const nsid = "com.atproto.server.refreshSession"; // @ts-ignore - dynamically generated 3479 3479 return this._server.xrpc.method(nsid, cfg); 3480 3480 } 3481 3481 ··· 3487 3487 ComAtprotoServerDeactivateAccount.HandlerOutput 3488 3488 >, 3489 3489 ) { 3490 - const nsid = "com.atproto.server.deactivateAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3490 + const nsid = "com.atproto.server.deactivateAccount"; // @ts-ignore - dynamically generated 3491 3491 return this._server.xrpc.method(nsid, cfg); 3492 3492 } 3493 3493 ··· 3499 3499 ComAtprotoServerUpdateEmail.HandlerOutput 3500 3500 >, 3501 3501 ) { 3502 - const nsid = "com.atproto.server.updateEmail"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3502 + const nsid = "com.atproto.server.updateEmail"; // @ts-ignore - dynamically generated 3503 3503 return this._server.xrpc.method(nsid, cfg); 3504 3504 } 3505 3505 ··· 3511 3511 ComAtprotoServerResetPassword.HandlerOutput 3512 3512 >, 3513 3513 ) { 3514 - const nsid = "com.atproto.server.resetPassword"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3514 + const nsid = "com.atproto.server.resetPassword"; // @ts-ignore - dynamically generated 3515 3515 return this._server.xrpc.method(nsid, cfg); 3516 3516 } 3517 3517 ··· 3523 3523 ComAtprotoServerCheckAccountStatus.HandlerOutput 3524 3524 >, 3525 3525 ) { 3526 - const nsid = "com.atproto.server.checkAccountStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3526 + const nsid = "com.atproto.server.checkAccountStatus"; // @ts-ignore - dynamically generated 3527 3527 return this._server.xrpc.method(nsid, cfg); 3528 3528 } 3529 3529 ··· 3535 3535 ComAtprotoServerRequestEmailUpdate.HandlerOutput 3536 3536 >, 3537 3537 ) { 3538 - const nsid = "com.atproto.server.requestEmailUpdate"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3538 + const nsid = "com.atproto.server.requestEmailUpdate"; // @ts-ignore - dynamically generated 3539 3539 return this._server.xrpc.method(nsid, cfg); 3540 3540 } 3541 3541 ··· 3547 3547 ComAtprotoServerRequestPasswordReset.HandlerOutput 3548 3548 >, 3549 3549 ) { 3550 - const nsid = "com.atproto.server.requestPasswordReset"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3550 + const nsid = "com.atproto.server.requestPasswordReset"; // @ts-ignore - dynamically generated 3551 3551 return this._server.xrpc.method(nsid, cfg); 3552 3552 } 3553 3553 ··· 3559 3559 ComAtprotoServerRequestAccountDelete.HandlerOutput 3560 3560 >, 3561 3561 ) { 3562 - const nsid = "com.atproto.server.requestAccountDelete"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3562 + const nsid = "com.atproto.server.requestAccountDelete"; // @ts-ignore - dynamically generated 3563 3563 return this._server.xrpc.method(nsid, cfg); 3564 3564 } 3565 3565 ··· 3571 3571 ComAtprotoServerCreateAccount.HandlerOutput 3572 3572 >, 3573 3573 ) { 3574 - const nsid = "com.atproto.server.createAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3574 + const nsid = "com.atproto.server.createAccount"; // @ts-ignore - dynamically generated 3575 3575 return this._server.xrpc.method(nsid, cfg); 3576 3576 } 3577 3577 ··· 3583 3583 ComAtprotoServerDeleteAccount.HandlerOutput 3584 3584 >, 3585 3585 ) { 3586 - const nsid = "com.atproto.server.deleteAccount"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3586 + const nsid = "com.atproto.server.deleteAccount"; // @ts-ignore - dynamically generated 3587 3587 return this._server.xrpc.method(nsid, cfg); 3588 3588 } 3589 3589 ··· 3595 3595 ComAtprotoServerCreateInviteCode.HandlerOutput 3596 3596 >, 3597 3597 ) { 3598 - const nsid = "com.atproto.server.createInviteCode"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3598 + const nsid = "com.atproto.server.createInviteCode"; // @ts-ignore - dynamically generated 3599 3599 return this._server.xrpc.method(nsid, cfg); 3600 3600 } 3601 3601 } ··· 3623 3623 ComAtprotoSyncGetHead.HandlerOutput 3624 3624 >, 3625 3625 ) { 3626 - const nsid = "com.atproto.sync.getHead"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3626 + const nsid = "com.atproto.sync.getHead"; // @ts-ignore - dynamically generated 3627 3627 return this._server.xrpc.method(nsid, cfg); 3628 3628 } 3629 3629 ··· 3635 3635 ComAtprotoSyncGetBlob.HandlerOutput 3636 3636 >, 3637 3637 ) { 3638 - const nsid = "com.atproto.sync.getBlob"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3638 + const nsid = "com.atproto.sync.getBlob"; // @ts-ignore - dynamically generated 3639 3639 return this._server.xrpc.method(nsid, cfg); 3640 3640 } 3641 3641 ··· 3647 3647 ComAtprotoSyncGetRepo.HandlerOutput 3648 3648 >, 3649 3649 ) { 3650 - const nsid = "com.atproto.sync.getRepo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3650 + const nsid = "com.atproto.sync.getRepo"; // @ts-ignore - dynamically generated 3651 3651 return this._server.xrpc.method(nsid, cfg); 3652 3652 } 3653 3653 ··· 3659 3659 ComAtprotoSyncNotifyOfUpdate.HandlerOutput 3660 3660 >, 3661 3661 ) { 3662 - const nsid = "com.atproto.sync.notifyOfUpdate"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3662 + const nsid = "com.atproto.sync.notifyOfUpdate"; // @ts-ignore - dynamically generated 3663 3663 return this._server.xrpc.method(nsid, cfg); 3664 3664 } 3665 3665 ··· 3671 3671 ComAtprotoSyncRequestCrawl.HandlerOutput 3672 3672 >, 3673 3673 ) { 3674 - const nsid = "com.atproto.sync.requestCrawl"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3674 + const nsid = "com.atproto.sync.requestCrawl"; // @ts-ignore - dynamically generated 3675 3675 return this._server.xrpc.method(nsid, cfg); 3676 3676 } 3677 3677 ··· 3683 3683 ComAtprotoSyncListBlobs.HandlerOutput 3684 3684 >, 3685 3685 ) { 3686 - const nsid = "com.atproto.sync.listBlobs"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3686 + const nsid = "com.atproto.sync.listBlobs"; // @ts-ignore - dynamically generated 3687 3687 return this._server.xrpc.method(nsid, cfg); 3688 3688 } 3689 3689 ··· 3695 3695 ComAtprotoSyncGetLatestCommit.HandlerOutput 3696 3696 >, 3697 3697 ) { 3698 - const nsid = "com.atproto.sync.getLatestCommit"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3698 + const nsid = "com.atproto.sync.getLatestCommit"; // @ts-ignore - dynamically generated 3699 3699 return this._server.xrpc.method(nsid, cfg); 3700 3700 } 3701 3701 ··· 3706 3706 ComAtprotoSyncSubscribeRepos.HandlerOutput 3707 3707 >, 3708 3708 ) { 3709 - const nsid = "com.atproto.sync.subscribeRepos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3709 + const nsid = "com.atproto.sync.subscribeRepos"; // @ts-ignore - dynamically generated 3710 3710 return this._server.xrpc.streamMethod(nsid, cfg); 3711 3711 } 3712 3712 ··· 3718 3718 ComAtprotoSyncGetRepoStatus.HandlerOutput 3719 3719 >, 3720 3720 ) { 3721 - const nsid = "com.atproto.sync.getRepoStatus"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3721 + const nsid = "com.atproto.sync.getRepoStatus"; // @ts-ignore - dynamically generated 3722 3722 return this._server.xrpc.method(nsid, cfg); 3723 3723 } 3724 3724 ··· 3730 3730 ComAtprotoSyncGetRecord.HandlerOutput 3731 3731 >, 3732 3732 ) { 3733 - const nsid = "com.atproto.sync.getRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3733 + const nsid = "com.atproto.sync.getRecord"; // @ts-ignore - dynamically generated 3734 3734 return this._server.xrpc.method(nsid, cfg); 3735 3735 } 3736 3736 ··· 3742 3742 ComAtprotoSyncListRepos.HandlerOutput 3743 3743 >, 3744 3744 ) { 3745 - const nsid = "com.atproto.sync.listRepos"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3745 + const nsid = "com.atproto.sync.listRepos"; // @ts-ignore - dynamically generated 3746 3746 return this._server.xrpc.method(nsid, cfg); 3747 3747 } 3748 3748 ··· 3754 3754 ComAtprotoSyncGetBlocks.HandlerOutput 3755 3755 >, 3756 3756 ) { 3757 - const nsid = "com.atproto.sync.getBlocks"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3757 + const nsid = "com.atproto.sync.getBlocks"; // @ts-ignore - dynamically generated 3758 3758 return this._server.xrpc.method(nsid, cfg); 3759 3759 } 3760 3760 ··· 3766 3766 ComAtprotoSyncListReposByCollection.HandlerOutput 3767 3767 >, 3768 3768 ) { 3769 - const nsid = "com.atproto.sync.listReposByCollection"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3769 + const nsid = "com.atproto.sync.listReposByCollection"; // @ts-ignore - dynamically generated 3770 3770 return this._server.xrpc.method(nsid, cfg); 3771 3771 } 3772 3772 ··· 3778 3778 ComAtprotoSyncGetCheckout.HandlerOutput 3779 3779 >, 3780 3780 ) { 3781 - const nsid = "com.atproto.sync.getCheckout"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3781 + const nsid = "com.atproto.sync.getCheckout"; // @ts-ignore - dynamically generated 3782 3782 return this._server.xrpc.method(nsid, cfg); 3783 3783 } 3784 3784 } ··· 3798 3798 ComAtprotoRepoListMissingBlobs.HandlerOutput 3799 3799 >, 3800 3800 ) { 3801 - const nsid = "com.atproto.repo.listMissingBlobs"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3801 + const nsid = "com.atproto.repo.listMissingBlobs"; // @ts-ignore - dynamically generated 3802 3802 return this._server.xrpc.method(nsid, cfg); 3803 3803 } 3804 3804 ··· 3810 3810 ComAtprotoRepoCreateRecord.HandlerOutput 3811 3811 >, 3812 3812 ) { 3813 - const nsid = "com.atproto.repo.createRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3813 + const nsid = "com.atproto.repo.createRecord"; // @ts-ignore - dynamically generated 3814 3814 return this._server.xrpc.method(nsid, cfg); 3815 3815 } 3816 3816 ··· 3822 3822 ComAtprotoRepoDeleteRecord.HandlerOutput 3823 3823 >, 3824 3824 ) { 3825 - const nsid = "com.atproto.repo.deleteRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3825 + const nsid = "com.atproto.repo.deleteRecord"; // @ts-ignore - dynamically generated 3826 3826 return this._server.xrpc.method(nsid, cfg); 3827 3827 } 3828 3828 ··· 3834 3834 ComAtprotoRepoPutRecord.HandlerOutput 3835 3835 >, 3836 3836 ) { 3837 - const nsid = "com.atproto.repo.putRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3837 + const nsid = "com.atproto.repo.putRecord"; // @ts-ignore - dynamically generated 3838 3838 return this._server.xrpc.method(nsid, cfg); 3839 3839 } 3840 3840 ··· 3846 3846 ComAtprotoRepoUploadBlob.HandlerOutput 3847 3847 >, 3848 3848 ) { 3849 - const nsid = "com.atproto.repo.uploadBlob"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3849 + const nsid = "com.atproto.repo.uploadBlob"; // @ts-ignore - dynamically generated 3850 3850 return this._server.xrpc.method(nsid, cfg); 3851 3851 } 3852 3852 ··· 3858 3858 ComAtprotoRepoImportRepo.HandlerOutput 3859 3859 >, 3860 3860 ) { 3861 - const nsid = "com.atproto.repo.importRepo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3861 + const nsid = "com.atproto.repo.importRepo"; // @ts-ignore - dynamically generated 3862 3862 return this._server.xrpc.method(nsid, cfg); 3863 3863 } 3864 3864 ··· 3870 3870 ComAtprotoRepoDescribeRepo.HandlerOutput 3871 3871 >, 3872 3872 ) { 3873 - const nsid = "com.atproto.repo.describeRepo"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3873 + const nsid = "com.atproto.repo.describeRepo"; // @ts-ignore - dynamically generated 3874 3874 return this._server.xrpc.method(nsid, cfg); 3875 3875 } 3876 3876 ··· 3882 3882 ComAtprotoRepoGetRecord.HandlerOutput 3883 3883 >, 3884 3884 ) { 3885 - const nsid = "com.atproto.repo.getRecord"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3885 + const nsid = "com.atproto.repo.getRecord"; // @ts-ignore - dynamically generated 3886 3886 return this._server.xrpc.method(nsid, cfg); 3887 3887 } 3888 3888 ··· 3894 3894 ComAtprotoRepoApplyWrites.HandlerOutput 3895 3895 >, 3896 3896 ) { 3897 - const nsid = "com.atproto.repo.applyWrites"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3897 + const nsid = "com.atproto.repo.applyWrites"; // @ts-ignore - dynamically generated 3898 3898 return this._server.xrpc.method(nsid, cfg); 3899 3899 } 3900 3900 ··· 3906 3906 ComAtprotoRepoListRecords.HandlerOutput 3907 3907 >, 3908 3908 ) { 3909 - const nsid = "com.atproto.repo.listRecords"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3909 + const nsid = "com.atproto.repo.listRecords"; // @ts-ignore - dynamically generated 3910 3910 return this._server.xrpc.method(nsid, cfg); 3911 3911 } 3912 3912 } ··· 3926 3926 ComAtprotoModerationCreateReport.HandlerOutput 3927 3927 >, 3928 3928 ) { 3929 - const nsid = "com.atproto.moderation.createReport"; // @ts-ignore - userType.nsid is dynamically generated and TypeScript can't infer its type 3929 + const nsid = "com.atproto.moderation.createReport"; // @ts-ignore - dynamically generated 3930 3930 return this._server.xrpc.method(nsid, cfg); 3931 3931 } 3932 3932 }
+18 -2
lex/lexicons.ts
··· 6 6 Lexicons, 7 7 ValidationError, 8 8 type ValidationResult, 9 - } from "@atproto/lexicon"; 9 + } from "@atp/lexicon"; 10 10 import { is$typed, maybe$typed } from "./util.ts"; 11 11 12 12 export const schemaDict = { ··· 7523 7523 "type": "string", 7524 7524 "format": "datetime", 7525 7525 }, 7526 + "via": { 7527 + "type": "ref", 7528 + "ref": "lex:com.atproto.repo.strongRef", 7529 + }, 7526 7530 }, 7527 7531 }, 7528 7532 }, ··· 7619 7623 "createdAt": { 7620 7624 "type": "string", 7621 7625 "format": "datetime", 7626 + }, 7627 + "via": { 7628 + "type": "ref", 7629 + "ref": "lex:com.atproto.repo.strongRef", 7622 7630 }, 7623 7631 }, 7624 7632 }, ··· 15160 15168 "main": { 15161 15169 "type": "record", 15162 15170 "description": 15163 - "Record declaring a 'like' of a piece of subject content. Duplicate likes from the same author to the same subject will be ignored by the AppView.", 15171 + "Record declaring a 'like' of a piece of subject content.", 15164 15172 "key": "tid", 15165 15173 "record": { 15166 15174 "type": "object", ··· 15176 15184 "createdAt": { 15177 15185 "type": "string", 15178 15186 "format": "datetime", 15187 + }, 15188 + "via": { 15189 + "type": "ref", 15190 + "ref": "lex:com.atproto.repo.strongRef", 15179 15191 }, 15180 15192 }, 15181 15193 }, ··· 15273 15285 "createdAt": { 15274 15286 "type": "string", 15275 15287 "format": "datetime", 15288 + }, 15289 + "via": { 15290 + "type": "ref", 15291 + "ref": "lex:com.atproto.repo.strongRef", 15276 15292 }, 15277 15293 }, 15278 15294 },
+1 -1
lex/types/app/bsky/actor/profile.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1 -1
lex/types/app/bsky/embed/external.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7
+1 -1
lex/types/app/bsky/embed/images.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import type * as AppBskyEmbedDefs from "./defs.ts";
+1 -1
lex/types/app/bsky/embed/video.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import type * as AppBskyEmbedDefs from "./defs.ts";
+1 -1
lex/types/app/bsky/feed/generator.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1
lex/types/app/bsky/feed/like.ts
··· 12 12 $type: "app.bsky.feed.like"; 13 13 subject: ComAtprotoRepoStrongRef.Main; 14 14 createdAt: string; 15 + via?: ComAtprotoRepoStrongRef.Main; 15 16 [k: string]: unknown; 16 17 } 17 18
+1
lex/types/app/bsky/feed/repost.ts
··· 12 12 $type: "app.bsky.feed.repost"; 13 13 subject: ComAtprotoRepoStrongRef.Main; 14 14 createdAt: string; 15 + via?: ComAtprotoRepoStrongRef.Main; 15 16 [k: string]: unknown; 16 17 } 17 18
+1 -1
lex/types/app/bsky/graph/list.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1 -1
lex/types/app/bsky/video/defs.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7
+1 -1
lex/types/com/atproto/label/subscribeLabels.ts
··· 4 4 import { validate as _validate } from "../../../../lexicons.ts"; 5 5 import { is$typed as _is$typed } from "../../../../util.ts"; 6 6 import { type $Typed } from "../../../../util.ts"; 7 - import { ErrorFrame } from "@sprk/xrpc-server"; 7 + import { ErrorFrame } from "@atp/xrpc-server"; 8 8 import type * as ComAtprotoLabelDefs from "./defs.ts"; 9 9 10 10 const is$typed = _is$typed, validate = _validate;
+1 -1
lex/types/com/atproto/repo/uploadBlob.ts
··· 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 4 import stream from "node:stream"; 5 - import { BlobRef } from "@atproto/lexicon"; 5 + import { BlobRef } from "@atp/lexicon"; 6 6 7 7 export type QueryParams = globalThis.Record<PropertyKey, never>; 8 8 export type InputSchema = string | Uint8Array | Blob;
+1 -1
lex/types/com/atproto/sync/subscribeRepos.ts
··· 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts"; 8 - import { ErrorFrame } from "@sprk/xrpc-server"; 8 + import { ErrorFrame } from "@atp/xrpc-server"; 9 9 10 10 const is$typed = _is$typed, validate = _validate; 11 11 const id = "com.atproto.sync.subscribeRepos";
+1 -1
lex/types/so/sprk/actor/profile.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1 -1
lex/types/so/sprk/embed/images.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import type * as SoSprkEmbedDefs from "./defs.ts";
+1 -1
lex/types/so/sprk/embed/video.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import type * as SoSprkEmbedDefs from "./defs.ts";
+1 -1
lex/types/so/sprk/feed/generator.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1
lex/types/so/sprk/feed/like.ts
··· 12 12 $type: "so.sprk.feed.like"; 13 13 subject: ComAtprotoRepoStrongRef.Main; 14 14 createdAt: string; 15 + via?: ComAtprotoRepoStrongRef.Main; 15 16 [k: string]: unknown; 16 17 } 17 18
+1 -1
lex/types/so/sprk/feed/music.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1
lex/types/so/sprk/feed/repost.ts
··· 12 12 $type: "so.sprk.feed.repost"; 13 13 subject: ComAtprotoRepoStrongRef.Main; 14 14 createdAt: string; 15 + via?: ComAtprotoRepoStrongRef.Main; 15 16 [k: string]: unknown; 16 17 } 17 18
+1 -1
lex/types/so/sprk/graph/list.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1 -1
lex/types/so/sprk/sound/audio.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import { type $Typed } from "../../../../util.ts";
+1 -1
lex/types/so/sprk/video/defs.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { BlobRef } from "@atproto/lexicon"; 4 + import { BlobRef } from "@atp/lexicon"; 5 5 import { validate as _validate } from "../../../../lexicons.ts"; 6 6 import { is$typed as _is$typed } from "../../../../util.ts"; 7 7 import type * as SoSprkSoundDefs from "../sound/defs.ts";
+1 -1
lex/util.ts
··· 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 4 5 - import { type ValidationResult } from "@atproto/lexicon"; 5 + import { type ValidationResult } from "@atp/lexicon"; 6 6 7 7 export type OmitKey<T, K extends keyof T> = { 8 8 [K2 in keyof T as K2 extends K ? never : K2]: T[K2];
+2 -1
lexicons/app/bsky/feed/like.json
··· 11 11 "required": ["subject", "createdAt"], 12 12 "properties": { 13 13 "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 14 - "createdAt": { "type": "string", "format": "datetime" } 14 + "createdAt": { "type": "string", "format": "datetime" }, 15 + "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 15 16 } 16 17 } 17 18 }
+2 -1
lexicons/app/bsky/feed/repost.json
··· 11 11 "required": ["subject", "createdAt"], 12 12 "properties": { 13 13 "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 14 - "createdAt": { "type": "string", "format": "datetime" } 14 + "createdAt": { "type": "string", "format": "datetime" }, 15 + "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 15 16 } 16 17 } 17 18 }
+3 -2
lexicons/so/sprk/feed/like.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "record", 7 - "description": "Record declaring a 'like' of a piece of subject content. Duplicate likes from the same author to the same subject will be ignored by the AppView.", 7 + "description": "Record declaring a 'like' of a piece of subject content.", 8 8 "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 11 "required": ["subject", "createdAt"], 12 12 "properties": { 13 13 "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 14 - "createdAt": { "type": "string", "format": "datetime" } 14 + "createdAt": { "type": "string", "format": "datetime" }, 15 + "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 15 16 } 16 17 } 17 18 }
+2 -1
lexicons/so/sprk/feed/repost.json
··· 11 11 "required": ["subject", "createdAt"], 12 12 "properties": { 13 13 "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, 14 - "createdAt": { "type": "string", "format": "datetime" } 14 + "createdAt": { "type": "string", "format": "datetime" }, 15 + "via": { "type": "ref", "ref": "com.atproto.repo.strongRef" } 15 16 } 16 17 } 17 18 }
+29 -10
main.ts
··· 2 2 import { cors } from "hono/cors"; 3 3 import { HTTPException } from "hono/http-exception"; 4 4 import { logger } from "hono/logger"; 5 - import { Database } from "./data-plane/server/index.ts"; 5 + import { Database } from "./data-plane/db/index.ts"; 6 6 import { env } from "./utils/env.ts"; 7 - import { createAuthVerifier } from "./services/auth-verifier.ts"; 7 + import { createAuthVerifier } from "./auth-verifier.ts"; 8 8 import API from "./api/index.ts"; 9 9 import { createServer } from "./lex/index.ts"; 10 10 import { ··· 15 15 import wellKnownRouter from "./api/well-known.ts"; 16 16 import { TakedownService } from "./services/takedown.ts"; 17 17 import { BidirectionalResolver } from "./utils/id-resolver.ts"; 18 - import { DidResolver } from "@atproto/identity"; 19 - import { AuthVerifier } from "./services/auth-verifier.ts"; 20 - import { AuthRequiredError } from "@sprk/xrpc-server"; 21 - import { RepoSubscription } from "./data-plane/server/subscription.ts"; 18 + import { DidResolver } from "@atp/identity"; 19 + import { AuthVerifier } from "./auth-verifier.ts"; 20 + import { AuthRequiredError } from "@atp/xrpc-server"; 21 + import { RepoSubscription } from "./data-plane/subscription.ts"; 22 + import { DataPlane } from "./data-plane/index.ts"; 22 23 import { configure, getConsoleSink, getLogger, Logger } from "@logtape/logtape"; 23 24 import { getPrettyFormatter } from "@logtape/pretty"; 25 + import { Hydrator } from "./hydration/index.ts"; 26 + import { Views } from "./views/index.ts"; 24 27 25 28 await configure({ 26 29 sinks: { ··· 42 45 43 46 export type AppContext = { 44 47 db: Database; 48 + dataplane: DataPlane; 49 + hydrator: Hydrator; 50 + views: Views; 45 51 logger: Logger; 46 52 resolver: BidirectionalResolver; 47 53 serviceDid: string; ··· 148 154 throw new Error("Failed to connect to database during startup"); 149 155 } 150 156 151 - // Read cursor from database 152 - const savedCursor = await db.getCursorState(); 157 + // Read cursor from database (skip in dev environment) 158 + const savedCursor = env.NODE_ENV === "development" 159 + ? null 160 + : await db.getCursorState(); 153 161 const startCursor = savedCursor !== null ? savedCursor : undefined; 154 162 155 - appLogger.info("Database cursor loaded", { cursor: startCursor }); 163 + appLogger.info("Database cursor loaded", { 164 + cursor: startCursor, 165 + isDev: env.NODE_ENV === "development", 166 + skippedSavedCursor: env.NODE_ENV === "development", 167 + }); 156 168 157 169 // DID and resolver setup 158 170 const baseIdResolver = createIdResolver(); 159 171 const resolver = createBidirectionalResolver(baseIdResolver); 160 172 const serviceDid = env.SERVICE_DID; 173 + 174 + const dataplane = new DataPlane(db, resolver.baseResolver); 175 + const hydrator = new Hydrator(dataplane); 176 + const views = new Views(); 161 177 162 178 // Services 163 179 const sub = new RepoSubscription({ ··· 167 183 startCursor, 168 184 }); 169 185 const takedownService = new TakedownService(db); 170 - const authVerifier = createAuthVerifier(db, { 186 + const authVerifier = createAuthVerifier(dataplane, { 171 187 ownDid: serviceDid, 172 188 alternateAudienceDids: [], 173 189 modServiceDid: env.MOD_SERVICE_DID, ··· 176 192 177 193 const ctx = { 178 194 db, 195 + dataplane, 196 + hydrator, 197 + views, 179 198 logger: appLogger, 180 199 resolver, 181 200 serviceDid,
+19 -9
main_test.ts
··· 1 - import { assertEquals } from "jsr:@std/assert"; 2 - import { assertMatch } from "jsr:@std/assert/match"; 1 + import { assertEquals } from "@std/assert"; 2 + import { assertMatch } from "@std/assert/match"; 3 3 import { AppContext, createApp } from "./main.ts"; 4 - import { Database } from "./data-plane/server/index.ts"; 4 + import { Database } from "./data-plane/db/index.ts"; 5 5 import { 6 6 createBidirectionalResolver, 7 7 createIdResolver, 8 8 } from "./utils/id-resolver.ts"; 9 9 import { TakedownService } from "./services/takedown.ts"; 10 - import { createAuthVerifier } from "./services/auth-verifier.ts"; 11 - import { RepoSubscription } from "./data-plane/server/subscription.ts"; 12 - import { MemoryRunner } from "./utils/memory-runner.ts"; 10 + import { createAuthVerifier } from "./auth-verifier.ts"; 11 + import { RepoSubscription } from "./data-plane/subscription.ts"; 12 + import { MemoryRunner } from "@atp/sync"; 13 13 import { getLogger } from "@logtape/logtape"; 14 + import { DataPlane } from "./data-plane/index.ts"; 15 + import { Hydrator } from "./hydration/index.ts"; 16 + import { Views } from "./views/index.ts"; 14 17 15 18 Deno.env.set("SERVICE_DID", "did:web:test"); 16 19 Deno.env.set("MOD_SERVICE_DID", "did:web:test"); ··· 36 39 saveCursorState: () => Promise.resolve(), 37 40 } as unknown as Database; 38 41 42 + const dataplane = new DataPlane(mockDb, resolver.baseResolver); 43 + const hydrator = new Hydrator(dataplane); 44 + const views = new Views(); 39 45 const takedownService = new TakedownService(mockDb); 40 46 const sub = new RepoSubscription({ 41 47 service: "wss://relay1.us-west.bsky.network", ··· 43 49 idResolver: baseIdResolver, 44 50 startCursor: undefined, 45 51 }); 46 - const authVerifier = createAuthVerifier(mockDb, { 52 + const authVerifier = createAuthVerifier(dataplane, { 47 53 ownDid: serviceDid, 48 54 alternateAudienceDids: [], 49 55 modServiceDid: "did:web:test", ··· 52 58 53 59 return { 54 60 db: mockDb, 61 + dataplane, 62 + hydrator, 63 + views, 55 64 logger: appLogger, 56 65 resolver, 57 66 serviceDid, ··· 175 184 // Create a direct MemoryRunner to test throttling 176 185 const runner = new MemoryRunner({ 177 186 startCursor: 0, 178 - cursorSaveIntervalMs: 100, // Use 100ms for faster testing 179 - setCursor: (cursor: number) => { 187 + setCursorInterval: 100, // Use 100ms for faster testing 188 + setCursor: (cursor: number): Promise<void> => { 180 189 saveCount++; 181 190 lastSavedCursor = cursor; 182 191 console.log(`Save #${saveCount}: cursor ${cursor}`); 192 + return Promise.resolve(); 183 193 }, 184 194 }); 185 195
+50
pipeline.ts
··· 1 + import { HydrationState } from "./hydration/index.ts"; 2 + 3 + export function createPipeline<Params, Skeleton, View, Context>( 4 + skeletonFn: ( 5 + input: SkeletonFnInput<Context, Params>, 6 + ) => Promise<Skeleton> | Skeleton, 7 + hydrationFn: ( 8 + input: HydrationFnInput<Context, Params, Skeleton>, 9 + ) => Promise<HydrationState>, 10 + rulesFn: (input: RulesFnInput<Context, Params, Skeleton>) => Skeleton, 11 + presentationFn: ( 12 + input: PresentationFnInput<Context, Params, Skeleton>, 13 + ) => View, 14 + ) { 15 + return async (params: Params, ctx: Context) => { 16 + const skeleton = await skeletonFn({ ctx, params }); 17 + const hydration = await hydrationFn({ ctx, params, skeleton }); 18 + const appliedRules = rulesFn({ ctx, params, skeleton, hydration }); 19 + return presentationFn({ ctx, params, skeleton: appliedRules, hydration }); 20 + }; 21 + } 22 + 23 + export type SkeletonFnInput<Context, Params> = { 24 + ctx: Context; 25 + params: Params; 26 + }; 27 + 28 + export type HydrationFnInput<Context, Params, Skeleton> = { 29 + ctx: Context; 30 + params: Params; 31 + skeleton: Skeleton; 32 + }; 33 + 34 + export type RulesFnInput<Context, Params, Skeleton> = { 35 + ctx: Context; 36 + params: Params; 37 + skeleton: Skeleton; 38 + hydration: HydrationState; 39 + }; 40 + 41 + export type PresentationFnInput<Context, Params, Skeleton> = { 42 + ctx: Context; 43 + params: Params; 44 + skeleton: Skeleton; 45 + hydration: HydrationState; 46 + }; 47 + 48 + export function noRules<S>(input: { skeleton: S }) { 49 + return input.skeleton; 50 + }
+36 -39
services/auth-verifier.ts auth-verifier.ts
··· 1 - import { KeyObject } from "node:crypto"; 2 - import { IncomingHttpHeaders } from "node:http"; 3 - import * as ui8 from "npm:uint8arrays"; 4 1 import * as jose from "jose"; 5 2 import { 6 3 AuthRequiredError, ··· 8 5 MethodAuthContext, 9 6 parseReqNsid, 10 7 verifyJwt, 11 - } from "@sprk/xrpc-server"; 12 - import { 13 - Code, 14 - DataPlaneClient, 15 - GetIdentityByDidResponse, 16 - getKeyAsDidKey, 17 - isDataplaneError, 18 - unpackIdentityKeys, 19 - } from "../data-plane/client/index.ts"; 20 - 21 - interface MinimalRequest { 22 - url?: string; 23 - method?: string; 24 - header: (name: string) => string | undefined; 25 - headers: IncomingHttpHeaders; 26 - } 8 + } from "@atp/xrpc-server"; 9 + import { DataPlane } from "./data-plane/index.ts"; 27 10 28 11 type StandardAuthOpts = { 29 12 skipAudCheck?: boolean; ··· 82 65 alternateAudienceDids: string[]; 83 66 modServiceDid: string; 84 67 adminPasses: string[]; 85 - entrywayJwtPublicKey?: KeyObject; 68 + entrywayJwtPublicKey?: CryptoKey; 86 69 }; 87 70 88 71 export interface ExtendedAuthVerifier { ··· 136 119 standardAudienceDids: Set<string>; 137 120 modServiceDid: string; 138 121 adminPasses: Set<string>; 139 - entrywayJwtPublicKey?: KeyObject; 122 + entrywayJwtPublicKey?: CryptoKey; 140 123 } 141 124 142 125 export function createAuthVerifier( 143 - dataplane: DataPlaneClient, 126 + dataplane: DataPlane, 144 127 opts: AuthVerifierOpts, 145 128 ): AuthVerifier { 146 129 const impl = new AuthVerifierImpl(dataplane, opts); ··· 190 173 public standardAudienceDids: Set<string>; 191 174 public modServiceDid: string; 192 175 private adminPasses: Set<string>; 193 - private entrywayJwtPublicKey?: KeyObject; 176 + private entrywayJwtPublicKey?: CryptoKey; 194 177 195 178 constructor( 196 - public dataplane: DataPlaneClient, 179 + public dataplane: DataPlane, 197 180 opts: AuthVerifierOpts, 198 181 ) { 199 182 this.ownDid = opts.ownDid; ··· 359 342 throw new AuthRequiredError("Malformed token", "InvalidToken"); 360 343 } else if ( 361 344 typeof aud !== "string" || 362 - !aud.startsWith("did:web:") || 363 - !aud.endsWith(".bsky.network") 345 + !aud.startsWith("did:web:") 364 346 ) { 365 347 throw new AuthRequiredError("Bad token aud", "InvalidToken"); 366 348 } else if (typeof scope !== "string" || !ALLOWED_AUTH_SCOPES.has(scope)) { ··· 444 426 const keyId = serviceId === "atproto_labeler" 445 427 ? "atproto_label" 446 428 : "atproto"; 447 - let identity: GetIdentityByDidResponse; 448 429 try { 449 - identity = await this.dataplane.getIdentityByDid({ did }); 430 + const identity = await this.dataplane.identity.getByDid(did); 431 + 432 + const keys = JSON.parse(identity.keys) as Record<string, { 433 + Type: string; 434 + PublicKeyMultibase: string; 435 + }>; 436 + const key = keys[keyId]; 437 + 438 + if (!key || !key.PublicKeyMultibase) { 439 + throw new AuthRequiredError("missing or bad key"); 440 + } 441 + 442 + return `did:key:${key.PublicKeyMultibase}`; 450 443 } catch (err) { 451 - if (isDataplaneError(err, Code.NotFound)) { 452 - throw new AuthRequiredError("identity unknown"); 444 + if (err instanceof AuthRequiredError) { 445 + throw err; 453 446 } 454 - throw err; 447 + throw new AuthRequiredError("identity unknown"); 455 448 } 456 - const keys = unpackIdentityKeys(identity.keys); 457 - const didKey = getKeyAsDidKey(keys, { id: keyId }); 458 - if (!didKey) { 459 - throw new AuthRequiredError("missing or bad key"); 460 - } 461 - return didKey; 462 449 }; 463 450 const assertLxmCheck = () => { 464 451 const lxm = parseReqNsid(reqCtx.req); ··· 559 546 const b64 = token.slice(BASIC.length); 560 547 let parsed: string[]; 561 548 try { 562 - parsed = ui8.toString(ui8.fromString(b64, "base64pad"), "utf8").split(":"); 549 + const binaryString = atob(b64); 550 + const bytes = new Uint8Array(binaryString.length); 551 + for (let i = 0; i < binaryString.length; i++) { 552 + bytes[i] = binaryString.charCodeAt(i); 553 + } 554 + parsed = new TextDecoder("utf-8").decode(bytes).split(":"); 563 555 } catch { 564 556 return null; 565 557 } ··· 571 563 export const buildBasicAuth = (username: string, password: string): string => { 572 564 return ( 573 565 BASIC + 574 - ui8.toString(ui8.fromString(`${username}:${password}`, "utf8"), "base64pad") 566 + btoa( 567 + new TextEncoder().encode(`${username}:${password}`).reduce( 568 + (data, byte) => data + String.fromCharCode(byte), 569 + "", 570 + ), 571 + ) 575 572 ); 576 573 };
+19 -9
services/takedown-filter.ts
··· 1 1 import { Context, Next } from "hono"; 2 2 import { TakedownService } from "./takedown.ts"; 3 - import lodash from "npm:lodash"; 4 3 import * as SoSprkFeedDefs from "../lex/types/so/sprk/feed/defs.ts"; 5 - const { get } = lodash; 6 4 7 5 /** 8 6 * Middleware that filters out taken-down content from responses ··· 210 208 let isTakenDown = false; 211 209 212 210 // Get URI for this specific content 213 - const uri = get(item, uriPath) as string | undefined; 214 - if (uri) { 211 + const uri = getNestedProperty(item, uriPath); 212 + if (uri && typeof uri === "string") { 215 213 const takedown = await takedownService.getTakedown(uri); 216 214 isTakenDown = takedown?.applied ?? false; 217 215 } ··· 219 217 // Check if author's repo is taken down 220 218 let isAuthorTakenDown = false; 221 219 // Look for author DID in common locations 222 - const authorDid = get(item, "author.did") || 223 - get(item, "post.author.did") || 224 - get(item, "user.did") || 225 - get(item, "actor.did"); 220 + const authorDid = getNestedProperty(item, "author.did") || 221 + getNestedProperty(item, "post.author.did") || 222 + getNestedProperty(item, "user.did") || 223 + getNestedProperty(item, "actor.did"); 226 224 227 - if (authorDid) { 225 + if (authorDid && typeof authorDid === "string") { 228 226 const repoTakedown = await takedownService.getRepoTakedown(authorDid); 229 227 isAuthorTakenDown = repoTakedown?.applied ?? false; 230 228 } ··· 371 369 372 370 return filteredReplies; 373 371 } 372 + 373 + // Helper function to safely access nested object properties 374 + function getNestedProperty(obj: unknown, path: string): unknown { 375 + if (!obj || typeof obj !== "object") return undefined; 376 + 377 + return path.split(".").reduce((current: unknown, key: string): unknown => { 378 + return current && typeof current === "object" && current !== null && 379 + key in current 380 + ? (current as Record<string, unknown>)[key] 381 + : undefined; 382 + }, obj); 383 + }
+5 -8
services/takedown.ts
··· 2 2 BlobTakedownDocument, 3 3 RepoTakedownDocument, 4 4 TakedownDocument, 5 - } from "../data-plane/server/models.ts"; 6 - import { Database } from "../data-plane/server/index.ts"; 5 + } from "../data-plane/db/models.ts"; 6 + import { Database } from "../data-plane/db/index.ts"; 7 7 8 8 export class TakedownService { 9 9 constructor(private db: Database) {} ··· 166 166 const takedowns = await this.db.models.Takedown 167 167 .find(query) 168 168 .sort({ targetUri: -1 }) 169 - .limit(limit + 1) 170 - .lean(); 169 + .limit(limit + 1); 171 170 172 171 const items = takedowns.slice(0, limit); 173 172 ··· 201 200 const takedowns = await this.db.models.RepoTakedown 202 201 .find(query) 203 202 .sort({ did: -1 }) 204 - .limit(limit + 1) 205 - .lean(); 203 + .limit(limit + 1); 206 204 207 205 const items = takedowns.slice(0, limit); 208 206 ··· 235 233 const takedowns = await this.db.models.BlobTakedown 236 234 .find(query) 237 235 .sort({ did: -1, cid: -1 }) 238 - .limit(limit + 1) 239 - .lean(); 236 + .limit(limit + 1); 240 237 241 238 const items = takedowns.slice(0, limit); 242 239
+1 -1
utils/audio-transformer.ts
··· 1 1 import type * as SoSprkSoundDefs from "../lex/types/so/sprk/sound/defs.ts"; 2 - import { AudioDocument } from "../data-plane/server/models.ts"; 2 + import { AudioDocument } from "../data-plane/db/models.ts"; 3 3 import { AppContext } from "../main.ts"; 4 4 import { createProfileViewBasic } from "./profile-helper.ts"; 5 5 import type { Label } from "../lex/types/com/atproto/label/defs.ts";
+1 -1
utils/embed-transformer.ts
··· 3 3 EmbedImage, 4 4 PostEmbed, 5 5 VideoMappingDocument, 6 - } from "../data-plane/server/models.ts"; 6 + } from "../data-plane/db/models.ts"; 7 7 import { env } from "./env.ts"; 8 8 9 9 interface ImageTransformOptions {
+5 -2
utils/env.ts
··· 1 - import * as dotenv from "npm:dotenv"; 2 - import { envInt, envStr } from "@atproto/common"; 1 + import * as dotenv from "dotenv"; 2 + import { envInt, envStr } from "@atp/common"; 3 3 4 4 dotenv.config({ quiet: true }); 5 5 ··· 14 14 ADMIN_PASSWORD: envStr("ADMIN_PASSWORD") ?? "admin-token", 15 15 HLS_CDN_URL: envStr("HLS_CDN_URL") ?? "https://vz-fb7436e9-c53.b-cdn.net", 16 16 VIDEO_CDN_URL: envStr("VIDEO_CDN_URL") ?? "https://hls.sprk.so", 17 + MEDIA_CDN_URL: envStr("MEDIA_CDN_URL") ?? "https://media.sprk.so", 18 + THUMB_CDN_URL: envStr("THUMB_CDN_URL") ?? "https://thumb.sprk.so", 19 + 17 20 RELAY_URL: envStr("RELAY_URL") ?? "wss://relay1.us-east.bsky.network", 18 21 19 22 DB_URI: envStr("DB_URI"),
+1 -1
utils/id-resolver.ts
··· 1 - import { AtprotoData, IdResolver, MemoryCache } from "@atproto/identity"; 1 + import { AtprotoData, IdResolver, MemoryCache } from "@atp/identity"; 2 2 3 3 const HOUR = 60e3 * 60; 4 4 const DAY = HOUR * 24;
-116
utils/memory-runner.ts
··· 1 - import PQueue from "p-queue"; 2 - import { ConsecutiveList, EventRunner } from "@atproto/sync"; 3 - 4 - export type MemoryRunnerOptions = { 5 - setCursor?: (cursor: number) => Promise<void> | void; 6 - concurrency?: number; 7 - startCursor?: number; 8 - cursorSaveIntervalMs?: number; 9 - }; 10 - 11 - // A queue with arbitrarily many partitions, each processing work sequentially. 12 - // Partitions are created lazily and taken out of memory when they go idle. 13 - export class MemoryRunner implements EventRunner { 14 - consecutive = new ConsecutiveList<number>(); 15 - mainQueue: PQueue; 16 - partitions = new Map<string, PQueue>(); 17 - cursor: number | undefined; 18 - private lastSaveTime = 0; 19 - private lastCursor: number | undefined; 20 - private saveTimeout: number | undefined; 21 - 22 - constructor(public opts: MemoryRunnerOptions = {}) { 23 - this.mainQueue = new PQueue({ concurrency: opts.concurrency ?? Infinity }); 24 - this.cursor = opts.startCursor; 25 - } 26 - 27 - getCursor() { 28 - return this.cursor; 29 - } 30 - 31 - addTask(partitionId: string, task: () => Promise<void>) { 32 - if (this.mainQueue.isPaused) return; 33 - return this.mainQueue.add(() => { 34 - return this.getPartition(partitionId).add(task); 35 - }); 36 - } 37 - 38 - private getPartition(partitionId: string) { 39 - let partition = this.partitions.get(partitionId); 40 - if (!partition) { 41 - partition = new PQueue({ concurrency: 1 }); 42 - partition.once("idle", () => this.partitions.delete(partitionId)); 43 - this.partitions.set(partitionId, partition); 44 - } 45 - return partition; 46 - } 47 - 48 - async trackEvent(did: string, seq: number, handler: () => Promise<void>) { 49 - if (this.mainQueue.isPaused) return; 50 - const item = this.consecutive.push(seq); 51 - await this.addTask(did, async () => { 52 - await handler(); 53 - const latest = item.complete().at(-1); 54 - if (latest !== undefined) { 55 - this.cursor = latest; 56 - if (this.opts.setCursor) { 57 - await this.throttledSaveCursor(this.cursor); 58 - } 59 - } 60 - }); 61 - } 62 - 63 - async processAll() { 64 - await this.mainQueue.onIdle(); 65 - } 66 - 67 - async destroy() { 68 - this.mainQueue.pause(); 69 - await this.mainQueue.onIdle(); 70 - this.mainQueue.clear(); 71 - this.partitions.forEach((p) => p.clear()); 72 - 73 - // Force save the latest cursor before shutdown 74 - await this.forceSaveCursor(); 75 - } 76 - 77 - private async throttledSaveCursor(cursor: number): Promise<void> { 78 - if (!this.opts.setCursor) return; 79 - 80 - this.lastCursor = cursor; 81 - const now = Date.now(); 82 - const saveInterval = this.opts.cursorSaveIntervalMs ?? 30000; 83 - 84 - // If we haven't saved recently, save immediately 85 - if (now - this.lastSaveTime >= saveInterval) { 86 - this.lastSaveTime = now; 87 - await this.opts.setCursor(cursor); 88 - } else { 89 - // Schedule a save for later if not already scheduled 90 - if (this.saveTimeout === undefined) { 91 - const timeUntilNextSave = saveInterval - (now - this.lastSaveTime); 92 - this.saveTimeout = setTimeout(async () => { 93 - try { 94 - if (this.lastCursor !== undefined && this.opts.setCursor) { 95 - this.lastSaveTime = Date.now(); 96 - await this.opts.setCursor(this.lastCursor); 97 - } 98 - } catch (err) { 99 - console.error("Error saving cursor in setTimeout:", err); 100 - } 101 - this.saveTimeout = undefined; 102 - }, timeUntilNextSave); 103 - } 104 - } 105 - } 106 - 107 - async forceSaveCursor(): Promise<void> { 108 - if (this.saveTimeout !== undefined) { 109 - clearTimeout(this.saveTimeout); 110 - this.saveTimeout = undefined; 111 - } 112 - if (this.lastCursor !== undefined && this.opts.setCursor) { 113 - await this.opts.setCursor(this.lastCursor); 114 - } 115 - } 116 - }
+2 -2
utils/post-transformer.ts
··· 1 - import { PostDocument } from "../data-plane/server/models.ts"; 1 + import { PostDocument } from "../data-plane/db/models.ts"; 2 2 import type { Label } from "../lex/types/com/atproto/label/defs.ts"; 3 3 import type * as SoSprkFeedDefs from "../lex/types/so/sprk/feed/defs.ts"; 4 4 import type * as SoSprkFeedPost from "../lex/types/so/sprk/feed/post.ts"; ··· 64 64 .filter((p) => p.embed?.$type === "so.sprk.embed.video") 65 65 .map((p) => `${p.authorDid}-${p.embed?.video?.ref.$link}`), 66 66 }, 67 - }).lean(), 67 + }), 68 68 // Get viewer likes 69 69 userDid 70 70 ? ctx.db.models.Like.find({
+6 -8
utils/profile-helper.ts
··· 6 6 ViewerState, 7 7 } from "../lex/types/so/sprk/actor/defs.ts"; 8 8 import type * as ComAtprotoRepoStrongRef from "../lex/types/com/atproto/repo/strongRef.ts"; 9 - import type { StoryDocument } from "../data-plane/server/models.ts"; 9 + import type { StoryDocument } from "../data-plane/db/models.ts"; 10 10 import type { Label } from "../lex/types/com/atproto/label/defs.ts"; 11 - import { ensureValidDid, isValidHandle } from "@atproto/syntax"; 11 + import { ensureValidDid, isValidHandle } from "@atp/syntax"; 12 12 import { AppContext } from "../main.ts"; 13 - import { XRPCError } from "@sprk/xrpc-server"; 13 + import { XRPCError } from "@atp/xrpc-server"; 14 14 15 15 // Helper function to create ProfileViewBasic with stories 16 16 export async function createProfileViewBasic( ··· 21 21 // Get author profile data 22 22 const profile = await ctx.db.models.Profile.findOne({ 23 23 authorDid: authorDid, 24 - }).lean(); 24 + }); 25 25 const actor = await ctx.db.models.Actor.findOne({ 26 26 did: authorDid, 27 - }).lean(); 27 + }); 28 28 const authorHandle = actor?.handle ?? "unknown.invalid"; 29 29 30 30 let stories: ComAtprotoRepoStrongRef.Main[] = []; ··· 41 41 indexedAt: { $gte: twentyFourHoursAgo.toISOString() }, 42 42 }) 43 43 .sort({ indexedAt: -1 }) 44 - .limit(15) 45 - .lean(); 44 + .limit(15); 46 45 47 46 // Convert recent stories to strongRefs 48 47 stories = recentStories.map((story: StoryDocument) => ({ ··· 360 359 }) 361 360 .sort({ indexedAt: -1 }) 362 361 .limit(15) 363 - .lean() 364 362 .catch((error: Error) => { 365 363 ctx.logger.warn( 366 364 "Failed to fetch stories for profile",
+2 -2
utils/retry.ts
··· 1 - import { createRetryable } from "@atproto/common"; 2 - import { ResponseType, XRPCError } from "@atproto/xrpc"; 1 + import { createRetryable } from "@atp/common"; 2 + import { ResponseType, XRPCError } from "@atp/xrpc"; 3 3 4 4 export const RETRYABLE_HTTP_STATUS_CODES = new Set([ 5 5 408,
+1 -1
utils/story-transformer.ts
··· 1 1 import type * as SoSprkFeedDefs from "../lex/types/so/sprk/feed/defs.ts"; 2 - import { StoryDocument } from "../data-plane/server/models.ts"; 2 + import { StoryDocument } from "../data-plane/db/models.ts"; 3 3 import { transformEmbed } from "./embed-transformer.ts"; 4 4 import { createProfileViewBasic } from "./profile-helper.ts"; 5 5 import { AppContext } from "../main.ts";
+61
utils/uris.ts
··· 1 + import { AtUri } from "@atp/syntax"; 2 + import { ids } from "../lex/lexicons.ts"; 3 + import { 4 + Main as StrongRef, 5 + validateMain as validateStrongRef, 6 + } from "../lex/types/com/atproto/repo/strongRef.ts"; 7 + 8 + /** 9 + * Convert a post URI to a threadgate URI. If the URI is not a valid 10 + * post URI, return URI unchanged. Threadgate lookups will then fail. 11 + * Threadgates aren't implemented yet but will be in the future. 12 + */ 13 + export function postUriToThreadgateUri(postUri: string) { 14 + const urip = new AtUri(postUri); 15 + if (urip.collection === ids.AppBskyFeedPost) { 16 + urip.collection = ids.AppBskyFeedThreadgate; 17 + } 18 + return urip.toString(); 19 + } 20 + 21 + /** 22 + * Convert a post URI to a postgate URI. If the URI is not a valid 23 + * post URI, return URI unchanged. Postgate lookups will then fail. 24 + * Postgates aren't implemented yet but will be in the future. 25 + */ 26 + export function postUriToPostgateUri(postUri: string) { 27 + const urip = new AtUri(postUri); 28 + if (urip.collection === ids.AppBskyFeedPost) { 29 + urip.collection = ids.AppBskyFeedPostgate; 30 + } 31 + return urip.toString(); 32 + } 33 + 34 + export function uriToDid(uri: string) { 35 + try { 36 + return new AtUri(uri).hostname; 37 + } catch (error) { 38 + console.log(`AtUri parser failed for URI: ${uri}, error:`, error); 39 + // Handle custom collection namespaces that AtUri might not recognize 40 + // Extract DID from URI manually as fallback 41 + const match = uri.match(/^at:\/\/(did:[^\/]+)/); 42 + if (match) { 43 + console.log(`Successfully extracted DID using fallback: ${match[1]}`); 44 + return match[1]; 45 + } 46 + console.error(`Failed to extract DID from URI: ${uri}`); 47 + throw new Error(`Invalid AT URI format: ${uri}`); 48 + } 49 + } 50 + 51 + // @TODO temp fix for proliferation of invalid pinned post values 52 + export function safePinnedPost(value: unknown) { 53 + if (!value || typeof value !== "object") { 54 + return; 55 + } 56 + const validated = validateStrongRef(value); 57 + if (!validated.success) { 58 + return; 59 + } 60 + return validated.value as StrongRef; 61 + }
+454
views/index.ts
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { HydrationState } from "../hydration/index.ts"; 3 + import { 4 + FeedViewPost, 5 + isPostView, 6 + PostView, 7 + ReasonPin, 8 + ReasonRepost, 9 + ReplyRef, 10 + } from "../lex/types/so/sprk/feed/defs.ts"; 11 + import { isRecord as isPostRecord } from "../lex/types/so/sprk/feed/post.ts"; 12 + import { 13 + KnownFollowers, 14 + ProfileView, 15 + ProfileViewBasic, 16 + ProfileViewDetailed, 17 + ViewerState as ProfileViewer, 18 + } from "../lex/types/so/sprk/actor/defs.ts"; 19 + import { 20 + BlockedPost, 21 + Embed, 22 + EmbedView, 23 + ImagesEmbed, 24 + ImagesEmbedView, 25 + isImagesEmbed, 26 + isVideoEmbed, 27 + MaybePostView, 28 + NotFoundPost, 29 + VideoEmbed, 30 + VideoEmbedView, 31 + } from "./types.ts"; 32 + import { INVALID_HANDLE } from "@atp/syntax"; 33 + import { cidFromBlobJson } from "./util.ts"; 34 + import { uriToDid } from "../utils/uris.ts"; 35 + import { env } from "../utils/env.ts"; 36 + import { mapDefined } from "@atp/common"; 37 + import { FeedItem, Repost } from "../hydration/feed.ts"; 38 + import { $Typed } from "../lex/util.ts"; 39 + 40 + export class Views { 41 + public indexedAtEpoch?: Date | undefined; 42 + 43 + constructor( 44 + opts?: { 45 + indexedAtEpoch?: Date | undefined; 46 + }, 47 + ) { 48 + this.indexedAtEpoch = opts?.indexedAtEpoch; 49 + } 50 + 51 + post( 52 + uri: string, 53 + state: HydrationState, 54 + depth = 0, 55 + ): PostView | undefined { 56 + const post = state.posts?.get(uri); 57 + if (!post) return; 58 + const parsedUri = new AtUri(uri); 59 + const authorDid = parsedUri.hostname; 60 + const author = this.profileBasic(authorDid, state); 61 + if (!author) return; 62 + const aggs = state.postAggs?.get(uri); 63 + const viewer = state.postViewers?.get(uri); 64 + return { 65 + uri, 66 + cid: post.cid, 67 + author, 68 + record: post.record, 69 + embed: depth < 2 && post.record.embed 70 + ? this.embed(uri, post.record.embed, state) 71 + : undefined, 72 + replyCount: aggs?.replies ?? 0, 73 + repostCount: aggs?.reposts ?? 0, 74 + likeCount: aggs?.likes ?? 0, 75 + indexedAt: this.indexedAt(post)?.toISOString() ?? 76 + new Date().toISOString(), 77 + viewer: viewer 78 + ? { 79 + repost: viewer.repost, 80 + like: viewer.like, 81 + } 82 + : undefined, 83 + }; 84 + } 85 + 86 + feedViewPost( 87 + item: FeedItem, 88 + state: HydrationState, 89 + ): FeedViewPost | undefined { 90 + let reason; 91 + if (item.authorPinned) { 92 + reason = this.reasonPin(); 93 + } else if (item.repost) { 94 + const repost = state.reposts?.get(item.repost.uri); 95 + if (!repost || !repost?.record.subject) return; 96 + if (repost.record.subject.uri !== item.post.uri) return; 97 + reason = this.reasonRepost(item.repost.uri, repost, state); 98 + if (!reason) return; 99 + } 100 + const post = this.post(item.post.uri, state); 101 + if (!post) return; 102 + const reply = this.replyRef(item.post.uri, state); 103 + return { 104 + post, 105 + reason, 106 + reply, 107 + }; 108 + } 109 + 110 + replyRef(uri: string, state: HydrationState): ReplyRef | undefined { 111 + const postRecord = state.posts?.get(uri.toString())?.record; 112 + if (!postRecord?.reply) return; 113 + let root = this.maybePost(postRecord.reply.root.uri, state); 114 + let parent = this.maybePost(postRecord.reply.parent.uri, state); 115 + if (!state.ctx?.include3pBlocks) { 116 + const childBlocks = state.postBlocks?.get(uri); 117 + const parentBlocks = state.postBlocks?.get(parent.uri); 118 + // if child blocks parent, block parent 119 + if (isPostView(parent) && childBlocks?.parent) { 120 + parent = this.blockedPost(parent.uri, parent.author.did, state); 121 + } 122 + // if child or parent blocks root, block root 123 + if (isPostView(root) && (childBlocks?.root || parentBlocks?.root)) { 124 + root = this.blockedPost(root.uri, root.author.did, state); 125 + } 126 + } 127 + let grandparentAuthor: ProfileViewBasic | undefined; 128 + if ( 129 + isPostView(parent) && 130 + isPostRecord(parent.record) && 131 + parent.record.reply 132 + ) { 133 + grandparentAuthor = this.profileBasic( 134 + // @ts-expect-error isValidPostRecord(parent.record) should be used but the "parent" is not IPDL decoded 135 + creatorFromUri(parent.record.reply.parent.uri), 136 + state, 137 + ); 138 + } 139 + return { 140 + root, 141 + parent, 142 + grandparentAuthor, 143 + }; 144 + } 145 + 146 + reasonPin(): $Typed<ReasonPin> { 147 + return { 148 + $type: "so.sprk.feed.defs#reasonPin", 149 + }; 150 + } 151 + 152 + reasonRepost( 153 + uri: string, 154 + repost: Repost, 155 + state: HydrationState, 156 + ): $Typed<ReasonRepost> | undefined { 157 + const creatorDid = uriToDid(uri); 158 + const creator = this.profileBasic(creatorDid, state); 159 + if (!creator) return; 160 + return { 161 + $type: "so.sprk.feed.defs#reasonRepost", 162 + by: creator, 163 + indexedAt: this.indexedAt(repost).toISOString(), 164 + }; 165 + } 166 + 167 + maybePost(uri: string, state: HydrationState): $Typed<MaybePostView> { 168 + const post = this.post(uri, state); 169 + if (!post) { 170 + return this.notFoundPost(uri); 171 + } 172 + if (this.viewerBlockExists(post.author.did, state)) { 173 + return this.blockedPost(uri, post.author.did, state); 174 + } 175 + return { 176 + ...post, 177 + $type: "so.sprk.feed.defs#postView", 178 + }; 179 + } 180 + 181 + blockedPost( 182 + uri: string, 183 + authorDid: string, 184 + state: HydrationState, 185 + ): $Typed<BlockedPost> { 186 + return { 187 + $type: "so.sprk.feed.defs#blockedPost", 188 + uri, 189 + blocked: true, 190 + author: { 191 + did: authorDid, 192 + viewer: this.blockedProfileViewer(authorDid, state), 193 + }, 194 + }; 195 + } 196 + 197 + notFoundPost(uri: string): $Typed<NotFoundPost> { 198 + return { 199 + $type: "so.sprk.feed.defs#notFoundPost", 200 + uri, 201 + notFound: true, 202 + }; 203 + } 204 + 205 + feedItemBlocksAndMutes( 206 + item: FeedItem, 207 + state: HydrationState, 208 + ): { 209 + originatorMuted: boolean; 210 + originatorBlocked: boolean; 211 + authorMuted: boolean; 212 + authorBlocked: boolean; 213 + ancestorAuthorBlocked: boolean; 214 + } { 215 + const authorDid = uriToDid(item.post.uri); 216 + const originatorDid = item.repost ? uriToDid(item.repost.uri) : authorDid; 217 + const post = state.posts?.get(item.post.uri); 218 + const parentUri = post?.record.reply?.parent.uri; 219 + const parentAuthorDid = parentUri && uriToDid(parentUri); 220 + const parent = parentUri ? state.posts?.get(parentUri) : undefined; 221 + const grandparentUri = parent?.record.reply?.parent.uri; 222 + const grandparentAuthorDid = grandparentUri && uriToDid(grandparentUri); 223 + return { 224 + originatorMuted: this.viewerMuteExists(originatorDid, state), 225 + originatorBlocked: this.viewerBlockExists(originatorDid, state), 226 + authorMuted: this.viewerMuteExists(authorDid, state), 227 + authorBlocked: this.viewerBlockExists(authorDid, state), 228 + ancestorAuthorBlocked: 229 + (!!parentAuthorDid && this.viewerBlockExists(parentAuthorDid, state)) || 230 + (!!grandparentAuthorDid && 231 + this.viewerBlockExists(grandparentAuthorDid, state)), 232 + }; 233 + } 234 + 235 + profile( 236 + did: string, 237 + state: HydrationState, 238 + ): ProfileView | undefined { 239 + const actor = state.actors?.get(did); 240 + if (!actor) return; 241 + const basicView = this.profileBasic(did, state); 242 + if (!basicView) return; 243 + return { 244 + ...basicView, 245 + $type: "so.sprk.actor.defs#profileView", 246 + description: actor.profile?.description || undefined, 247 + indexedAt: actor.indexedAt && actor.sortedAt 248 + ? this.indexedAt({ 249 + sortedAt: actor.sortedAt, 250 + indexedAt: actor.indexedAt, 251 + }).toISOString() 252 + : undefined, 253 + }; 254 + } 255 + 256 + profileDetailed( 257 + did: string, 258 + state: HydrationState, 259 + ): ProfileViewDetailed | undefined { 260 + const actor = state.actors?.get(did); 261 + if (!actor) return; 262 + const baseView = this.profile(did, state); 263 + if (!baseView) return; 264 + const knownFollowers = this.knownFollowers(did, state); 265 + const profileAggs = state.profileAggs?.get(did); 266 + 267 + return { 268 + ...baseView, 269 + $type: "so.sprk.actor.defs#profileViewDetailed", 270 + viewer: baseView.viewer 271 + ? { 272 + ...baseView.viewer, 273 + knownFollowers, 274 + } 275 + : undefined, 276 + followersCount: profileAggs?.followers ?? 0, 277 + followsCount: profileAggs?.follows ?? 0, 278 + postsCount: profileAggs?.posts ?? 0, 279 + associated: { 280 + feedgens: profileAggs?.feeds, 281 + }, 282 + }; 283 + } 284 + 285 + profileBasic( 286 + did: string, 287 + state: HydrationState, 288 + ): ProfileViewBasic | undefined { 289 + const actor = state.actors?.get(did); 290 + if (!actor) return; 291 + return { 292 + did, 293 + handle: actor.handle ?? INVALID_HANDLE, 294 + displayName: actor.profile?.displayName, 295 + avatar: actor.profile?.avatar 296 + ? `${env.MEDIA_CDN_URL}/avatar/medium/${did}/${actor.profile.avatar.ref}/webp` 297 + : undefined, 298 + viewer: this.profileViewer(did, state), 299 + createdAt: actor.createdAt?.toISOString(), 300 + }; 301 + } 302 + 303 + profileViewer(did: string, state: HydrationState): ProfileViewer | undefined { 304 + const viewer = state.profileViewers?.get(did); 305 + if (!viewer) return; 306 + const blockedByUri = viewer.blockedBy; 307 + const blockingUri = viewer.blocking; 308 + const block = !!blockedByUri || !!blockingUri; 309 + return { 310 + blockedBy: !!blockedByUri, 311 + blocking: blockingUri, 312 + following: viewer.following && !block ? viewer.following : undefined, 313 + followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined, 314 + }; 315 + } 316 + 317 + viewerMuteExists(did: string, state: HydrationState): boolean { 318 + const viewer = state.profileViewers?.get(did); 319 + if (!viewer) return false; 320 + return !viewer.muted; 321 + } 322 + 323 + blockedProfileViewer( 324 + did: string, 325 + state: HydrationState, 326 + ): ProfileViewer | undefined { 327 + const viewer = state.profileViewers?.get(did); 328 + if (!viewer) return; 329 + const blockedByUri = viewer.blockedBy; 330 + const blockingUri = viewer.blocking; 331 + return { 332 + blockedBy: !!blockedByUri, 333 + blocking: blockingUri, 334 + }; 335 + } 336 + 337 + knownFollowers( 338 + did: string, 339 + state: HydrationState, 340 + ): KnownFollowers | undefined { 341 + const knownFollowers = state.knownFollowers?.get(did); 342 + if (!knownFollowers) return; 343 + const blocks = state.bidirectionalBlocks?.get(did); 344 + const followers = mapDefined(knownFollowers.followers, (followerDid) => { 345 + if (this.viewerBlockExists(followerDid, state)) { 346 + return undefined; 347 + } 348 + if (blocks?.get(followerDid)) { 349 + return undefined; 350 + } 351 + if (this.actorIsNoHosted(followerDid, state)) { 352 + // @TODO only needed right now to work around getProfile's { includeTakedowns: true } 353 + return undefined; 354 + } 355 + return this.profileBasic(followerDid, state); 356 + }); 357 + return { count: knownFollowers.count, followers }; 358 + } 359 + 360 + embed( 361 + postUri: string, 362 + embed: Embed | { $type: string }, 363 + state?: HydrationState, 364 + ): (EmbedView & { $type: string }) | undefined { 365 + if (isImagesEmbed(embed)) { 366 + return this.imagesEmbed(uriToDid(postUri), embed); 367 + } else if (isVideoEmbed(embed)) { 368 + const authorDid = uriToDid(postUri); 369 + const videoCid = embed.video ? cidFromBlobJson(embed.video) : ""; 370 + const videoMappingKey = `${authorDid}-${videoCid}`; 371 + const videoMapping = state?.videoMappings?.get(videoMappingKey) || null; 372 + return this.videoEmbed(authorDid, embed, videoMapping); 373 + } else { 374 + return undefined; 375 + } 376 + } 377 + 378 + imagesEmbed( 379 + did: string, 380 + embed: ImagesEmbed, 381 + ): ImagesEmbedView & { $type: string } { 382 + const imgViews = embed.images.map((img) => ({ 383 + thumb: `${env.MEDIA_CDN_URL}/img/medium/${did}/${ 384 + cidFromBlobJson(img.image) 385 + }/webp`, 386 + fullsize: `${env.MEDIA_CDN_URL}/img/full/${did}/${ 387 + cidFromBlobJson(img.image) 388 + }/webp`, 389 + alt: img.alt, 390 + aspectRatio: img.aspectRatio, 391 + })); 392 + return { 393 + $type: "so.sprk.embed.images#view" as const, 394 + images: imgViews, 395 + }; 396 + } 397 + 398 + videoEmbed( 399 + did: string, 400 + embed: VideoEmbed, 401 + videoMapping?: { bunnyGuid: string } | null, 402 + ): VideoEmbedView & { $type: string } { 403 + const cid = cidFromBlobJson(embed.video); 404 + 405 + let playlist: string; 406 + let thumbnail: string; 407 + 408 + if (videoMapping) { 409 + playlist = `${env.HLS_CDN_URL}/${videoMapping.bunnyGuid}/playlist.m3u8`; 410 + thumbnail = `${env.HLS_CDN_URL}/${videoMapping.bunnyGuid}/thumbnail.jpg`; 411 + } else { 412 + playlist = `${env.VIDEO_CDN_URL}/watch/${did}/${cid}/playlist.m3u8`; 413 + thumbnail = `${env.THUMB_CDN_URL}/${did}/${cid}/thumbnail`; 414 + } 415 + 416 + return { 417 + $type: "so.sprk.embed.video#view" as const, 418 + cid, 419 + playlist, 420 + thumbnail, 421 + alt: embed.alt, 422 + aspectRatio: embed.aspectRatio, 423 + }; 424 + } 425 + indexedAt({ sortedAt, indexedAt }: { sortedAt: Date; indexedAt: Date }) { 426 + if (!this.indexedAtEpoch) return sortedAt; 427 + return indexedAt && indexedAt > this.indexedAtEpoch ? indexedAt : sortedAt; 428 + } 429 + viewerBlockExists(did: string, state: HydrationState): boolean { 430 + const viewer = state.profileViewers?.get(did); 431 + if (!viewer) return false; 432 + return !!( 433 + viewer.blockedBy || 434 + viewer.blocking 435 + ); 436 + } 437 + actorIsNoHosted(did: string, state: HydrationState): boolean { 438 + return ( 439 + this.actorIsDeactivated(did, state) || this.actorIsTakendown(did, state) 440 + ); 441 + } 442 + actorIsDeactivated(did: string, state: HydrationState): boolean { 443 + if (state.actors?.get(did)?.upstreamStatus === "deactivated") return true; 444 + return false; 445 + } 446 + 447 + actorIsTakendown(did: string, state: HydrationState): boolean { 448 + const actor = state.actors?.get(did); 449 + if (actor?.takedownRef) return true; 450 + if (actor?.upstreamStatus === "takendown") return true; 451 + if (actor?.upstreamStatus === "suspended") return true; 452 + return false; 453 + } 454 + }
+46
views/types.ts
··· 1 + import { 2 + Main as ImagesEmbed, 3 + View as ImagesEmbedView, 4 + } from "../lex/types/so/sprk/embed/images.ts"; 5 + import { 6 + Main as VideoEmbed, 7 + View as VideoEmbedView, 8 + } from "../lex/types/so/sprk/embed/video.ts"; 9 + import { 10 + BlockedPost, 11 + GeneratorView, 12 + NotFoundPost, 13 + PostView, 14 + } from "../lex/types/so/sprk/feed/defs.ts"; 15 + import { LabelerView } from "../lex/types/app/bsky/labeler/defs.ts"; 16 + 17 + export type { 18 + Main as ImagesEmbed, 19 + View as ImagesEmbedView, 20 + } from "../lex/types/so/sprk/embed/images.ts"; 21 + export { isMain as isImagesEmbed } from "../lex/types/so/sprk/embed/images.ts"; 22 + export type { 23 + Main as VideoEmbed, 24 + View as VideoEmbedView, 25 + } from "../lex/types/so/sprk/embed/video.ts"; 26 + export { isMain as isVideoEmbed } from "../lex/types/so/sprk/embed/video.ts"; 27 + export type { 28 + BlockedPost, 29 + GeneratorView, 30 + NotFoundPost, 31 + PostView, 32 + } from "../lex/types/so/sprk/feed/defs.ts"; 33 + 34 + export type Embed = 35 + | ImagesEmbed 36 + | VideoEmbed; 37 + 38 + export type EmbedView = 39 + | ImagesEmbedView 40 + | VideoEmbedView; 41 + 42 + export type MaybePostView = PostView | NotFoundPost | BlockedPost; 43 + 44 + export type RecordEmbedViewInternal = 45 + | GeneratorView 46 + | LabelerView;
+40
views/util.ts
··· 1 + import { BlobRef } from "@atp/lexicon"; 2 + 3 + // Simple string format function to replace util.format 4 + const format = (template: string, ...args: string[]): string => { 5 + return template.replace(/%s/g, () => args.shift() || ""); 6 + }; 7 + 8 + export const cidFromBlobJson = (json: BlobRef) => { 9 + if (json instanceof BlobRef) { 10 + return json.ref.toString(); 11 + } 12 + // @NOTE below handles the fact that parseRecordBytes() produces raw json rather than lexicon values 13 + if (json["$type"] === "blob") { 14 + return (json["ref"]?.["$link"] ?? "") as string; 15 + } 16 + return (json["cid"] ?? "") as string; 17 + }; 18 + 19 + export class VideoUriBuilder { 20 + constructor( 21 + private opts: { 22 + playlistUrlPattern: string; // e.g. https://hostname/vid/%s/%s/playlist.m3u8 23 + thumbnailUrlPattern: string; // e.g. https://hostname/vid/%s/%s/thumbnail.jpg 24 + }, 25 + ) {} 26 + playlist({ did, cid }: { did: string; cid: string }) { 27 + return format( 28 + this.opts.playlistUrlPattern, 29 + encodeURIComponent(did), 30 + encodeURIComponent(cid), 31 + ); 32 + } 33 + thumbnail({ did, cid }: { did: string; cid: string }) { 34 + return format( 35 + this.opts.thumbnailUrlPattern, 36 + encodeURIComponent(did), 37 + encodeURIComponent(cid), 38 + ); 39 + } 40 + }