···11import { AppContext } from "../../../../main.ts";
22-import { mapDefined } from "@atproto/common";
33-import { INVALID_HANDLE } from "@atproto/syntax";
22+import { mapDefined } from "@atp/common";
33+import { INVALID_HANDLE } from "@atp/syntax";
44import { Server } from "../../../../lex/index.ts";
55-import { AuthRequiredError } from "@sprk/xrpc-server";
55+import { AuthRequiredError } from "@atp/xrpc-server";
6677export default function (server: Server, ctx: AppContext) {
88 server.com.atproto.admin.getAccountInfos({
+1-1
api/com/atproto/admin/getSubjectStatus.ts
···11import { AppContext } from "../../../../main.ts";
22import { Server } from "../../../../lex/index.ts";
33-import { AuthRequiredError, XRPCError } from "@sprk/xrpc-server";
33+import { AuthRequiredError, XRPCError } from "@atp/xrpc-server";
4455export default function (server: Server, ctx: AppContext) {
66 server.com.atproto.admin.getSubjectStatus({
+1-1
api/com/atproto/admin/updateSubjectStatus.ts
···33import { Server } from "../../../../lex/index.ts";
44import type * as ComAtprotoAdminDefs from "../../../../lex/types/com/atproto/admin/defs.ts";
55import type * as ComAtprotoRepoStrongRef from "../../../../lex/types/com/atproto/repo/strongRef.ts";
66-import { AuthRequiredError } from "@sprk/xrpc-server";
66+import { AuthRequiredError } from "@atp/xrpc-server";
7788export default function (server: Server, ctx: AppContext) {
99 server.com.atproto.admin.updateSubjectStatus({
+2-2
api/com/atproto/identity/resolveHandle.ts
···11-import * as ident from "@atproto/syntax";
22-import { InvalidRequestError } from "@sprk/xrpc-server";
11+import * as ident from "@atp/syntax";
22+import { InvalidRequestError } from "@atp/xrpc-server";
33import { Server } from "../../../../lex/index.ts";
44import { AppContext } from "../../../../main.ts";
55
+24-86
api/com/atproto/repo/getRecord.ts
···11-import { AtUri } from "@atproto/syntax";
22-import { InvalidRequestError } from "@sprk/xrpc-server";
33-import { Server } from "../../../../lex/index.ts";
11+import { AtUri } from "@atp/syntax";
22+import { InvalidRequestError } from "@atp/xrpc-server";
43import { AppContext } from "../../../../main.ts";
55-import { OutputSchema } from "../../../../lex/types/com/atproto/repo/getRecord.ts";
66-77-interface TakedownInfo {
88- reason: string;
99- takenDownBy: string;
1010- takenDownAt: string;
1111- warning: string;
1212-}
44+import { Server } from "../../../../lex/index.ts";
135146export default function (server: Server, ctx: AppContext) {
157 server.com.atproto.repo.getRecord({
168 auth: ctx.authVerifier.optionalStandardOrRole,
1717- handler: async ({ params, auth }) => {
99+ handler: async ({ auth, params }) => {
1810 const { repo, collection, rkey, cid } = params;
1911 const { includeTakedowns } = ctx.authVerifier.parseCreds(auth);
2020-2121- if (!repo || !collection || !rkey) {
2222- throw new InvalidRequestError("Missing required parameters");
2323- }
2424-2525- // Resolve the handle to DID if needed
2626- let did;
2727- try {
2828- if (repo.startsWith("did:")) {
2929- did = repo;
3030- } else {
3131- // Assume it's a handle
3232- const didDoc = await ctx.resolver.resolveHandleToDidDoc(repo);
3333- did = didDoc.did;
3434- }
3535- } catch {
1212+ const [did] = await ctx.hydrator.actor.getDids([repo]);
1313+ if (!did) {
3614 throw new InvalidRequestError(`Could not find repo: ${repo}`);
3715 }
38163939- if (!did) {
1717+ const actors = await ctx.hydrator.actor.getActors([did], {
1818+ includeTakedowns,
1919+ });
2020+ if (!actors.get(did)) {
4021 throw new InvalidRequestError(`Could not find repo: ${repo}`);
4122 }
42234343- // Create the URI
4424 const uri = AtUri.make(did, collection, rkey).toString();
4545-4646- try {
4747- const record = await ctx.db.models.Record.findOne({ uri }).lean();
4848-4949- if (!record || (cid && record.cid !== cid)) {
5050- // For admins, provide more detailed information about what we tried to query
5151- if (includeTakedowns) {
5252- ctx.logger.info("Admin record lookup failed", {
5353- uri,
5454- collection,
5555- did,
5656- rkey,
5757- cid,
5858- foundRecord: !!record,
5959- cidMatch: record ? (cid ? record.cid === cid : true) : false,
6060- });
6161- }
6262- throw new InvalidRequestError(`Could not locate record: ${uri}`);
6363- }
6464-6565- // Parse the original record JSON
6666- const recordValue = record.json;
6767-6868- // Check if the record is subject to a takedown
6969- const takedown = await ctx.takedownService.getTakedown(uri);
2525+ const result = await ctx.hydrator.getRecord(uri, includeTakedowns);
70267171- // If record is taken down and user is not an admin, deny access
7272- if (takedown && !includeTakedowns) {
7373- throw new InvalidRequestError(`Record is taken down: ${uri}`);
7474- }
2727+ if (!result || (cid && result.cid !== cid)) {
2828+ throw new InvalidRequestError(
2929+ `Could not locate record: ${uri}`,
3030+ "RecordNotFound",
3131+ );
3232+ }
75337676- // Format the response according to the output schema
7777- const response: OutputSchema & { takedown?: TakedownInfo } = {
3434+ return {
3535+ encoding: "application/json" as const,
3636+ body: {
7837 uri: uri,
7979- cid: record.cid,
8080- value: recordValue,
8181- };
8282-8383- // Include takedown info for admins
8484- if (includeTakedowns && takedown) {
8585- response.takedown = {
8686- reason: takedown.reason,
8787- takenDownBy: takedown.takenDownBy,
8888- takenDownAt: takedown.takenDownAt,
8989- warning:
9090- "This content has been taken down and is only visible to admins",
9191- };
9292- }
9393-9494- return {
9595- encoding: "application/json",
9696- body: response,
9797- };
9898- } catch (err) {
9999- if (err instanceof InvalidRequestError) {
100100- throw err;
101101- }
102102- throw new InvalidRequestError(`Error retrieving record: ${uri}`);
103103- }
3838+ cid: result.cid,
3939+ value: result.record,
4040+ },
4141+ };
10442 },
10543 });
10644}
···22import { AppContext } from "../../../../main.ts";
33import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getStories.ts";
44import { transformStoriesToStoryViews } from "../../../../utils/story-transformer.ts";
55-import { StoryDocument } from "../../../../data-plane/server/models.ts";
55+import { StoryDocument } from "../../../../data-plane/db/models.ts";
6677// Constants
88const MAX_STORIES_LIMIT = 25;
···182182 const dbStories = await ctx.db.models.Story.find({
183183 uri: { $in: uniqueUris },
184184 })
185185- .lean()
186185 .exec();
187186188187 if (dbStories.length === 0) {
+91-140
api/so/sprk/feed/getStoriesTimeline.ts
···11-import { InvalidRequestError } from "@sprk/xrpc-server";
11+import { InvalidRequestError } from "@atp/xrpc-server";
22import { Server } from "../../../../lex/index.ts";
33import { AppContext } from "../../../../main.ts";
44import { transformStoriesToStoryViews } from "../../../../utils/story-transformer.ts";
55-import { decodeBase64, encodeBase64 } from "jsr:@std/encoding";
55+import { decodeBase64, encodeBase64 } from "@std/encoding";
66import type { ProfileViewBasic } from "../../../../lex/types/so/sprk/actor/defs.ts";
77import type * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts";
88···168168 server.so.sprk.feed.getStoriesTimeline({
169169 auth: ctx.authVerifier.standard,
170170 handler: async ({ params, auth }) => {
171171- try {
172172- const { limit: limitParam = DEFAULT_LIMIT, cursor } = params;
173173- const userDid = auth.credentials.iss;
171171+ const { limit: limitParam = DEFAULT_LIMIT, cursor } = params;
172172+ const userDid = auth.credentials.iss;
174173175175- // Validate and sanitize limit
176176- const limit = typeof limitParam === "string"
177177- ? parseInt(limitParam, 10)
178178- : limitParam;
174174+ // Validate and sanitize limit
175175+ const limit = typeof limitParam === "string"
176176+ ? parseInt(limitParam, 10)
177177+ : limitParam;
179178180180- if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) {
181181- throw new InvalidRequestError(
182182- `Invalid limit: must be between 1 and ${MAX_LIMIT}`,
183183- );
184184- }
179179+ if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) {
180180+ throw new InvalidRequestError(
181181+ `Invalid limit: must be between 1 and ${MAX_LIMIT}`,
182182+ );
183183+ }
185184186186- // Parse cursor if provided
187187- let cursorData: CursorData | undefined;
188188- if (cursor) {
189189- cursorData = parseCursor(cursor);
190190- }
185185+ // Parse cursor if provided
186186+ let cursorData: CursorData | undefined;
187187+ if (cursor) {
188188+ cursorData = parseCursor(cursor);
189189+ }
191190192192- // Get accounts that the viewer follows (with optimization)
193193- const followedDids = await getUserFollows(ctx, userDid);
191191+ // Get accounts that the viewer follows (with optimization)
192192+ const followedDids = await getUserFollows(ctx, userDid);
194193195195- if (followedDids.length === 0) {
196196- return {
197197- encoding: "application/json",
198198- body: {
199199- storiesByAuthor: [],
200200- },
201201- };
202202- }
194194+ if (followedDids.length === 0) {
195195+ return {
196196+ encoding: "application/json",
197197+ body: {
198198+ storiesByAuthor: [],
199199+ },
200200+ };
201201+ }
203202204204- // Build optimized query
205205- const query = buildStoriesQuery(followedDids, cursorData);
203203+ // Build optimized query
204204+ const query = buildStoriesQuery(followedDids, cursorData);
206205207207- // Get stories from database with optimized query
208208- const stories = await ctx.db.models.Story.find(query)
209209- .sort({ indexedAt: -1, _id: -1 })
210210- .limit(limit + 1) // Get one extra for hasMore check
211211- .lean()
212212- .exec();
206206+ // Get stories from database with optimized query
207207+ const stories = await ctx.db.models.Story.find(query)
208208+ .sort({ indexedAt: -1, _id: -1 })
209209+ .limit(limit + 1) // Get one extra for hasMore check
210210+ .exec();
213211214214- if (stories.length === 0) {
215215- return {
216216- encoding: "application/json",
217217- body: {
218218- storiesByAuthor: [],
219219- },
220220- };
221221- }
212212+ if (stories.length === 0) {
213213+ return {
214214+ encoding: "application/json",
215215+ body: {
216216+ storiesByAuthor: [],
217217+ },
218218+ };
219219+ }
222220223223- // Check if we have more results (for cursor)
224224- const hasMore = stories.length > limit;
225225- if (hasMore) {
226226- stories.pop(); // Remove the extra item
227227- }
221221+ // Check if we have more results (for cursor)
222222+ const hasMore = stories.length > limit;
223223+ if (hasMore) {
224224+ stories.pop(); // Remove the extra item
225225+ }
228226229229- // Get all unique author DIDs for batch block checking
230230- const authorDids = [
231231- ...new Set(stories.map((story) => story.authorDid)),
232232- ];
227227+ // Get all unique author DIDs for batch block checking
228228+ const authorDids = [
229229+ ...new Set(stories.map((story) => story.authorDid)),
230230+ ];
233231234234- // Batch check all block relationships
235235- const blockedAuthorDids = await batchCheckBlockedAuthors(
236236- ctx,
237237- authorDids,
238238- userDid,
239239- );
232232+ // Batch check all block relationships
233233+ const blockedAuthorDids = await batchCheckBlockedAuthors(
234234+ ctx,
235235+ authorDids,
236236+ userDid,
237237+ );
240238241241- // Filter out stories from blocked authors
242242- const accessibleStories = stories.filter(
243243- (story) => !blockedAuthorDids.has(story.authorDid),
244244- );
239239+ // Filter out stories from blocked authors
240240+ const accessibleStories = stories.filter(
241241+ (story) => !blockedAuthorDids.has(story.authorDid),
242242+ );
245243246246- if (accessibleStories.length === 0) {
247247- return {
248248- encoding: "application/json",
249249- body: {
250250- storiesByAuthor: [],
251251- },
252252- };
253253- }
254254-255255- // Transform stories to story views using batch transformer
256256- const storyViews = await transformStoriesToStoryViews(
257257- accessibleStories,
258258- ctx,
259259- );
260260-261261- // Group stories by author efficiently
262262- const storiesByAuthor = groupStoriesByAuthor(storyViews);
263263-264264- // Generate next cursor if there are more results
265265- let nextCursor: string | undefined;
266266- if (hasMore && accessibleStories.length > 0) {
267267- const lastStory = accessibleStories[accessibleStories.length - 1];
268268- nextCursor = generateCursor(
269269- lastStory.indexedAt,
270270- String(lastStory._id),
271271- );
272272- }
273273-274274- const response = {
275275- storiesByAuthor,
276276- ...(nextCursor && { cursor: nextCursor }),
277277- };
278278-244244+ if (accessibleStories.length === 0) {
279245 return {
280246 encoding: "application/json",
281281- body: response,
247247+ body: {
248248+ storiesByAuthor: [],
249249+ },
282250 };
283283- } catch (error) {
284284- // Handle specific error types
285285- if (error instanceof InvalidRequestError) {
286286- return {
287287- status: 400,
288288- message: error.message,
289289- };
290290- }
251251+ }
291252292292- // Log unexpected errors
293293- console.error("Error fetching stories timeline:", error);
294294-295295- // Handle specific error cases
296296- if (error instanceof Error) {
297297- const message = error.message;
253253+ // Transform stories to story views using batch transformer
254254+ const storyViews = await transformStoriesToStoryViews(
255255+ accessibleStories,
256256+ ctx,
257257+ );
298258299299- // Database connectivity issues
300300- if (message.includes("connection") || message.includes("timeout")) {
301301- return {
302302- status: 503,
303303- message: "Database temporarily unavailable",
304304- };
305305- }
259259+ // Group stories by author efficiently
260260+ const storiesByAuthor = groupStoriesByAuthor(storyViews);
306261307307- // Rate limiting
308308- if (message.includes("limit") || message.includes("quota")) {
309309- return {
310310- status: 429,
311311- message: "Rate limit exceeded",
312312- };
313313- }
262262+ // Generate next cursor if there are more results
263263+ let nextCursor: string | undefined;
264264+ if (hasMore && accessibleStories.length > 0) {
265265+ const lastStory = accessibleStories[accessibleStories.length - 1];
266266+ nextCursor = generateCursor(
267267+ lastStory.indexedAt,
268268+ String(lastStory._id),
269269+ );
270270+ }
314271315315- // Authentication errors
316316- if (message.includes("auth") || message.includes("credential")) {
317317- return {
318318- status: 401,
319319- message: "Authentication required",
320320- };
321321- }
322322- }
272272+ const response = {
273273+ storiesByAuthor,
274274+ ...(nextCursor && { cursor: nextCursor }),
275275+ };
323276324324- // Generic server error
325325- return {
326326- status: 500,
327327- message: "Internal server error",
328328- };
329329- }
277277+ return {
278278+ encoding: "application/json",
279279+ body: response,
280280+ };
330281 },
331282 });
332283}
+4-6
api/so/sprk/feed/getSuggestedFeeds.ts
···33import {
44 BskyGeneratorDocument,
55 SprkGeneratorDocument,
66-} from "../../../../data-plane/server/models.ts";
66+} from "../../../../data-plane/db/models.ts";
77import { getProfileView } from "../../../../utils/profile-helper.ts";
88import type * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts";
99-import { decodeBase64, encodeBase64 } from "jsr:@std/encoding";
99+import { decodeBase64, encodeBase64 } from "@std/encoding";
10101111interface CursorData {
1212 likeCount: number;
···121121 // Get both BskyGenerator and SprkGenerator documents
122122 const [bskyGenerators, sprkGenerators] = await Promise.all([
123123 ctx.db.models.BskyGenerator.find(query)
124124- .sort({ likeCount: -1, _id: -1 })
125125- .lean(),
124124+ .sort({ likeCount: -1, _id: -1 }),
126125 ctx.db.models.SprkGenerator.find(query)
127127- .sort({ likeCount: -1, _id: -1 })
128128- .lean(),
126126+ .sort({ likeCount: -1, _id: -1 }),
129127 ]);
130128131129 // Combine and sort all generators by like count
+2-3
api/so/sprk/feed/getTimeline.ts
···11import { Server } from "../../../../lex/index.ts";
22import { AppContext } from "../../../../main.ts";
33import { transformPostsToPostViews } from "../../../../utils/post-transformer.ts";
44-import { decodeBase64, encodeBase64 } from "jsr:@std/encoding";
44+import { decodeBase64, encodeBase64 } from "@std/encoding";
55import { OutputSchema } from "../../../../lex/types/so/sprk/feed/getTimeline.ts";
6677interface CursorData {
···101101 const query = buildTimelineQuery(followedDids, cursorData);
102102 const posts = await ctx.db.models.Post.find(query)
103103 .sort({ createdAt: -1, _id: -1 })
104104- .limit(limit + 1) // Get one extra for hasMore check
105105- .lean();
104104+ .limit(limit + 1); // Get one extra for hasMore check
106105107106 // Check if there are more results
108107 const hasMore = posts.length > limit;
+1-1
api/so/sprk/feed/searchPosts.ts
···44import * as SoSprkFeedDefs from "../../../../lex/types/so/sprk/feed/defs.ts";
55import { OutputSchema } from "../../../../lex/types/so/sprk/feed/searchPosts.ts";
66import { RootFilterQuery } from "mongoose";
77-import { PostDocument } from "../../../../data-plane/server/models.ts";
77+import { PostDocument } from "../../../../data-plane/db/models.ts";
8899// Helper to escape user input for safe RegExp usage
1010function escapeRegExp(str: string): string {
+4-6
api/so/sprk/graph/getFollowers.ts
···11import { Server } from "../../../../lex/index.ts";
22import { AppContext } from "../../../../main.ts";
33-import { FollowDocument } from "../../../../data-plane/server/models.ts";
44-import { ensureValidDid, isValidHandle } from "@atproto/syntax";
33+import { FollowDocument } from "../../../../data-plane/db/models.ts";
44+import { ensureValidDid, isValidHandle } from "@atp/syntax";
55import { RootFilterQuery } from "mongoose";
66-import { XRPCError } from "@sprk/xrpc-server";
66+import { XRPCError } from "@atp/xrpc-server";
77import { OutputSchema } from "../../../../lex/types/so/sprk/graph/getFollowers.ts";
88import {
99 getProfileView,
···6262 : undefined;
63636464 // Extract follower DIDs and batch fetch profile views
6565- const followerDids = followers.map((follow: FollowDocument) =>
6666- follow.authorDid
6767- );
6565+ const followerDids = followers.map((follow) => follow.authorDid);
6866 const profileViews = await getProfileViews(ctx, followerDids, viewerDid);
69677068 const res = {
+4-6
api/so/sprk/graph/getFollows.ts
···11import { Server } from "../../../../lex/index.ts";
22-import { FollowDocument } from "../../../../data-plane/server/models.ts";
22+import { FollowDocument } from "../../../../data-plane/db/models.ts";
33import { AppContext } from "../../../../main.ts";
44-import { ensureValidDid, isValidHandle } from "@atproto/syntax";
44+import { ensureValidDid, isValidHandle } from "@atp/syntax";
55import { RootFilterQuery } from "mongoose";
66-import { XRPCError } from "@sprk/xrpc-server";
66+import { XRPCError } from "@atp/xrpc-server";
77import { OutputSchema } from "../../../../lex/types/so/sprk/graph/getFollows.ts";
88import {
99 getProfileView,
···6262 : undefined;
63636464 // Extract follow subject DIDs and batch fetch profile views
6565- const followSubjectDids = follows.map((follow: FollowDocument) =>
6666- follow.subject
6767- );
6565+ const followSubjectDids = follows.map((follow) => follow.subject);
6866 const profileViews = await getProfileViews(
6967 ctx,
7068 followSubjectDids,
···11import PQueue from "p-queue";
22-import { Database } from "./index.ts";
22+import { Database } from "./db/index.ts";
33import { Logger } from "@logtape/logtape";
44-import { env } from "../../utils/env.ts";
44+import { env } from "../utils/env.ts";
5566// A simple queue for in-process, out-of-band/backgrounded work
77
+4-5
data-plane/server/index.ts
data-plane/db/index.ts
···11import mongoose, { Connection } from "mongoose";
22-import { IdResolver, MemoryCache } from "@atproto/identity";
22+import { IdResolver, MemoryCache } from "@atp/identity";
33import { env } from "../../utils/env.ts";
44-import { DataPlaneClient, GetIdentityByDidResponse } from "../client/index.ts";
54import * as models from "./models.ts";
66-import { getResultFromDoc } from "./util.ts";
55+import { getResultFromDoc } from "../util.ts";
76import { getLogger } from "@logtape/logtape";
8798const HOUR = 60 * 60 * 1000;
109const DAY = HOUR * 24;
11101212-export class Database implements DataPlaneClient {
1111+export class Database {
1312 private connection!: Connection;
1413 public models!: models.DatabaseModels;
1514 public logger = getLogger(["appview", "database"]);
···199198 // Implement DataPlaneClient interface
200199 async getIdentityByDid(
201200 { did }: { did: string },
202202- ): Promise<GetIdentityByDidResponse> {
201201+ ): Promise<{ did: string; handle?: string } | undefined> {
203202 const doc = await this.idResolver.did.resolve(did);
204203 if (!doc) {
205204 throw new Error("DID not found");
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import * as Block from "../../../../lex/types/app/bsky/graph/block.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import * as Block from "../../../lex/types/app/bsky/graph/block.ts";
55import { BackgroundQueue } from "../../background.ts";
66-import { Database } from "../../index.ts";
77-import { BlockDocument } from "../../models.ts";
66+import { Database } from "../../db/index.ts";
77+import { BlockDocument } from "../../db/models.ts";
88import { RecordProcessor } from "../processor.ts";
991010const lexId = lex.ids.AppBskyGraphBlock;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import * as Follow from "../../../../lex/types/app/bsky/graph/follow.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import * as Follow from "../../../lex/types/app/bsky/graph/follow.ts";
55import { BackgroundQueue } from "../../background.ts";
66-import { Database } from "../../index.ts";
77-import { FollowDocument } from "../../models.ts";
66+import { Database } from "../../db/index.ts";
77+import { FollowDocument } from "../../db/models.ts";
88import { RecordProcessor } from "../processor.ts";
991010const lexId = lex.ids.AppBskyGraphFollow;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../../lex/lexicons.ts";
44-import * as FeedGenerator from "../../../../../lex/types/app/bsky/feed/generator.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../../lex/lexicons.ts";
44+import * as FeedGenerator from "../../../../lex/types/app/bsky/feed/generator.ts";
55import { BackgroundQueue } from "../../../background.ts";
66-import { Database } from "../../../index.ts";
77-import { BskyGeneratorDocument } from "../../../models.ts";
66+import { Database } from "../../../db/index.ts";
77+import { BskyGeneratorDocument } from "../../../db/models.ts";
88import { RecordProcessor } from "../../processor.ts";
991010const lexId = lex.ids.AppBskyFeedGenerator;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../../lex/lexicons.ts";
44-import * as FeedGenerator from "../../../../../lex/types/so/sprk/feed/generator.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../../lex/lexicons.ts";
44+import * as FeedGenerator from "../../../../lex/types/so/sprk/feed/generator.ts";
55import { BackgroundQueue } from "../../../background.ts";
66-import { Database } from "../../../index.ts";
77-import { SprkGeneratorDocument } from "../../../models.ts";
66+import { Database } from "../../../db/index.ts";
77+import { SprkGeneratorDocument } from "../../../db/models.ts";
88import { RecordProcessor } from "../../processor.ts";
991010const lexId = lex.ids.SoSprkFeedGenerator;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import * as Like from "../../../../lex/types/so/sprk/feed/like.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import * as Like from "../../../lex/types/so/sprk/feed/like.ts";
55import { BackgroundQueue } from "../../background.ts";
66-import { Database } from "../../index.ts";
77-import { LikeDocument } from "../../models.ts";
66+import { Database } from "../../db/index.ts";
77+import { LikeDocument } from "../../db/models.ts";
88import { RecordProcessor } from "../processor.ts";
991010const lexId = lex.ids.SoSprkFeedLike;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import * as Music from "../../../../lex/types/so/sprk/feed/music.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import * as Music from "../../../lex/types/so/sprk/feed/music.ts";
55import { BackgroundQueue } from "../../background.ts";
66-import { Database } from "../../index.ts";
77-import { MusicDocument } from "../../models.ts";
66+import { Database } from "../../db/index.ts";
77+import { MusicDocument } from "../../db/models.ts";
88import { RecordProcessor } from "../processor.ts";
99-import { normalizeObject } from "../../../../utils/embed-normalizer.ts";
99+import { normalizeObject } from "../../../utils/embed-normalizer.ts";
10101111const lexId = lex.ids.SoSprkFeedMusic;
1212type IndexedMusic = MusicDocument;
···11import { CID } from "multiformats/cid";
22-import { AtUri } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import { isMain as isEmbedImage } from "../../../../lex/types/so/sprk/embed/images.ts";
55-import { isMain as isEmbedVideo } from "../../../../lex/types/so/sprk/embed/video.ts";
22+import { AtUri } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import { isMain as isEmbedImage } from "../../../lex/types/so/sprk/embed/images.ts";
55+import { isMain as isEmbedVideo } from "../../../lex/types/so/sprk/embed/video.ts";
66import {
77 Record as PostRecord,
88 ReplyRef,
99-} from "../../../../lex/types/so/sprk/feed/post.ts";
1010-import { Record as GateRecord } from "../../../../lex/types/so/sprk/feed/threadgate.ts";
99+} from "../../../lex/types/so/sprk/feed/post.ts";
1010+import { Record as GateRecord } from "../../../lex/types/so/sprk/feed/threadgate.ts";
1111import {
1212 isLink,
1313 isMention,
1414-} from "../../../../lex/types/so/sprk/richtext/facet.ts";
1414+} from "../../../lex/types/so/sprk/richtext/facet.ts";
1515import { BackgroundQueue } from "../../background.ts";
1616-import { Database } from "../../index.ts";
1717-import { PostDocument } from "../../models.ts";
1616+import { Database } from "../../db/index.ts";
1717+import { PostDocument } from "../../db/models.ts";
1818import {
1919 getAncestorsAndSelf,
2020 getDescendents,
···2424import {
2525 normalizeEmbed,
2626 normalizeObject,
2727-} from "../../../../utils/embed-normalizer.ts";
2828-import { jsonToLex } from "@atproto/api";
2727+} from "../../../utils/embed-normalizer.ts";
2828+import { jsonToLex } from "@atp/lexicon";
29293030type PostAncestor = {
3131 uri: string;
···11import { CID } from "multiformats/cid";
22-import { AtUri, normalizeDatetimeAlways } from "@atproto/syntax";
33-import * as lex from "../../../../lex/lexicons.ts";
44-import * as Story from "../../../../lex/types/so/sprk/feed/story.ts";
22+import { AtUri, normalizeDatetimeAlways } from "@atp/syntax";
33+import * as lex from "../../../lex/lexicons.ts";
44+import * as Story from "../../../lex/types/so/sprk/feed/story.ts";
55import { BackgroundQueue } from "../../background.ts";
66-import { Database } from "../../index.ts";
77-import { StoryDocument } from "../../models.ts";
66+import { Database } from "../../db/index.ts";
77+import { StoryDocument } from "../../db/models.ts";
88import { RecordProcessor } from "../processor.ts";
99import {
1010 normalizeEmbed,
1111 normalizeObject,
1212-} from "../../../../utils/embed-normalizer.ts";
1212+} from "../../../utils/embed-normalizer.ts";
13131414const lexId = lex.ids.SoSprkFeedStory;
1515type IndexedStory = StoryDocument;
···11import { CID } from "multiformats/cid";
22-import { stringifyLex } from "@atproto/lexicon";
33-import { AtUri } from "@atproto/syntax";
44-import { lexicons } from "../../../lex/lexicons.ts";
22+import { jsonStringToLex, stringifyLex } from "@atp/lexicon";
33+import { AtUri } from "@atp/syntax";
44+import { lexicons } from "../../lex/lexicons.ts";
55import { BackgroundQueue } from "../background.ts";
66-import { Database } from "../index.ts";
77-import { jsonToLex } from "@atproto/api";
66+import { Database } from "../db/index.ts";
8798// @NOTE re: insertions and deletions. Due to how record updates are handled,
109// (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn).
···260259 return this.handleNotifs({ deleted });
261260 }
262261263263- const record = jsonToLex(recordDoc.json);
262262+ const record = jsonStringToLex(recordDoc.json);
264263 if (!this.matchesSchema(record)) {
265264 return this.handleNotifs({ deleted });
266265 }
···66 Lexicons,
77 ValidationError,
88 type ValidationResult,
99-} from "@atproto/lexicon";
99+} from "@atp/lexicon";
1010import { is$typed, maybe$typed } from "./util.ts";
11111212export const schemaDict = {
···75237523 "type": "string",
75247524 "format": "datetime",
75257525 },
75267526+ "via": {
75277527+ "type": "ref",
75287528+ "ref": "lex:com.atproto.repo.strongRef",
75297529+ },
75267530 },
75277531 },
75287532 },
···76197623 "createdAt": {
76207624 "type": "string",
76217625 "format": "datetime",
76267626+ },
76277627+ "via": {
76287628+ "type": "ref",
76297629+ "ref": "lex:com.atproto.repo.strongRef",
76227630 },
76237631 },
76247632 },
···1516015168 "main": {
1516115169 "type": "record",
1516215170 "description":
1516315163- "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.",
1517115171+ "Record declaring a 'like' of a piece of subject content.",
1516415172 "key": "tid",
1516515173 "record": {
1516615174 "type": "object",
···1517615184 "createdAt": {
1517715185 "type": "string",
1517815186 "format": "datetime",
1518715187+ },
1518815188+ "via": {
1518915189+ "type": "ref",
1519015190+ "ref": "lex:com.atproto.repo.strongRef",
1517915191 },
1518015192 },
1518115193 },
···1527315285 "createdAt": {
1527415286 "type": "string",
1527515287 "format": "datetime",
1528815288+ },
1528915289+ "via": {
1529015290+ "type": "ref",
1529115291+ "ref": "lex:com.atproto.repo.strongRef",
1527615292 },
1527715293 },
1527815294 },
+1-1
lex/types/app/bsky/actor/profile.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
+1-1
lex/types/app/bsky/embed/external.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77
+1-1
lex/types/app/bsky/embed/images.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import type * as AppBskyEmbedDefs from "./defs.ts";
+1-1
lex/types/app/bsky/embed/video.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import type * as AppBskyEmbedDefs from "./defs.ts";
+1-1
lex/types/app/bsky/feed/generator.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
+1-1
lex/types/app/bsky/video/defs.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77
+1-1
lex/types/com/atproto/label/subscribeLabels.ts
···44import { validate as _validate } from "../../../../lexicons.ts";
55import { is$typed as _is$typed } from "../../../../util.ts";
66import { type $Typed } from "../../../../util.ts";
77-import { ErrorFrame } from "@sprk/xrpc-server";
77+import { ErrorFrame } from "@atp/xrpc-server";
88import type * as ComAtprotoLabelDefs from "./defs.ts";
991010const is$typed = _is$typed, validate = _validate;
+1-1
lex/types/com/atproto/repo/uploadBlob.ts
···22 * GENERATED CODE - DO NOT MODIFY
33 */
44import stream from "node:stream";
55-import { BlobRef } from "@atproto/lexicon";
55+import { BlobRef } from "@atp/lexicon";
6677export type QueryParams = globalThis.Record<PropertyKey, never>;
88export type InputSchema = string | Uint8Array | Blob;
+1-1
lex/types/com/atproto/sync/subscribeRepos.ts
···55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
88-import { ErrorFrame } from "@sprk/xrpc-server";
88+import { ErrorFrame } from "@atp/xrpc-server";
991010const is$typed = _is$typed, validate = _validate;
1111const id = "com.atproto.sync.subscribeRepos";
+1-1
lex/types/so/sprk/actor/profile.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
+1-1
lex/types/so/sprk/embed/images.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import type * as SoSprkEmbedDefs from "./defs.ts";
+1-1
lex/types/so/sprk/embed/video.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import type * as SoSprkEmbedDefs from "./defs.ts";
+1-1
lex/types/so/sprk/feed/generator.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
+1-1
lex/types/so/sprk/sound/audio.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import { type $Typed } from "../../../../util.ts";
+1-1
lex/types/so/sprk/video/defs.ts
···11/**
22 * GENERATED CODE - DO NOT MODIFY
33 */
44-import { BlobRef } from "@atproto/lexicon";
44+import { BlobRef } from "@atp/lexicon";
55import { validate as _validate } from "../../../../lexicons.ts";
66import { is$typed as _is$typed } from "../../../../util.ts";
77import type * as SoSprkSoundDefs from "../sound/defs.ts";
+1-1
lex/util.ts
···22 * GENERATED CODE - DO NOT MODIFY
33 */
4455-import { type ValidationResult } from "@atproto/lexicon";
55+import { type ValidationResult } from "@atp/lexicon";
6677export type OmitKey<T, K extends keyof T> = {
88 [K2 in keyof T as K2 extends K ? never : K2]: T[K2];
···11import type * as SoSprkSoundDefs from "../lex/types/so/sprk/sound/defs.ts";
22-import { AudioDocument } from "../data-plane/server/models.ts";
22+import { AudioDocument } from "../data-plane/db/models.ts";
33import { AppContext } from "../main.ts";
44import { createProfileViewBasic } from "./profile-helper.ts";
55import type { Label } from "../lex/types/com/atproto/label/defs.ts";
+1-1
utils/embed-transformer.ts
···33 EmbedImage,
44 PostEmbed,
55 VideoMappingDocument,
66-} from "../data-plane/server/models.ts";
66+} from "../data-plane/db/models.ts";
77import { env } from "./env.ts";
8899interface ImageTransformOptions {
···11-import { AtprotoData, IdResolver, MemoryCache } from "@atproto/identity";
11+import { AtprotoData, IdResolver, MemoryCache } from "@atp/identity";
2233const HOUR = 60e3 * 60;
44const DAY = HOUR * 24;
-116
utils/memory-runner.ts
···11-import PQueue from "p-queue";
22-import { ConsecutiveList, EventRunner } from "@atproto/sync";
33-44-export type MemoryRunnerOptions = {
55- setCursor?: (cursor: number) => Promise<void> | void;
66- concurrency?: number;
77- startCursor?: number;
88- cursorSaveIntervalMs?: number;
99-};
1010-1111-// A queue with arbitrarily many partitions, each processing work sequentially.
1212-// Partitions are created lazily and taken out of memory when they go idle.
1313-export class MemoryRunner implements EventRunner {
1414- consecutive = new ConsecutiveList<number>();
1515- mainQueue: PQueue;
1616- partitions = new Map<string, PQueue>();
1717- cursor: number | undefined;
1818- private lastSaveTime = 0;
1919- private lastCursor: number | undefined;
2020- private saveTimeout: number | undefined;
2121-2222- constructor(public opts: MemoryRunnerOptions = {}) {
2323- this.mainQueue = new PQueue({ concurrency: opts.concurrency ?? Infinity });
2424- this.cursor = opts.startCursor;
2525- }
2626-2727- getCursor() {
2828- return this.cursor;
2929- }
3030-3131- addTask(partitionId: string, task: () => Promise<void>) {
3232- if (this.mainQueue.isPaused) return;
3333- return this.mainQueue.add(() => {
3434- return this.getPartition(partitionId).add(task);
3535- });
3636- }
3737-3838- private getPartition(partitionId: string) {
3939- let partition = this.partitions.get(partitionId);
4040- if (!partition) {
4141- partition = new PQueue({ concurrency: 1 });
4242- partition.once("idle", () => this.partitions.delete(partitionId));
4343- this.partitions.set(partitionId, partition);
4444- }
4545- return partition;
4646- }
4747-4848- async trackEvent(did: string, seq: number, handler: () => Promise<void>) {
4949- if (this.mainQueue.isPaused) return;
5050- const item = this.consecutive.push(seq);
5151- await this.addTask(did, async () => {
5252- await handler();
5353- const latest = item.complete().at(-1);
5454- if (latest !== undefined) {
5555- this.cursor = latest;
5656- if (this.opts.setCursor) {
5757- await this.throttledSaveCursor(this.cursor);
5858- }
5959- }
6060- });
6161- }
6262-6363- async processAll() {
6464- await this.mainQueue.onIdle();
6565- }
6666-6767- async destroy() {
6868- this.mainQueue.pause();
6969- await this.mainQueue.onIdle();
7070- this.mainQueue.clear();
7171- this.partitions.forEach((p) => p.clear());
7272-7373- // Force save the latest cursor before shutdown
7474- await this.forceSaveCursor();
7575- }
7676-7777- private async throttledSaveCursor(cursor: number): Promise<void> {
7878- if (!this.opts.setCursor) return;
7979-8080- this.lastCursor = cursor;
8181- const now = Date.now();
8282- const saveInterval = this.opts.cursorSaveIntervalMs ?? 30000;
8383-8484- // If we haven't saved recently, save immediately
8585- if (now - this.lastSaveTime >= saveInterval) {
8686- this.lastSaveTime = now;
8787- await this.opts.setCursor(cursor);
8888- } else {
8989- // Schedule a save for later if not already scheduled
9090- if (this.saveTimeout === undefined) {
9191- const timeUntilNextSave = saveInterval - (now - this.lastSaveTime);
9292- this.saveTimeout = setTimeout(async () => {
9393- try {
9494- if (this.lastCursor !== undefined && this.opts.setCursor) {
9595- this.lastSaveTime = Date.now();
9696- await this.opts.setCursor(this.lastCursor);
9797- }
9898- } catch (err) {
9999- console.error("Error saving cursor in setTimeout:", err);
100100- }
101101- this.saveTimeout = undefined;
102102- }, timeUntilNextSave);
103103- }
104104- }
105105- }
106106-107107- async forceSaveCursor(): Promise<void> {
108108- if (this.saveTimeout !== undefined) {
109109- clearTimeout(this.saveTimeout);
110110- this.saveTimeout = undefined;
111111- }
112112- if (this.lastCursor !== undefined && this.opts.setCursor) {
113113- await this.opts.setCursor(this.lastCursor);
114114- }
115115- }
116116-}
+2-2
utils/post-transformer.ts
···11-import { PostDocument } from "../data-plane/server/models.ts";
11+import { PostDocument } from "../data-plane/db/models.ts";
22import type { Label } from "../lex/types/com/atproto/label/defs.ts";
33import type * as SoSprkFeedDefs from "../lex/types/so/sprk/feed/defs.ts";
44import type * as SoSprkFeedPost from "../lex/types/so/sprk/feed/post.ts";
···6464 .filter((p) => p.embed?.$type === "so.sprk.embed.video")
6565 .map((p) => `${p.authorDid}-${p.embed?.video?.ref.$link}`),
6666 },
6767- }).lean(),
6767+ }),
6868 // Get viewer likes
6969 userDid
7070 ? ctx.db.models.Like.find({
+6-8
utils/profile-helper.ts
···66 ViewerState,
77} from "../lex/types/so/sprk/actor/defs.ts";
88import type * as ComAtprotoRepoStrongRef from "../lex/types/com/atproto/repo/strongRef.ts";
99-import type { StoryDocument } from "../data-plane/server/models.ts";
99+import type { StoryDocument } from "../data-plane/db/models.ts";
1010import type { Label } from "../lex/types/com/atproto/label/defs.ts";
1111-import { ensureValidDid, isValidHandle } from "@atproto/syntax";
1111+import { ensureValidDid, isValidHandle } from "@atp/syntax";
1212import { AppContext } from "../main.ts";
1313-import { XRPCError } from "@sprk/xrpc-server";
1313+import { XRPCError } from "@atp/xrpc-server";
14141515// Helper function to create ProfileViewBasic with stories
1616export async function createProfileViewBasic(
···2121 // Get author profile data
2222 const profile = await ctx.db.models.Profile.findOne({
2323 authorDid: authorDid,
2424- }).lean();
2424+ });
2525 const actor = await ctx.db.models.Actor.findOne({
2626 did: authorDid,
2727- }).lean();
2727+ });
2828 const authorHandle = actor?.handle ?? "unknown.invalid";
29293030 let stories: ComAtprotoRepoStrongRef.Main[] = [];
···4141 indexedAt: { $gte: twentyFourHoursAgo.toISOString() },
4242 })
4343 .sort({ indexedAt: -1 })
4444- .limit(15)
4545- .lean();
4444+ .limit(15);
46454746 // Convert recent stories to strongRefs
4847 stories = recentStories.map((story: StoryDocument) => ({
···360359 })
361360 .sort({ indexedAt: -1 })
362361 .limit(15)
363363- .lean()
364362 .catch((error: Error) => {
365363 ctx.logger.warn(
366364 "Failed to fetch stories for profile",
+2-2
utils/retry.ts
···11-import { createRetryable } from "@atproto/common";
22-import { ResponseType, XRPCError } from "@atproto/xrpc";
11+import { createRetryable } from "@atp/common";
22+import { ResponseType, XRPCError } from "@atp/xrpc";
3344export const RETRYABLE_HTTP_STATUS_CODES = new Set([
55 408,
+1-1
utils/story-transformer.ts
···11import type * as SoSprkFeedDefs from "../lex/types/so/sprk/feed/defs.ts";
22-import { StoryDocument } from "../data-plane/server/models.ts";
22+import { StoryDocument } from "../data-plane/db/models.ts";
33import { transformEmbed } from "./embed-transformer.ts";
44import { createProfileViewBasic } from "./profile-helper.ts";
55import { AppContext } from "../main.ts";
+61
utils/uris.ts
···11+import { AtUri } from "@atp/syntax";
22+import { ids } from "../lex/lexicons.ts";
33+import {
44+ Main as StrongRef,
55+ validateMain as validateStrongRef,
66+} from "../lex/types/com/atproto/repo/strongRef.ts";
77+88+/**
99+ * Convert a post URI to a threadgate URI. If the URI is not a valid
1010+ * post URI, return URI unchanged. Threadgate lookups will then fail.
1111+ * Threadgates aren't implemented yet but will be in the future.
1212+ */
1313+export function postUriToThreadgateUri(postUri: string) {
1414+ const urip = new AtUri(postUri);
1515+ if (urip.collection === ids.AppBskyFeedPost) {
1616+ urip.collection = ids.AppBskyFeedThreadgate;
1717+ }
1818+ return urip.toString();
1919+}
2020+2121+/**
2222+ * Convert a post URI to a postgate URI. If the URI is not a valid
2323+ * post URI, return URI unchanged. Postgate lookups will then fail.
2424+ * Postgates aren't implemented yet but will be in the future.
2525+ */
2626+export function postUriToPostgateUri(postUri: string) {
2727+ const urip = new AtUri(postUri);
2828+ if (urip.collection === ids.AppBskyFeedPost) {
2929+ urip.collection = ids.AppBskyFeedPostgate;
3030+ }
3131+ return urip.toString();
3232+}
3333+3434+export function uriToDid(uri: string) {
3535+ try {
3636+ return new AtUri(uri).hostname;
3737+ } catch (error) {
3838+ console.log(`AtUri parser failed for URI: ${uri}, error:`, error);
3939+ // Handle custom collection namespaces that AtUri might not recognize
4040+ // Extract DID from URI manually as fallback
4141+ const match = uri.match(/^at:\/\/(did:[^\/]+)/);
4242+ if (match) {
4343+ console.log(`Successfully extracted DID using fallback: ${match[1]}`);
4444+ return match[1];
4545+ }
4646+ console.error(`Failed to extract DID from URI: ${uri}`);
4747+ throw new Error(`Invalid AT URI format: ${uri}`);
4848+ }
4949+}
5050+5151+// @TODO temp fix for proliferation of invalid pinned post values
5252+export function safePinnedPost(value: unknown) {
5353+ if (!value || typeof value !== "object") {
5454+ return;
5555+ }
5656+ const validated = validateStrongRef(value);
5757+ if (!validated.success) {
5858+ return;
5959+ }
6060+ return validated.value as StrongRef;
6161+}
···11+import {
22+ Main as ImagesEmbed,
33+ View as ImagesEmbedView,
44+} from "../lex/types/so/sprk/embed/images.ts";
55+import {
66+ Main as VideoEmbed,
77+ View as VideoEmbedView,
88+} from "../lex/types/so/sprk/embed/video.ts";
99+import {
1010+ BlockedPost,
1111+ GeneratorView,
1212+ NotFoundPost,
1313+ PostView,
1414+} from "../lex/types/so/sprk/feed/defs.ts";
1515+import { LabelerView } from "../lex/types/app/bsky/labeler/defs.ts";
1616+1717+export type {
1818+ Main as ImagesEmbed,
1919+ View as ImagesEmbedView,
2020+} from "../lex/types/so/sprk/embed/images.ts";
2121+export { isMain as isImagesEmbed } from "../lex/types/so/sprk/embed/images.ts";
2222+export type {
2323+ Main as VideoEmbed,
2424+ View as VideoEmbedView,
2525+} from "../lex/types/so/sprk/embed/video.ts";
2626+export { isMain as isVideoEmbed } from "../lex/types/so/sprk/embed/video.ts";
2727+export type {
2828+ BlockedPost,
2929+ GeneratorView,
3030+ NotFoundPost,
3131+ PostView,
3232+} from "../lex/types/so/sprk/feed/defs.ts";
3333+3434+export type Embed =
3535+ | ImagesEmbed
3636+ | VideoEmbed;
3737+3838+export type EmbedView =
3939+ | ImagesEmbedView
4040+ | VideoEmbedView;
4141+4242+export type MaybePostView = PostView | NotFoundPost | BlockedPost;
4343+4444+export type RecordEmbedViewInternal =
4545+ | GeneratorView
4646+ | LabelerView;
+40
views/util.ts
···11+import { BlobRef } from "@atp/lexicon";
22+33+// Simple string format function to replace util.format
44+const format = (template: string, ...args: string[]): string => {
55+ return template.replace(/%s/g, () => args.shift() || "");
66+};
77+88+export const cidFromBlobJson = (json: BlobRef) => {
99+ if (json instanceof BlobRef) {
1010+ return json.ref.toString();
1111+ }
1212+ // @NOTE below handles the fact that parseRecordBytes() produces raw json rather than lexicon values
1313+ if (json["$type"] === "blob") {
1414+ return (json["ref"]?.["$link"] ?? "") as string;
1515+ }
1616+ return (json["cid"] ?? "") as string;
1717+};
1818+1919+export class VideoUriBuilder {
2020+ constructor(
2121+ private opts: {
2222+ playlistUrlPattern: string; // e.g. https://hostname/vid/%s/%s/playlist.m3u8
2323+ thumbnailUrlPattern: string; // e.g. https://hostname/vid/%s/%s/thumbnail.jpg
2424+ },
2525+ ) {}
2626+ playlist({ did, cid }: { did: string; cid: string }) {
2727+ return format(
2828+ this.opts.playlistUrlPattern,
2929+ encodeURIComponent(did),
3030+ encodeURIComponent(cid),
3131+ );
3232+ }
3333+ thumbnail({ did, cid }: { did: string; cid: string }) {
3434+ return format(
3535+ this.opts.thumbnailUrlPattern,
3636+ encodeURIComponent(did),
3737+ encodeURIComponent(cid),
3838+ );
3939+ }
4040+}