[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

revert stories archive

-686
-2
api/index.ts
··· 21 21 import resolveHandle from "./com/atproto/identity/resolveHandle.ts"; 22 22 import getStories from "./so/sprk/story/getStories.ts"; 23 23 import getStoriesTimeline from "./so/sprk/story/getTimeline.ts"; 24 - import getStoriesArchive from "./so/sprk/story/getArchive.ts"; 25 24 import getProfiles from "./so/sprk/actor/getProfiles.ts"; 26 25 import searchPosts from "./so/sprk/feed/searchPosts.ts"; 27 26 import getActorAudios from "./so/sprk/sound/getActorAudios.ts"; ··· 61 60 resolveHandle(server, ctx); 62 61 getStories(server, ctx); 63 62 getStoriesTimeline(server, ctx); 64 - getStoriesArchive(server, ctx); 65 63 searchPosts(server, ctx); 66 64 getActorAudios(server, ctx); 67 65 getTrendingAudios(server, ctx);
-143
api/so/sprk/story/getArchive.ts
··· 1 - import { InvalidRequestError } from "@atp/xrpc-server"; 2 - import { mapDefined } from "@atp/common"; 3 - import { AppContext } from "../../../../context.ts"; 4 - import { HydrateCtx, HydrationState } from "../../../../hydration/index.ts"; 5 - import { parseString } from "../../../../hydration/util.ts"; 6 - import { Server } from "../../../../lex/index.ts"; 7 - import { 8 - OutputSchema, 9 - QueryParams, 10 - } from "../../../../lex/types/so/sprk/story/getArchive.ts"; 11 - import { 12 - createPipeline, 13 - HydrationFnInput, 14 - PresentationFnInput, 15 - RulesFnInput, 16 - SkeletonFnInput, 17 - } from "../../../../pipeline.ts"; 18 - import { uriToDid } from "../../../../utils/uris.ts"; 19 - import { resHeaders } from "../../../util.ts"; 20 - 21 - const MAX_LIMIT = 100; 22 - const DEFAULT_LIMIT = 50; 23 - 24 - export default function (server: Server, ctx: AppContext) { 25 - const getArchive = createPipeline(skeleton, hydration, rules, presentation); 26 - server.so.sprk.story.getArchive({ 27 - auth: ctx.authVerifier.standard, 28 - handler: async ({ params, auth, req }) => { 29 - const { includeTakedowns } = ctx.authVerifier.parseCreds(auth); 30 - const viewer = auth.credentials.iss; 31 - const labelers = ctx.reqLabelers(req); 32 - const hydrateCtx = await ctx.hydrator.createContext({ 33 - viewer, 34 - labelers, 35 - includeTakedowns, 36 - }); 37 - 38 - const { limit: limitParam = DEFAULT_LIMIT, cursor } = params; 39 - const limit = typeof limitParam === "string" 40 - ? parseInt(limitParam, 10) 41 - : limitParam; 42 - 43 - if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) { 44 - throw new InvalidRequestError( 45 - `Invalid limit: must be between 1 and ${MAX_LIMIT}`, 46 - ); 47 - } 48 - 49 - const [result, repoRev] = await Promise.all([ 50 - getArchive( 51 - { 52 - ...params, 53 - limit, 54 - cursor, 55 - hydrateCtx: hydrateCtx.copy({ viewer, includeTakedowns }), 56 - }, 57 - ctx, 58 - ), 59 - ctx.hydrator.actor.getRepoRevSafe(viewer), 60 - ]); 61 - 62 - return { 63 - encoding: "application/json", 64 - body: result, 65 - headers: resHeaders({ 66 - repoRev, 67 - labelers: hydrateCtx.labelers, 68 - }), 69 - }; 70 - }, 71 - }); 72 - } 73 - 74 - const skeleton = async ( 75 - inputs: SkeletonFnInput<Context, Params>, 76 - ): Promise<Skeleton> => { 77 - const { ctx, params } = inputs; 78 - const viewer = params.hydrateCtx.viewer!; 79 - const res = await ctx.dataplane.stories.getArchive( 80 - viewer, 81 - params.limit, 82 - params.cursor, 83 - params.hydrateCtx.includeTakedowns || false, 84 - ); 85 - 86 - return { 87 - stories: res.stories.map((story) => story.uri), 88 - cursor: parseString(res.cursor), 89 - }; 90 - }; 91 - 92 - const hydration = async ( 93 - inputs: HydrationFnInput<Context, Params, Skeleton>, 94 - ): Promise<HydrationState> => { 95 - const { ctx, params, skeleton } = inputs; 96 - const authorDids = [...new Set(skeleton.stories.map((uri) => uriToDid(uri)))]; 97 - 98 - const [stories, actors] = await Promise.all([ 99 - ctx.hydrator.story.getArchivedStories( 100 - skeleton.stories, 101 - params.hydrateCtx.includeTakedowns || false, 102 - ), 103 - ctx.hydrator.actor.getActors(authorDids, params.hydrateCtx), 104 - ]); 105 - 106 - return { 107 - stories, 108 - actors, 109 - }; 110 - }; 111 - 112 - const rules = (inputs: RulesFnInput<Context, Params, Skeleton>): Skeleton => { 113 - const { skeleton, hydration } = inputs; 114 - const availableStories = skeleton.stories.filter((uri) => { 115 - return Boolean(hydration.stories?.get(uri)); 116 - }); 117 - return { stories: availableStories, cursor: skeleton.cursor }; 118 - }; 119 - 120 - const presentation = ( 121 - inputs: PresentationFnInput<Context, Params, Skeleton>, 122 - ): OutputSchema => { 123 - const { ctx, skeleton, hydration } = inputs; 124 - const storyViews = mapDefined(skeleton.stories, (uri) => { 125 - return ctx.views.story(uri, hydration); 126 - }); 127 - return { 128 - stories: storyViews, 129 - ...(skeleton.cursor && { cursor: skeleton.cursor }), 130 - }; 131 - }; 132 - 133 - type Context = AppContext; 134 - 135 - type Params = QueryParams & { 136 - hydrateCtx: HydrateCtx & { viewer: string }; 137 - limit: number; 138 - }; 139 - 140 - type Skeleton = { 141 - stories: string[]; 142 - cursor?: string; 143 - };
-4
data-plane/db/index.ts
··· 55 55 "Record", 56 56 models.recordSchema, 57 57 ), 58 - ArchivedRecord: this.connection.model<models.ArchivedRecordDocument>( 59 - "ArchivedRecord", 60 - models.archivedRecordSchema, 61 - ), 62 58 DuplicateRecord: this.connection.model<models.DuplicateRecordDocument>( 63 59 "DuplicateRecord", 64 60 models.duplicateRecordSchema,
-34
data-plane/db/models.ts
··· 114 114 takedownRef: string; 115 115 invalidReplyRoot?: boolean; 116 116 } 117 - 118 - export interface ArchivedRecordDocument { 119 - uri: string; 120 - cid: string; 121 - did: string; 122 - collectionName: string; 123 - rkey: string; 124 - createdAt: string; 125 - indexedAt: string; 126 - json: string; 127 - archivedAt: string; 128 - deleteReason: "user_delete" | "takedown"; 129 - takedownRef?: string; 130 - } 131 117 export const recordSchema = new Schema<RecordDocument>({ 132 118 uri: { type: String, required: true, unique: true, index: true }, 133 119 cid: { type: String, required: true }, ··· 141 127 takedownRef: { type: String, required: false }, 142 128 invalidReplyRoot: { type: Boolean, required: false }, 143 129 }); 144 - 145 - export const archivedRecordSchema = new Schema<ArchivedRecordDocument>({ 146 - uri: { type: String, required: true, unique: true, index: true }, 147 - cid: { type: String, required: true }, 148 - did: { type: String, required: true, index: true }, 149 - collectionName: { type: String, required: true, index: true }, 150 - rkey: { type: String, required: true }, 151 - createdAt: { type: String, required: true }, 152 - indexedAt: { type: String, required: true }, 153 - json: { type: String, required: true }, 154 - archivedAt: { type: String, required: true }, 155 - deleteReason: { 156 - type: String, 157 - required: true, 158 - enum: ["user_delete", "takedown"], 159 - }, 160 - takedownRef: { type: String, required: false }, 161 - }) 162 - .index({ did: 1, collectionName: 1, indexedAt: -1 }); 163 130 164 131 // duplicate records 165 132 ··· 699 666 700 667 export interface DatabaseModels { 701 668 Record: Model<RecordDocument>; 702 - ArchivedRecord: Model<ArchivedRecordDocument>; 703 669 DuplicateRecord: Model<DuplicateRecordDocument>; 704 670 Like: Model<LikeDocument>; 705 671 Post: Model<PostDocument>;
-1
data-plane/indexing/plugins/story.ts
··· 70 70 insertFn, 71 71 findDuplicate, 72 72 deleteFn, 73 - archiveOnDelete: true, 74 73 notifsForInsert, 75 74 notifsForDelete, 76 75 });
-23
data-plane/indexing/processor.ts
··· 30 30 replacedBy: S | null, 31 31 ) => { notifs: Notif[]; toDelete: string[] }; 32 32 updateAggregates?: (db: Database, obj: S) => Promise<void>; 33 - archiveOnDelete?: boolean; 34 33 }; 35 34 36 35 type Notif = { ··· 247 246 248 247 async deleteRecord(uri: AtUri, cascading = false) { 249 248 const uriStr = uri.toString(); 250 - const record = await this.db.models.Record.findOne({ uri: uriStr }).lean(); 251 - 252 - if (record && this.params.archiveOnDelete) { 253 - const isTakedown = !!record.takedownRef; 254 - await this.db.models.ArchivedRecord.findOneAndUpdate( 255 - { uri: uriStr }, 256 - { 257 - uri: record.uri, 258 - cid: record.cid, 259 - did: record.did, 260 - collectionName: record.collectionName, 261 - rkey: record.rkey, 262 - createdAt: record.createdAt, 263 - indexedAt: record.indexedAt, 264 - json: record.json, 265 - archivedAt: new Date().toISOString(), 266 - deleteReason: isTakedown ? "takedown" : "user_delete", 267 - takedownRef: record.takedownRef || undefined, 268 - }, 269 - { upsert: true, new: true }, 270 - ); 271 - } 272 249 273 250 await this.db.models.Record.deleteOne({ uri: uriStr }); 274 251 await this.db.models.DuplicateRecord.deleteOne({ uri: uriStr });
-48
data-plane/routes/records.ts
··· 60 60 return { records }; 61 61 } 62 62 63 - export async function getArchivedRecords( 64 - db: Database, 65 - uris: string[], 66 - collection?: string, 67 - ): Promise<{ 68 - records: Array<Record>; 69 - }> { 70 - const validUris = collection 71 - ? uris.filter((uri) => new AtUri(uri).collection === collection) 72 - : uris; 73 - 74 - const res = validUris.length 75 - ? await db.models.ArchivedRecord.find({ 76 - uri: { $in: validUris }, 77 - }) 78 - : []; 79 - 80 - const byUri = keyBy(res, "uri"); 81 - 82 - const records: Record[] = uris.map((uri) => { 83 - const row = byUri.get(uri); 84 - const createdAt = row?.createdAt 85 - ? new Date(row.createdAt).toISOString() 86 - : undefined; 87 - const indexedAt = row?.indexedAt 88 - ? new Date(row.indexedAt).toISOString() 89 - : undefined; 90 - 91 - return { 92 - record: row?.json ?? JSON.stringify(null), 93 - uri, 94 - cid: row?.cid, 95 - createdAt, 96 - indexedAt, 97 - sortedAt: compositeTime(createdAt, indexedAt), 98 - takenDown: !!row?.takedownRef, 99 - takedownRef: row?.takedownRef || undefined, 100 - }; 101 - }); 102 - 103 - return { records }; 104 - } 105 - 106 63 // Helper function to get post records with metadata 107 64 async function getPostRecords( 108 65 db: Database, ··· 200 157 201 158 async getStoryRecords(uris: string[]) { 202 159 const result = await getRecords(this.db, uris, ids.SoSprkStoryPost); 203 - return result; 204 - } 205 - 206 - async getArchivedStoryRecords(uris: string[]) { 207 - const result = await getArchivedRecords(this.db, uris, ids.SoSprkStoryPost); 208 160 return result; 209 161 } 210 162 }
-62
data-plane/routes/stories.ts
··· 1 1 import { Database } from "../db/index.ts"; 2 2 import { TimeCidKeyset } from "../db/pagination.ts"; 3 3 import { compositeTime } from "../util.ts"; 4 - import { ids } from "../../lex/lexicons.ts"; 5 4 6 5 const STORIES_EXPIRY_HOURS = 24; 7 6 ··· 115 114 })); 116 115 117 116 // Generate cursor from last item if we have more results 118 - let nextCursor: string | undefined; 119 - if (hasMore && resultStories.length > 0) { 120 - nextCursor = this.timeCidKeyset.packFromResult(resultStories); 121 - } 122 - 123 - return { 124 - stories: transformedStories, 125 - cursor: nextCursor, 126 - }; 127 - } 128 - 129 - /** 130 - * Get archived stories for an author 131 - */ 132 - async getArchive( 133 - actorDid: string, 134 - limit = 50, 135 - cursor?: string, 136 - includeTakedowns = false, 137 - ): Promise<{ stories: StoryItem[]; cursor?: string }> { 138 - const baseQuery: { 139 - did: string; 140 - collectionName: string; 141 - $or?: Array< 142 - { takedownRef?: { $exists: boolean } } | { takedownRef: string } 143 - >; 144 - } = { 145 - did: actorDid, 146 - collectionName: ids.SoSprkStoryPost, 147 - }; 148 - 149 - if (!includeTakedowns) { 150 - baseQuery.$or = [ 151 - { takedownRef: { $exists: false } }, 152 - { takedownRef: "" }, 153 - ]; 154 - } 155 - 156 - const storiesQuery = this.db.models.ArchivedRecord.find(baseQuery); 157 - 158 - const paginatedQuery = this.timeCidKeyset.paginate(storiesQuery, { 159 - limit: limit + 1, 160 - cursor, 161 - direction: "desc", 162 - }); 163 - 164 - const stories = await paginatedQuery.exec(); 165 - const hasMore = stories.length > limit; 166 - const resultStories = hasMore ? stories.slice(0, limit) : stories; 167 - 168 - const transformedStories: StoryItem[] = resultStories.map((story) => ({ 169 - uri: story.uri, 170 - cid: story.cid, 171 - authorDid: story.did, 172 - createdAt: story.createdAt, 173 - indexedAt: story.indexedAt, 174 - archived: true, 175 - sortAt: compositeTime(story.createdAt, story.indexedAt) || 176 - story.createdAt, 177 - })); 178 - 179 117 let nextCursor: string | undefined; 180 118 if (hasMore && resultStories.length > 0) { 181 119 nextCursor = this.timeCidKeyset.packFromResult(resultStories);
-20
hydration/story.ts
··· 29 29 ); 30 30 }, base); 31 31 } 32 - 33 - async getArchivedStories( 34 - uris: string[], 35 - includeTakedowns = false, 36 - given = new HydrationMap<Story>(), 37 - ): Promise<Stories> { 38 - const [have, need] = split(uris, (uri) => given.has(uri)); 39 - const base = have.reduce( 40 - (acc, uri) => acc.set(uri, given.get(uri) ?? null), 41 - new HydrationMap<Story>(), 42 - ); 43 - if (!need.length) return base; 44 - 45 - const res = await this.dataplane.records.getArchivedStoryRecords(need); 46 - 47 - return need.reduce((acc, uri, i) => { 48 - const record = parseRecord<StoryRecord>(res.records[i], includeTakedowns); 49 - return acc.set(uri, record ?? null); 50 - }, base); 51 - } 52 32 }
-13
lex/index.ts
··· 216 216 import type * as SoSprkActorGetProfiles from "./types/so/sprk/actor/getProfiles.ts"; 217 217 import type * as SoSprkActorGetPreferences from "./types/so/sprk/actor/getPreferences.ts"; 218 218 import type * as SoSprkStoryGetTimeline from "./types/so/sprk/story/getTimeline.ts"; 219 - import type * as SoSprkStoryGetArchive from "./types/so/sprk/story/getArchive.ts"; 220 219 import type * as SoSprkStoryGetStories from "./types/so/sprk/story/getStories.ts"; 221 220 import type * as SoSprkLabelerGetServices from "./types/so/sprk/labeler/getServices.ts"; 222 221 import type * as ComAtprotoTempDereferenceScope from "./types/com/atproto/temp/dereferenceScope.ts"; ··· 3314 3313 >, 3315 3314 ) { 3316 3315 const nsid = "so.sprk.story.getTimeline"; // @ts-ignore - dynamically generated 3317 - return this._server.xrpc.method(nsid, cfg); 3318 - } 3319 - 3320 - getArchive<A extends Auth = void>( 3321 - cfg: MethodConfigOrHandler< 3322 - A, 3323 - SoSprkStoryGetArchive.QueryParams, 3324 - SoSprkStoryGetArchive.HandlerInput, 3325 - SoSprkStoryGetArchive.HandlerOutput 3326 - >, 3327 - ) { 3328 - const nsid = "so.sprk.story.getArchive"; // @ts-ignore - dynamically generated 3329 3316 return this._server.xrpc.method(nsid, cfg); 3330 3317 } 3331 3318
-47
lex/lexicons.ts
··· 20753 20753 }, 20754 20754 }, 20755 20755 }, 20756 - "SoSprkStoryGetArchive": { 20757 - "lexicon": 1, 20758 - "id": "so.sprk.story.getArchive", 20759 - "defs": { 20760 - "main": { 20761 - "type": "query", 20762 - "description": 20763 - "Get archived stories for the requesting account. Requires auth.", 20764 - "parameters": { 20765 - "type": "params", 20766 - "properties": { 20767 - "limit": { 20768 - "type": "integer", 20769 - "minimum": 1, 20770 - "maximum": 100, 20771 - "default": 50, 20772 - }, 20773 - "cursor": { 20774 - "type": "string", 20775 - }, 20776 - }, 20777 - }, 20778 - "output": { 20779 - "encoding": "application/json", 20780 - "schema": { 20781 - "type": "object", 20782 - "required": [ 20783 - "stories", 20784 - ], 20785 - "properties": { 20786 - "cursor": { 20787 - "type": "string", 20788 - }, 20789 - "stories": { 20790 - "type": "array", 20791 - "items": { 20792 - "type": "ref", 20793 - "ref": "lex:so.sprk.story.defs#storyView", 20794 - }, 20795 - }, 20796 - }, 20797 - }, 20798 - }, 20799 - }, 20800 - }, 20801 - }, 20802 20756 "SoSprkStoryGetStories": { 20803 20757 "lexicon": 1, 20804 20758 "id": "so.sprk.story.getStories", ··· 26935 26889 SoSprkActorProfile: "so.sprk.actor.profile", 26936 26890 SoSprkStoryDefs: "so.sprk.story.defs", 26937 26891 SoSprkStoryGetTimeline: "so.sprk.story.getTimeline", 26938 - SoSprkStoryGetArchive: "so.sprk.story.getArchive", 26939 26892 SoSprkStoryGetStories: "so.sprk.story.getStories", 26940 26893 SoSprkStoryPost: "so.sprk.story.post", 26941 26894 SoSprkLabelerDefs: "so.sprk.labeler.defs",
-30
lex/types/so/sprk/story/getArchive.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import type * as SoSprkStoryDefs from "./defs.ts"; 5 - 6 - export type QueryParams = { 7 - limit: number; 8 - cursor?: string; 9 - }; 10 - export type InputSchema = undefined; 11 - 12 - export interface OutputSchema { 13 - cursor?: string; 14 - stories: (SoSprkStoryDefs.StoryView)[]; 15 - } 16 - 17 - export type HandlerInput = void; 18 - 19 - export interface HandlerSuccess { 20 - encoding: "application/json"; 21 - body: OutputSchema; 22 - headers?: { [key: string]: string }; 23 - } 24 - 25 - export interface HandlerError { 26 - status: number; 27 - message?: string; 28 - } 29 - 30 - export type HandlerOutput = HandlerError | HandlerSuccess;
-36
lexicons/so/sprk/story/getArchive.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "so.sprk.story.getArchive", 4 - "defs": { 5 - "main": { 6 - "type": "query", 7 - "description": "Get archived stories for the requesting account. Requires auth.", 8 - "parameters": { 9 - "type": "params", 10 - "properties": { 11 - "limit": { 12 - "type": "integer", 13 - "minimum": 1, 14 - "maximum": 100, 15 - "default": 50 16 - }, 17 - "cursor": { "type": "string" } 18 - } 19 - }, 20 - "output": { 21 - "encoding": "application/json", 22 - "schema": { 23 - "type": "object", 24 - "required": ["stories"], 25 - "properties": { 26 - "cursor": { "type": "string" }, 27 - "stories": { 28 - "type": "array", 29 - "items": { "type": "ref", "ref": "so.sprk.story.defs#storyView" } 30 - } 31 - } 32 - } 33 - } 34 - } 35 - } 36 - }
-219
tests/stories_test.ts
··· 4 4 const VALID_BLOB_CID = 5 5 "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; 6 6 7 - const storyRecordJson = (createdAt: string) => { 8 - return JSON.stringify({ 9 - $type: "so.sprk.story.post", 10 - createdAt, 11 - media: { 12 - $type: "so.sprk.media.image", 13 - image: { 14 - $type: "blob", 15 - ref: { $link: VALID_BLOB_CID }, 16 - mimeType: "image/jpeg", 17 - size: 250000, 18 - }, 19 - alt: "Archived story image", 20 - aspectRatio: { width: 1080, height: 1920 }, 21 - }, 22 - }); 23 - }; 24 - 25 7 Deno.test({ 26 8 name: "Stories", 27 9 sanitizeOps: false, ··· 116 98 } 117 99 }, 118 100 ); 119 - 120 - await t.step( 121 - "getArchive excludes takedown archived stories by default", 122 - async () => { 123 - const { ctx, cleanup } = await createTestContext({ 124 - actors: false, 125 - profiles: false, 126 - posts: false, 127 - replies: false, 128 - stories: false, 129 - likes: false, 130 - reposts: false, 131 - follows: false, 132 - blocks: false, 133 - audio: false, 134 - generators: false, 135 - preferences: false, 136 - records: false, 137 - actorSync: false, 138 - }); 139 - 140 - try { 141 - const now = new Date().toISOString(); 142 - const archivedUri = `at://${ 143 - TEST_USERS[0].did 144 - }/so.sprk.story.post/story1`; 145 - const takedownArchivedUri = `at://${ 146 - TEST_USERS[0].did 147 - }/so.sprk.story.post/story-takedown`; 148 - const otherAuthorArchivedUri = `at://${ 149 - TEST_USERS[2].did 150 - }/so.sprk.story.post/story2`; 151 - 152 - await ctx.db.models.ArchivedRecord.create({ 153 - uri: archivedUri, 154 - cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjqstory1", 155 - did: TEST_USERS[0].did, 156 - collectionName: "so.sprk.story.post", 157 - rkey: "story1", 158 - createdAt: now, 159 - indexedAt: now, 160 - json: storyRecordJson(now), 161 - archivedAt: now, 162 - deleteReason: "user_delete", 163 - }); 164 - await ctx.db.models.ArchivedRecord.create({ 165 - uri: takedownArchivedUri, 166 - cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjqstorytakedown", 167 - did: TEST_USERS[0].did, 168 - collectionName: "so.sprk.story.post", 169 - rkey: "story-takedown", 170 - createdAt: now, 171 - indexedAt: now, 172 - json: storyRecordJson(now), 173 - archivedAt: now, 174 - deleteReason: "takedown", 175 - takedownRef: "SPRK-TAKEDOWN-1", 176 - }); 177 - await ctx.db.models.ArchivedRecord.create({ 178 - uri: otherAuthorArchivedUri, 179 - cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjqstory2", 180 - did: TEST_USERS[2].did, 181 - collectionName: "so.sprk.story.post", 182 - rkey: "story2", 183 - createdAt: now, 184 - indexedAt: now, 185 - json: storyRecordJson(now), 186 - archivedAt: now, 187 - deleteReason: "user_delete", 188 - }); 189 - 190 - const res = await ctx.dataplane.stories.getArchive( 191 - TEST_USERS[0].did, 192 - 10, 193 - ); 194 - 195 - assertEquals(res.stories.length, 1); 196 - assertEquals(res.stories[0].uri, archivedUri); 197 - assertEquals(res.stories[0].archived, true); 198 - assertEquals(res.cursor, undefined); 199 - 200 - const resIncludingTakedowns = await ctx.dataplane.stories.getArchive( 201 - TEST_USERS[0].did, 202 - 10, 203 - undefined, 204 - true, 205 - ); 206 - assertEquals(resIncludingTakedowns.stories.length, 2); 207 - } finally { 208 - await cleanup(); 209 - } 210 - }, 211 - ); 212 - 213 - await t.step( 214 - "getArchivedStories hydrates from archived records", 215 - async () => { 216 - const { ctx, cleanup } = await createTestContext({ 217 - actors: false, 218 - profiles: false, 219 - posts: false, 220 - replies: false, 221 - stories: false, 222 - likes: false, 223 - reposts: false, 224 - follows: false, 225 - blocks: false, 226 - audio: false, 227 - generators: false, 228 - preferences: false, 229 - records: false, 230 - actorSync: false, 231 - }); 232 - 233 - try { 234 - const now = new Date().toISOString(); 235 - const archivedUri = `at://${ 236 - TEST_USERS[0].did 237 - }/so.sprk.story.post/story1`; 238 - const missingUri = `at://${ 239 - TEST_USERS[2].did 240 - }/so.sprk.story.post/story2`; 241 - 242 - await ctx.db.models.ArchivedRecord.create({ 243 - uri: archivedUri, 244 - cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjqstory3", 245 - did: TEST_USERS[0].did, 246 - collectionName: "so.sprk.story.post", 247 - rkey: "story1", 248 - createdAt: now, 249 - indexedAt: now, 250 - json: storyRecordJson(now), 251 - archivedAt: now, 252 - deleteReason: "user_delete", 253 - }); 254 - 255 - const hydrated = await ctx.hydrator.story.getArchivedStories([ 256 - archivedUri, 257 - missingUri, 258 - ]); 259 - 260 - assertEquals(Boolean(hydrated.get(archivedUri)), true); 261 - assertEquals(hydrated.get(missingUri), null); 262 - } finally { 263 - await cleanup(); 264 - } 265 - }, 266 - ); 267 - 268 - await t.step("getArchivedStories applies takedown filtering", async () => { 269 - const { ctx, cleanup } = await createTestContext({ 270 - actors: false, 271 - profiles: false, 272 - posts: false, 273 - replies: false, 274 - stories: false, 275 - likes: false, 276 - reposts: false, 277 - follows: false, 278 - blocks: false, 279 - audio: false, 280 - generators: false, 281 - preferences: false, 282 - records: false, 283 - actorSync: false, 284 - }); 285 - 286 - try { 287 - const now = new Date().toISOString(); 288 - const takedownUri = `at://${ 289 - TEST_USERS[0].did 290 - }/so.sprk.story.post/story-takedown`; 291 - 292 - await ctx.db.models.ArchivedRecord.create({ 293 - uri: takedownUri, 294 - cid: "bafyreihivhfhv6rh4x4a4znkqrvqwp5xw4xvqjqstory4", 295 - did: TEST_USERS[0].did, 296 - collectionName: "so.sprk.story.post", 297 - rkey: "story-takedown", 298 - createdAt: now, 299 - indexedAt: now, 300 - json: storyRecordJson(now), 301 - archivedAt: now, 302 - deleteReason: "user_delete", 303 - takedownRef: "SPRK-TAKEDOWN-1", 304 - }); 305 - 306 - const hidden = await ctx.hydrator.story.getArchivedStories([ 307 - takedownUri, 308 - ]); 309 - assertEquals(hidden.get(takedownUri), null); 310 - 311 - const included = await ctx.hydrator.story.getArchivedStories( 312 - [takedownUri], 313 - true, 314 - ); 315 - assertEquals(Boolean(included.get(takedownUri)), true); 316 - } finally { 317 - await cleanup(); 318 - } 319 - }); 320 101 }, 321 102 });
-4
tests/util.ts
··· 140 140 "Record", 141 141 models.recordSchema, 142 142 ), 143 - ArchivedRecord: connection.model<models.ArchivedRecordDocument>( 144 - "ArchivedRecord", 145 - models.archivedRecordSchema, 146 - ), 147 143 DuplicateRecord: connection.model<models.DuplicateRecordDocument>( 148 144 "DuplicateRecord", 149 145 models.duplicateRecordSchema,