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

at main 349 lines 12 kB view raw
1import { assertEquals } from "@std/assert"; 2import { createTestApp, TEST_USERS } from "./util.ts"; 3import { $OutputBody } from "../lex/so/sprk/feed/getCrosspostThread.ts"; 4 5Deno.test({ 6 name: "Crosspost thread endpoint", 7 sanitizeOps: false, 8 sanitizeResources: false, 9 fn: async (t) => { 10 const { app, ctx, cleanup } = await createTestApp({ 11 actors: true, 12 profiles: false, 13 posts: false, 14 replies: false, 15 stories: false, 16 likes: false, 17 reposts: false, 18 follows: false, 19 blocks: false, 20 audio: false, 21 generators: false, 22 preferences: false, 23 records: false, 24 actorSync: false, 25 }); 26 27 try { 28 const parentUri = `at://${TEST_USERS[0].did}/so.sprk.feed.post/post1`; 29 const validCid = 30 "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; 31 const parentCid = validCid; 32 const reply1Uri = `at://${TEST_USERS[1].did}/app.bsky.feed.post/cross1`; 33 const reply2Uri = `at://${TEST_USERS[2].did}/app.bsky.feed.post/cross2`; 34 const reply3Uri = `at://${TEST_USERS[2].did}/app.bsky.feed.post/cross3`; 35 const reply4Uri = `at://${TEST_USERS[3].did}/app.bsky.feed.post/cross4`; 36 const cycleAUri = `at://${TEST_USERS[1].did}/app.bsky.feed.post/cycleA`; 37 const cycleBUri = `at://${TEST_USERS[2].did}/app.bsky.feed.post/cycleB`; 38 const reply1Cid = validCid; 39 const reply2Cid = validCid; 40 const reply3Cid = validCid; 41 const reply4Cid = validCid; 42 const cycleACid = validCid; 43 const cycleBCid = validCid; 44 45 const time0 = new Date("2026-01-01T00:00:00.000Z").toISOString(); 46 const time1 = new Date("2026-01-01T00:01:00.000Z").toISOString(); 47 const time2 = new Date("2026-01-01T00:02:00.000Z").toISOString(); 48 const time3 = new Date("2026-01-01T00:03:00.000Z").toISOString(); 49 const time4 = new Date("2026-01-01T00:04:00.000Z").toISOString(); 50 const time5 = new Date("2026-01-01T00:05:00.000Z").toISOString(); 51 const time6 = new Date("2026-01-01T00:06:00.000Z").toISOString(); 52 53 await ctx.db.models.Post.create({ 54 uri: parentUri, 55 cid: parentCid, 56 authorDid: TEST_USERS[0].did, 57 caption: { text: "root" }, 58 media: { 59 $type: "so.sprk.media.images", 60 images: [], 61 }, 62 createdAt: time0, 63 indexedAt: time0, 64 likeCount: 1, 65 replyCount: 2, 66 repostCount: 0, 67 }); 68 69 await ctx.db.models.CrosspostReply.create([ 70 { 71 uri: reply1Uri, 72 cid: reply1Cid, 73 authorDid: TEST_USERS[1].did, 74 text: "reply-1", 75 reply: { 76 root: { uri: parentUri, cid: parentCid }, 77 parent: { uri: parentUri, cid: parentCid }, 78 }, 79 createdAt: time1, 80 indexedAt: time1, 81 likeCount: 2, 82 replyCount: 1, 83 }, 84 { 85 uri: reply2Uri, 86 cid: reply2Cid, 87 authorDid: TEST_USERS[2].did, 88 text: "reply-2", 89 reply: { 90 root: { uri: parentUri, cid: parentCid }, 91 parent: { uri: reply1Uri, cid: reply1Cid }, 92 }, 93 createdAt: time2, 94 indexedAt: time2, 95 likeCount: 3, 96 replyCount: 0, 97 }, 98 { 99 uri: reply3Uri, 100 cid: reply3Cid, 101 authorDid: TEST_USERS[2].did, 102 text: "reply-3", 103 reply: { 104 root: { uri: parentUri, cid: parentCid }, 105 parent: { uri: parentUri, cid: parentCid }, 106 }, 107 createdAt: time3, 108 indexedAt: time3, 109 likeCount: 1, 110 replyCount: 0, 111 }, 112 { 113 uri: reply4Uri, 114 cid: reply4Cid, 115 authorDid: TEST_USERS[3].did, 116 text: "reply-4", 117 reply: { 118 root: { uri: parentUri, cid: parentCid }, 119 parent: { uri: parentUri, cid: parentCid }, 120 }, 121 createdAt: time4, 122 indexedAt: time4, 123 likeCount: 10, 124 replyCount: 0, 125 }, 126 { 127 uri: cycleAUri, 128 cid: cycleACid, 129 authorDid: TEST_USERS[1].did, 130 text: "cycle-a", 131 reply: { 132 root: { uri: parentUri, cid: parentCid }, 133 parent: { uri: cycleBUri, cid: cycleBCid }, 134 }, 135 createdAt: time5, 136 indexedAt: time5, 137 likeCount: 0, 138 replyCount: 0, 139 }, 140 { 141 uri: cycleBUri, 142 cid: cycleBCid, 143 authorDid: TEST_USERS[2].did, 144 text: "cycle-b", 145 reply: { 146 root: { uri: parentUri, cid: parentCid }, 147 parent: { uri: cycleAUri, cid: cycleACid }, 148 }, 149 createdAt: time6, 150 indexedAt: time6, 151 likeCount: 0, 152 replyCount: 0, 153 }, 154 ]); 155 156 const blockUri = `at://${TEST_USERS[1].did}/so.sprk.graph.block/block1`; 157 await ctx.db.models.Block.create({ 158 uri: blockUri, 159 cid: validCid, 160 authorDid: TEST_USERS[1].did, 161 subject: TEST_USERS[2].did, 162 createdAt: time6, 163 indexedAt: time6, 164 }); 165 166 await t.step( 167 "returns thread-style descendants from a post anchor", 168 async () => { 169 const res = await app.request( 170 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 171 encodeURIComponent(parentUri) 172 }&depth=5&parentHeight=5&sort=oldest&limit=50`, 173 ); 174 assertEquals(res.status, 200); 175 176 const body = await res.json() as $OutputBody; 177 assertEquals(body.thread.length, 5); 178 assertEquals(body.thread[0].uri, parentUri); 179 assertEquals(body.thread[0].depth, 0); 180 assertEquals(body.thread[1].uri, reply1Uri); 181 assertEquals(body.thread[1].depth, 1); 182 assertEquals(body.thread[2].uri, reply3Uri); 183 assertEquals(body.thread[2].depth, 1); 184 assertEquals(body.thread[3].uri, reply4Uri); 185 assertEquals(body.thread[3].depth, 1); 186 assertEquals(body.thread[4].uri, reply2Uri); 187 assertEquals(body.thread[4].depth, 2); 188 }, 189 ); 190 191 await t.step("applies limit and cursor pagination", async () => { 192 const firstRes = await app.request( 193 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 194 encodeURIComponent(parentUri) 195 }&depth=5&parentHeight=5&sort=oldest&limit=2`, 196 ); 197 assertEquals(firstRes.status, 200); 198 const firstBody = await firstRes.json() as $OutputBody; 199 assertEquals(firstBody.thread.length, 2); 200 assertEquals(firstBody.thread[0].uri, parentUri); 201 assertEquals(firstBody.thread[1].uri, reply1Uri); 202 assertEquals(firstBody.cursor, "2"); 203 204 const secondRes = await app.request( 205 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 206 encodeURIComponent(parentUri) 207 }&depth=5&parentHeight=5&sort=oldest&limit=2&cursor=${firstBody.cursor}`, 208 ); 209 assertEquals(secondRes.status, 200); 210 const secondBody = await secondRes.json() as $OutputBody; 211 assertEquals(secondBody.thread.length, 2); 212 assertEquals(secondBody.thread[0].uri, reply3Uri); 213 assertEquals(secondBody.thread[1].uri, reply4Uri); 214 assertEquals(secondBody.cursor, "4"); 215 216 const thirdRes = await app.request( 217 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 218 encodeURIComponent(parentUri) 219 }&depth=5&parentHeight=5&sort=oldest&limit=2&cursor=${secondBody.cursor}`, 220 ); 221 assertEquals(thirdRes.status, 200); 222 const thirdBody = await thirdRes.json() as $OutputBody; 223 assertEquals(thirdBody.thread.length, 1); 224 assertEquals(thirdBody.thread[0].uri, reply2Uri); 225 assertEquals(thirdBody.cursor, undefined); 226 }); 227 228 await t.step("respects newest sibling ordering", async () => { 229 const res = await app.request( 230 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 231 encodeURIComponent(parentUri) 232 }&depth=1&parentHeight=5&sort=newest&limit=50`, 233 ); 234 assertEquals(res.status, 200); 235 236 const body = await res.json() as $OutputBody; 237 assertEquals(body.thread.length, 4); 238 assertEquals(body.thread[0].uri, parentUri); 239 assertEquals(body.thread[1].uri, reply4Uri); 240 assertEquals(body.thread[2].uri, reply3Uri); 241 assertEquals(body.thread[3].uri, reply1Uri); 242 }); 243 244 await t.step("respects top sibling ordering", async () => { 245 const res = await app.request( 246 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 247 encodeURIComponent(parentUri) 248 }&depth=1&parentHeight=5&sort=top&limit=50`, 249 ); 250 assertEquals(res.status, 200); 251 252 const body = await res.json() as $OutputBody; 253 assertEquals(body.thread.length, 4); 254 assertEquals(body.thread[0].uri, parentUri); 255 assertEquals(body.thread[1].uri, reply4Uri); 256 assertEquals(body.thread[2].uri, reply1Uri); 257 assertEquals(body.thread[3].uri, reply3Uri); 258 }); 259 260 await t.step("includes ancestors for reply anchor", async () => { 261 const res = await app.request( 262 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 263 encodeURIComponent(reply2Uri) 264 }&depth=0&parentHeight=5&sort=oldest&limit=50`, 265 ); 266 assertEquals(res.status, 200); 267 268 const body = await res.json() as $OutputBody; 269 assertEquals(body.thread.length, 3); 270 assertEquals(body.thread[0].uri, parentUri); 271 assertEquals(body.thread[0].depth, -2); 272 assertEquals(body.thread[1].uri, reply1Uri); 273 assertEquals(body.thread[1].depth, -1); 274 assertEquals(body.thread[2].uri, reply2Uri); 275 assertEquals(body.thread[2].depth, 0); 276 }); 277 278 await t.step("applies parent/root 3p-block moderation", async () => { 279 const res = await app.request( 280 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 281 encodeURIComponent(parentUri) 282 }&depth=5&parentHeight=5&sort=oldest&limit=50`, 283 ); 284 assertEquals(res.status, 200); 285 286 const body = await res.json() as $OutputBody; 287 const blocked = body.thread.find((item) => item.uri === reply2Uri); 288 assertEquals( 289 blocked?.value.$type, 290 "so.sprk.feed.defs#blockedPost", 291 ); 292 }); 293 294 await t.step( 295 "hides taken-down thread records for standard viewers", 296 async () => { 297 await ctx.db.models.Record.create({ 298 uri: reply4Uri, 299 cid: reply4Cid, 300 did: TEST_USERS[3].did, 301 collectionName: "app.bsky.feed.post", 302 rkey: "cross4", 303 createdAt: time4, 304 indexedAt: time4, 305 json: JSON.stringify({ 306 $type: "app.bsky.feed.post", 307 text: "reply-4", 308 createdAt: time4, 309 }), 310 takedownRef: "TAKEDOWN", 311 }); 312 313 const res = await app.request( 314 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 315 encodeURIComponent(parentUri) 316 }&depth=5&parentHeight=5&sort=oldest&limit=50`, 317 ); 318 assertEquals(res.status, 200); 319 const body = await res.json() as $OutputBody; 320 assertEquals( 321 body.thread.some((item) => item.uri === reply4Uri), 322 false, 323 ); 324 }, 325 ); 326 327 await t.step( 328 "stops on cyclic ancestors and keeps anchor at depth 0", 329 async () => { 330 const res = await app.request( 331 `/xrpc/so.sprk.feed.getCrosspostThread?anchor=${ 332 encodeURIComponent(cycleAUri) 333 }&depth=0&parentHeight=10&sort=oldest&limit=50`, 334 ); 335 assertEquals(res.status, 200); 336 337 const body = await res.json() as $OutputBody; 338 assertEquals(body.thread.length, 2); 339 assertEquals(body.thread[0].uri, cycleBUri); 340 assertEquals(body.thread[0].depth, -1); 341 assertEquals(body.thread[1].uri, cycleAUri); 342 assertEquals(body.thread[1].depth, 0); 343 }, 344 ); 345 } finally { 346 await cleanup(); 347 } 348 }, 349});