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

even MORE admin endpoints (yeah)

+213 -57
+9 -1
services/appview/src/db.ts
··· 353 353 reason: string 354 354 takenDownBy: string 355 355 takenDownAt: string 356 + ref: string | null 357 + applied: boolean 356 358 } 357 359 358 360 export const takedownSchema = new Schema<TakedownDocument>({ ··· 361 363 reason: { type: String, required: true }, 362 364 takenDownBy: { type: String, required: true }, 363 365 takenDownAt: { type: String, required: true }, 366 + ref: { type: String, required: false }, 367 + applied: { type: Boolean, required: true, default: false }, 364 368 }) 365 369 366 370 // Repository takedown schema ··· 370 374 takenDownBy: string 371 375 takenDownAt: string 372 376 ref: string | null 377 + applied: boolean 373 378 } 374 379 375 380 export const repoTakedownSchema = new Schema<RepoTakedownDocument>({ ··· 378 383 takenDownBy: { type: String, required: true }, 379 384 takenDownAt: { type: String, required: true }, 380 385 ref: { type: String, required: false, default: null }, 386 + applied: { type: Boolean, required: true, default: false }, 381 387 }) 382 388 383 389 // Blob takedown schema ··· 388 394 takenDownBy: string 389 395 takenDownAt: string 390 396 ref: string | null 397 + applied: boolean 391 398 } 392 399 393 400 export const blobTakedownSchema = new Schema<BlobTakedownDocument>({ ··· 397 404 takenDownBy: { type: String, required: true }, 398 405 takenDownAt: { type: String, required: true }, 399 406 ref: { type: String, required: false, default: null }, 407 + applied: { type: Boolean, required: true, default: false }, 400 408 }) 401 409 402 410 // Ensure compound index on did + cid for blob takedowns ··· 406 414 did: string 407 415 handle: string | null 408 416 indexedAt: string 409 - takedownRef: string | null 417 + takedownRef: string | null 410 418 upstreamStatus: string | null 411 419 } 412 420
+6 -1
services/appview/src/index.ts
··· 24 24 import { createTakedownRouter } from './routes/admin/takedowns.js' 25 25 import { createUpdateSubjectStatusRouter } from './routes/com/atproto/admin/updateSubjectStatus.js' 26 26 import { createGetRecordRouter } from './routes/com/atproto/repo/getRecord.js' 27 + import { createGetAccountInfosRouter } from './routes/com/atproto/admin/getAccountInfos.js' 28 + import { createGetSubjectStatusRouter } from './routes/com/atproto/admin/getSubjectStatus.js' 27 29 import wellKnownRouter from './well-known.js' 28 30 import { TakedownService } from './services/takedown.js' 29 31 import { IndexingService } from './services/indexing.js' ··· 110 112 const updateSubjectStatusRouter = createUpdateSubjectStatusRouter(ctx) 111 113 const takedownRouter = createTakedownRouter(ctx) 112 114 const getRecordRouter = createGetRecordRouter(ctx) 115 + const getAccountInfosRouter = createGetAccountInfosRouter(ctx) 116 + const getSubjectStatusRouter = createGetSubjectStatusRouter(ctx) 113 117 114 118 app.route('/', getPostsRouter) 115 119 app.route('/', getPostThreadRouter) ··· 121 125 app.route('/', updateSubjectStatusRouter) 122 126 app.route('/', takedownRouter) 123 127 app.route('/', getRecordRouter) 124 - 128 + app.route('/', getAccountInfosRouter) 129 + app.route('/', getSubjectStatusRouter) 125 130 app.route('/', wellKnownRouter()) 126 131 127 132 // Root route
+28 -39
services/appview/src/middleware/takedown-filter.ts
··· 36 36 body.profile?.did || 37 37 body.subject?.did 38 38 if (targetDid) { 39 - const isRepoTakenDown = await takedownService.isRepoTakenDown(targetDid) 40 - if (isRepoTakenDown) { 39 + const repoTakedown = await takedownService.getRepoTakedown(targetDid) 40 + if (repoTakedown?.applied) { 41 41 // For specific user/profile views, return minimal placeholder 42 42 if (body.did && body.$type && body.$type.includes('profileView')) { 43 43 const takenDownProfile = { ··· 86 86 ) 87 87 body.feed = filteredFeed 88 88 } else if (body.thread && body.thread.post) { 89 - const isThreadTakenDown = await takedownService.isTakenDown( 90 - body.thread.post.uri, 91 - ) 89 + const takedown = await takedownService.getTakedown(body.thread.post.uri) 90 + const isThreadTakenDown = takedown?.applied ?? false 92 91 93 92 // Also check if the thread author repo is taken down 94 93 let isAuthorTakenDown = false 95 94 if (body.thread.post.author?.did) { 96 - isAuthorTakenDown = await takedownService.isRepoTakenDown( 97 - body.thread.post.author.did, 98 - ) 95 + const repoTakedown = await takedownService.getRepoTakedown(body.thread.post.author.did) 96 + isAuthorTakenDown = repoTakedown?.applied ?? false 99 97 } 100 98 101 99 if (isThreadTakenDown || isAuthorTakenDown) { ··· 117 115 body.profiles = filteredProfiles 118 116 } else if (body.profile) { 119 117 if (body.profile.did) { 120 - const isRepoTakenDown = await takedownService.isRepoTakenDown( 121 - body.profile.did, 122 - ) 123 - if (isRepoTakenDown) { 118 + const repoTakedown = await takedownService.getRepoTakedown(body.profile.did) 119 + if (repoTakedown?.applied) { 124 120 body.profile = null 125 121 } 126 122 } 127 123 } else if (body.did && body.$type && body.$type.includes('profileView')) { 128 124 // For direct ProfileViewDetailed objects (so.sprk.actor.getProfile) 129 - const isRepoTakenDown = await takedownService.isRepoTakenDown(body.did) 130 - if (isRepoTakenDown) { 125 + const repoTakedown = await takedownService.getRepoTakedown(body.did) 126 + if (repoTakedown?.applied) { 131 127 // Return a minimal placeholder object for taken-down profiles 132 128 const takenDownProfile = { 133 129 $type: body.$type, ··· 150 146 } else if (body.subject) { 151 147 // For followers/follows response that has a subject profile 152 148 if (body.subject.did) { 153 - const isRepoTakenDown = await takedownService.isRepoTakenDown( 154 - body.subject.did, 155 - ) 156 - if (isRepoTakenDown) { 149 + const repoTakedown = await takedownService.getRepoTakedown(body.subject.did) 150 + if (repoTakedown?.applied) { 157 151 // Keep minimal info about the profile but mark it as taken down 158 152 body.subject = { 159 153 $type: body.subject.$type, ··· 208 202 // Get URI for this specific content 209 203 const uri = get(item, uriPath) as string | undefined 210 204 if (uri) { 211 - isTakenDown = await takedownService.isTakenDown(uri) 205 + const takedown = await takedownService.getTakedown(uri) 206 + isTakenDown = takedown?.applied ?? false 212 207 } 213 208 214 209 // Check if author's repo is taken down ··· 221 216 get(item, 'actor.did') 222 217 223 218 if (authorDid) { 224 - isAuthorTakenDown = await takedownService.isRepoTakenDown(authorDid) 219 + const repoTakedown = await takedownService.getRepoTakedown(authorDid) 220 + isAuthorTakenDown = repoTakedown?.applied ?? false 225 221 } 226 222 227 223 // Keep the item only if neither the content nor the author is taken down 228 224 if (!isTakenDown && !isAuthorTakenDown) { 229 225 // Also check for any embedded items like quotes or replies 230 226 if (item.embed && item.embed.record && item.embed.record.author?.did) { 231 - const embedAuthorTakenDown = await takedownService.isRepoTakenDown( 232 - item.embed.record.author.did, 233 - ) 234 - if (embedAuthorTakenDown) { 227 + const embedAuthorTakedown = await takedownService.getRepoTakedown(item.embed.record.author.did) 228 + if (embedAuthorTakedown?.applied) { 235 229 // Null out the embed if from a taken-down repo 236 230 item.embed = { 237 231 $type: item.embed.$type, ··· 239 233 } 240 234 } else if (item.embed.record.uri) { 241 235 // Check if the specific embedded content is taken down 242 - const embedContentTakenDown = await takedownService.isTakenDown( 243 - item.embed.record.uri, 244 - ) 245 - if (embedContentTakenDown) { 236 + const embedContentTakedown = await takedownService.getTakedown(item.embed.record.uri) 237 + if (embedContentTakedown?.applied) { 246 238 item.embed = { 247 239 $type: item.embed.$type, 248 240 takenDown: true, ··· 268 260 269 261 for (const profile of profiles) { 270 262 if (profile.did) { 271 - const isRepoTakenDown = await takedownService.isRepoTakenDown(profile.did) 272 - if (!isRepoTakenDown) { 263 + const repoTakedown = await takedownService.getRepoTakedown(profile.did) 264 + if (!repoTakedown?.applied) { 273 265 filteredProfiles.push(profile) 274 266 } else { 275 267 // For UI consistency, push a minimal placeholder for taken-down profiles ··· 304 296 for (const image of images) { 305 297 // Check if the image is taken down based on blob CID 306 298 if (image.cid && image.did) { 307 - const isBlobTakenDown = await takedownService.isBlobTakenDown( 308 - image.did, 309 - image.cid, 310 - ) 311 - if (!isBlobTakenDown) { 299 + const blobTakedown = await takedownService.getBlobTakedown(image.did, image.cid) 300 + if (!blobTakedown?.applied) { 312 301 filteredImages.push(image) 313 302 } 314 303 } else { ··· 331 320 332 321 for (const reply of replies) { 333 322 if (reply.post && reply.post.uri) { 334 - const isTakenDown = await takedownService.isTakenDown(reply.post.uri) 323 + const takedown = await takedownService.getTakedown(reply.post.uri) 324 + const isTakenDown = takedown?.applied ?? false 335 325 336 326 // Check if author's repo is taken down 337 327 let isAuthorTakenDown = false 338 328 if (reply.post.author?.did) { 339 - isAuthorTakenDown = await takedownService.isRepoTakenDown( 340 - reply.post.author.did, 341 - ) 329 + const repoTakedown = await takedownService.getRepoTakedown(reply.post.author.did) 330 + isAuthorTakenDown = repoTakedown?.applied ?? false 342 331 } 343 332 344 333 if (!isTakenDown && !isAuthorTakenDown) {
+31 -16
services/appview/src/routes/com/atproto/admin/getAccountInfos.ts
··· 1 1 import { Hono } from 'hono' 2 - import { zValidator } from '@hono/zod-validator' 3 - import { z } from 'zod' 4 - import { HTTPException } from 'hono/http-exception' 5 - import { TakedownService } from '../../../../services/takedown.js' 6 2 import { authMiddleware } from '../../../../auth/middleware.js' 7 3 import { AppContext } from '../../../../index.js' 8 - import type * as ComAtprotoAdminUpdateSubjectStatus from '../../../../lexicon/types/com/atproto/admin/updateSubjectStatus.js' 9 - import type * as ComAtprotoAdminDefs from '../../../../lexicon/types/com/atproto/admin/defs.js' 10 - import type * as ComAtprotoRepoStrongRef from '../../../../lexicon/types/com/atproto/repo/strongRef.js' 4 + import { mapDefined } from '@atproto/common' 5 + import { INVALID_HANDLE } from '@atproto/syntax' 11 6 12 - export const createGetAccountInfosRouter = (_ctx: AppContext) => { 7 + export const createGetAccountInfosRouter = (ctx: AppContext) => { 13 8 const router = new Hono() 14 9 15 10 // XRPC endpoint for Ozone integration: com.atproto.admin.getAccountInfos 16 11 router.get( 17 12 '/xrpc/com.atproto.admin.getAccountInfos', 18 13 (c, next) => authMiddleware(c, next, true), 19 - zValidator( 20 - 'json', 21 - z.object({ 22 - dids: z.array(z.string()), 23 - }), 24 - ), 25 14 async (c) => { 26 - const { dids } = c.req.valid('json') 27 - } 15 + const dids = c.req.queries('dids[]') 16 + if (!dids || dids.length === 0) { 17 + return c.json({ error: 'Missing or empty dids parameter' }, 400) 18 + } 19 + 20 + const timestamp = new Date().toISOString() 21 + 22 + const infos = await Promise.all( 23 + mapDefined(dids, async (did) => { 24 + await ctx.indexingService.indexHandle(did, timestamp) 25 + const actor = await ctx.db.models.Actor.findOne({ did }) 26 + if (!actor) return 27 + 28 + const profile = await ctx.db.models.Profile.findOne({ 29 + did: actor.did, 30 + }) 31 + 32 + return { 33 + did: actor.did, 34 + handle: actor.handle ?? INVALID_HANDLE, 35 + relatedRecords: [profile], 36 + indexedAt: actor.indexedAt, 37 + } 38 + }), 39 + ) 40 + 41 + return c.json(infos) 42 + }, 28 43 ) 29 44 30 45 return router
+86
services/appview/src/routes/com/atproto/admin/getSubjectStatus.ts
··· 1 + import { Hono } from 'hono' 2 + import { authMiddleware, optionalAuthMiddleware } from '../../../../auth/middleware.js' 3 + import { AppContext } from '../../../../index.js' 4 + 5 + export const createGetSubjectStatusRouter = (ctx: AppContext) => { 6 + const router = new Hono() 7 + 8 + router.get('/xrpc/com.atproto.admin.getSubjectStatus', (c, next) => optionalAuthMiddleware(c, next), async (c) => { 9 + const did = c.req.query('did') 10 + const uri = c.req.query('uri') 11 + const blob = c.req.query('blob') 12 + 13 + if (!did && !uri && !blob) { 14 + return c.json({ error: 'Missing required parameter' }, 400) 15 + } 16 + 17 + let subject 18 + let takedown 19 + if (did) { 20 + const actor = await ctx.db.models.Actor.findOne({ did }) 21 + const repoTakedown = await ctx.db.models.RepoTakedown.findOne({ 22 + subjectDid: did 23 + }) 24 + if (!actor) { 25 + return c.json({ error: 'Actor not found' }, 404) 26 + } 27 + subject = { 28 + did: actor.did, 29 + } 30 + if (repoTakedown) { 31 + takedown = { 32 + applied: repoTakedown.applied, 33 + ref: repoTakedown.ref, 34 + } 35 + } 36 + } else if (uri) { 37 + const record = 38 + (await ctx.db.models.Profile.findOne({ uri })) ?? 39 + (await ctx.db.models.Post.findOne({ uri })) ?? 40 + (await ctx.db.models.Audio.findOne({ uri })) 41 + const recordTakedown = await ctx.db.models.Takedown.findOne({ 42 + subjectUri: uri, 43 + }) 44 + if (!record) { 45 + return c.json({ error: 'Record not found' }, 404) 46 + } 47 + subject = { 48 + uri: record.uri, 49 + cid: record.cid, 50 + } 51 + if (recordTakedown) { 52 + takedown = { 53 + applied: recordTakedown.applied, 54 + ref: recordTakedown.ref, 55 + } 56 + } 57 + } else if (blob) { 58 + const blobRecord = 59 + (await ctx.db.models.Profile.findOne({ blob })) ?? 60 + (await ctx.db.models.Post.findOne({ blob })) ?? 61 + (await ctx.db.models.Audio.findOne({ blob })) 62 + if (!blobRecord) { 63 + return c.json({ error: 'Blob record not found' }, 404) 64 + } 65 + subject = { 66 + did: blobRecord.authorDid, 67 + cid: blobRecord.cid, 68 + recordUri: blobRecord.uri, 69 + } 70 + const blobTakedown = await ctx.db.models.BlobTakedown.findOne({ 71 + subjectDid: blobRecord.authorDid, 72 + subjectCid: blobRecord.cid, 73 + }) 74 + if (blobTakedown) { 75 + takedown = { 76 + applied: blobTakedown.applied, 77 + ref: blobTakedown.ref, 78 + } 79 + } 80 + } 81 + 82 + return c.json({ subject, takedown }) 83 + }) 84 + 85 + return router 86 + }
+3
services/appview/src/routes/com/atproto/admin/updateSubjectStatus.ts
··· 59 59 adminDid, 60 60 ref: takedown.ref, 61 61 }) 62 + await takedownService.updateRepoTakedownApplied(subject.did, takedown.applied) 62 63 } else { 63 64 // Remove takedown 64 65 await takedownService.removeRepoTakedown(subject.did) ··· 79 80 reason: 'Moderation via Ozone', 80 81 adminDid, 81 82 }) 83 + await takedownService.updateTakedownApplied(subject.uri, takedown.applied) 82 84 } else { 83 85 // Remove takedown 84 86 await takedownService.removeTakedown(subject.uri) ··· 100 102 adminDid, 101 103 ref: takedown.ref, 102 104 }) 105 + await takedownService.updateBlobTakedownApplied(subject.did, subject.cid, takedown.applied) 103 106 } else { 104 107 // Remove takedown 105 108 await takedownService.removeBlobTakedown(subject.did, subject.cid)
+50
services/appview/src/services/takedown.ts
··· 18 18 reason, 19 19 takenDownBy: adminDid, 20 20 takenDownAt: new Date().toISOString(), 21 + applied: true, 21 22 }) 22 23 } 23 24 ··· 37 38 takenDownBy: adminDid, 38 39 takenDownAt: new Date().toISOString(), 39 40 ref: ref || null, 41 + applied: false, 40 42 }) 41 43 } 42 44 ··· 58 60 takenDownBy: adminDid, 59 61 takenDownAt: new Date().toISOString(), 60 62 ref: ref || null, 63 + applied: false, 61 64 }) 62 65 } 63 66 ··· 77 80 reason: string 78 81 takenDownBy: string 79 82 takenDownAt: string 83 + applied: boolean 80 84 } | null> { 81 85 const takedown = await this.db.models.Takedown.findOne({ targetUri: uri }).lean() 82 86 return takedown ··· 215 219 })), 216 220 cursor: takedowns.length > limit ? takedowns[limit - 1].did : undefined 217 221 } 222 + } 223 + 224 + async updateTakedownApplied(targetUri: string, applied: boolean): Promise<void> { 225 + await this.db.models.Takedown.updateOne( 226 + { targetUri }, 227 + { $set: { applied } } 228 + ) 229 + } 230 + 231 + async updateRepoTakedownApplied(did: string, applied: boolean): Promise<void> { 232 + await this.db.models.RepoTakedown.updateOne( 233 + { did }, 234 + { $set: { applied } } 235 + ) 236 + } 237 + 238 + async updateBlobTakedownApplied(did: string, cid: string, applied: boolean): Promise<void> { 239 + await this.db.models.BlobTakedown.updateOne( 240 + { did, cid }, 241 + { $set: { applied } } 242 + ) 243 + } 244 + 245 + async getRepoTakedown(did: string): Promise<{ 246 + did: string 247 + reason: string 248 + takenDownBy: string 249 + takenDownAt: string 250 + ref: string | null 251 + applied: boolean 252 + } | null> { 253 + const takedown = await this.db.models.RepoTakedown.findOne({ did }).lean() 254 + return takedown 255 + } 256 + 257 + async getBlobTakedown(did: string, cid: string): Promise<{ 258 + did: string 259 + cid: string 260 + reason: string 261 + takenDownBy: string 262 + takenDownAt: string 263 + ref: string | null 264 + applied: boolean 265 + } | null> { 266 + const takedown = await this.db.models.BlobTakedown.findOne({ did, cid }).lean() 267 + return takedown 218 268 } 219 269 }