From 62a7c2b2da9508c94bc5bac80b7e61ec34e4cec3 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Tue, 29 Apr 2025 21:43:57 -0400 Subject: [PATCH] even MORE admin endpoints (yeah) --- services/appview/src/db.ts | 10 ++- services/appview/src/index.ts | 7 +- .../appview/src/middleware/takedown-filter.ts | 67 ++++++--------- .../com/atproto/admin/getAccountInfos.ts | 47 ++++++---- .../com/atproto/admin/getSubjectStatus.ts | 86 +++++++++++++++++++ .../com/atproto/admin/updateSubjectStatus.ts | 3 + services/appview/src/services/takedown.ts | 50 +++++++++++ 7 files changed, 213 insertions(+), 57 deletions(-) create mode 100644 services/appview/src/routes/com/atproto/admin/getSubjectStatus.ts diff --git a/services/appview/src/db.ts b/services/appview/src/db.ts index 2907159..45b181b 100644 --- a/services/appview/src/db.ts +++ b/services/appview/src/db.ts @@ -353,6 +353,8 @@ export interface TakedownDocument extends Document { reason: string takenDownBy: string takenDownAt: string + ref: string | null + applied: boolean } export const takedownSchema = new Schema({ @@ -361,6 +363,8 @@ export const takedownSchema = new Schema({ reason: { type: String, required: true }, takenDownBy: { type: String, required: true }, takenDownAt: { type: String, required: true }, + ref: { type: String, required: false }, + applied: { type: Boolean, required: true, default: false }, }) // Repository takedown schema @@ -370,6 +374,7 @@ export interface RepoTakedownDocument extends Document { takenDownBy: string takenDownAt: string ref: string | null + applied: boolean } export const repoTakedownSchema = new Schema({ @@ -378,6 +383,7 @@ export const repoTakedownSchema = new Schema({ takenDownBy: { type: String, required: true }, takenDownAt: { type: String, required: true }, ref: { type: String, required: false, default: null }, + applied: { type: Boolean, required: true, default: false }, }) // Blob takedown schema @@ -388,6 +394,7 @@ export interface BlobTakedownDocument extends Document { takenDownBy: string takenDownAt: string ref: string | null + applied: boolean } export const blobTakedownSchema = new Schema({ @@ -397,6 +404,7 @@ export const blobTakedownSchema = new Schema({ takenDownBy: { type: String, required: true }, takenDownAt: { type: String, required: true }, ref: { type: String, required: false, default: null }, + applied: { type: Boolean, required: true, default: false }, }) // Ensure compound index on did + cid for blob takedowns @@ -406,7 +414,7 @@ export interface ActorDocument extends Document { did: string handle: string | null indexedAt: string - takedownRef: string | null + takedownRef: string | null upstreamStatus: string | null } diff --git a/services/appview/src/index.ts b/services/appview/src/index.ts index 96efc82..44c3f33 100644 --- a/services/appview/src/index.ts +++ b/services/appview/src/index.ts @@ -24,6 +24,8 @@ import { createGetFollowsRouter } from './routes/so/sprk/graph/getFollows.js' import { createTakedownRouter } from './routes/admin/takedowns.js' import { createUpdateSubjectStatusRouter } from './routes/com/atproto/admin/updateSubjectStatus.js' import { createGetRecordRouter } from './routes/com/atproto/repo/getRecord.js' +import { createGetAccountInfosRouter } from './routes/com/atproto/admin/getAccountInfos.js' +import { createGetSubjectStatusRouter } from './routes/com/atproto/admin/getSubjectStatus.js' import wellKnownRouter from './well-known.js' import { TakedownService } from './services/takedown.js' import { IndexingService } from './services/indexing.js' @@ -110,6 +112,8 @@ export class Server { const updateSubjectStatusRouter = createUpdateSubjectStatusRouter(ctx) const takedownRouter = createTakedownRouter(ctx) const getRecordRouter = createGetRecordRouter(ctx) + const getAccountInfosRouter = createGetAccountInfosRouter(ctx) + const getSubjectStatusRouter = createGetSubjectStatusRouter(ctx) app.route('/', getPostsRouter) app.route('/', getPostThreadRouter) @@ -121,7 +125,8 @@ export class Server { app.route('/', updateSubjectStatusRouter) app.route('/', takedownRouter) app.route('/', getRecordRouter) - + app.route('/', getAccountInfosRouter) + app.route('/', getSubjectStatusRouter) app.route('/', wellKnownRouter()) // Root route diff --git a/services/appview/src/middleware/takedown-filter.ts b/services/appview/src/middleware/takedown-filter.ts index b775af5..66195e9 100644 --- a/services/appview/src/middleware/takedown-filter.ts +++ b/services/appview/src/middleware/takedown-filter.ts @@ -36,8 +36,8 @@ export const takedownFilterMiddleware = async (c: Context, next: Next) => { body.profile?.did || body.subject?.did if (targetDid) { - const isRepoTakenDown = await takedownService.isRepoTakenDown(targetDid) - if (isRepoTakenDown) { + const repoTakedown = await takedownService.getRepoTakedown(targetDid) + if (repoTakedown?.applied) { // For specific user/profile views, return minimal placeholder if (body.did && body.$type && body.$type.includes('profileView')) { const takenDownProfile = { @@ -86,16 +86,14 @@ export const takedownFilterMiddleware = async (c: Context, next: Next) => { ) body.feed = filteredFeed } else if (body.thread && body.thread.post) { - const isThreadTakenDown = await takedownService.isTakenDown( - body.thread.post.uri, - ) + const takedown = await takedownService.getTakedown(body.thread.post.uri) + const isThreadTakenDown = takedown?.applied ?? false // Also check if the thread author repo is taken down let isAuthorTakenDown = false if (body.thread.post.author?.did) { - isAuthorTakenDown = await takedownService.isRepoTakenDown( - body.thread.post.author.did, - ) + const repoTakedown = await takedownService.getRepoTakedown(body.thread.post.author.did) + isAuthorTakenDown = repoTakedown?.applied ?? false } if (isThreadTakenDown || isAuthorTakenDown) { @@ -117,17 +115,15 @@ export const takedownFilterMiddleware = async (c: Context, next: Next) => { body.profiles = filteredProfiles } else if (body.profile) { if (body.profile.did) { - const isRepoTakenDown = await takedownService.isRepoTakenDown( - body.profile.did, - ) - if (isRepoTakenDown) { + const repoTakedown = await takedownService.getRepoTakedown(body.profile.did) + if (repoTakedown?.applied) { body.profile = null } } } else if (body.did && body.$type && body.$type.includes('profileView')) { // For direct ProfileViewDetailed objects (so.sprk.actor.getProfile) - const isRepoTakenDown = await takedownService.isRepoTakenDown(body.did) - if (isRepoTakenDown) { + const repoTakedown = await takedownService.getRepoTakedown(body.did) + if (repoTakedown?.applied) { // Return a minimal placeholder object for taken-down profiles const takenDownProfile = { $type: body.$type, @@ -150,10 +146,8 @@ export const takedownFilterMiddleware = async (c: Context, next: Next) => { } else if (body.subject) { // For followers/follows response that has a subject profile if (body.subject.did) { - const isRepoTakenDown = await takedownService.isRepoTakenDown( - body.subject.did, - ) - if (isRepoTakenDown) { + const repoTakedown = await takedownService.getRepoTakedown(body.subject.did) + if (repoTakedown?.applied) { // Keep minimal info about the profile but mark it as taken down body.subject = { $type: body.subject.$type, @@ -208,7 +202,8 @@ async function filterTakenDownItems( // Get URI for this specific content const uri = get(item, uriPath) as string | undefined if (uri) { - isTakenDown = await takedownService.isTakenDown(uri) + const takedown = await takedownService.getTakedown(uri) + isTakenDown = takedown?.applied ?? false } // Check if author's repo is taken down @@ -221,17 +216,16 @@ async function filterTakenDownItems( get(item, 'actor.did') if (authorDid) { - isAuthorTakenDown = await takedownService.isRepoTakenDown(authorDid) + const repoTakedown = await takedownService.getRepoTakedown(authorDid) + isAuthorTakenDown = repoTakedown?.applied ?? false } // Keep the item only if neither the content nor the author is taken down if (!isTakenDown && !isAuthorTakenDown) { // Also check for any embedded items like quotes or replies if (item.embed && item.embed.record && item.embed.record.author?.did) { - const embedAuthorTakenDown = await takedownService.isRepoTakenDown( - item.embed.record.author.did, - ) - if (embedAuthorTakenDown) { + const embedAuthorTakedown = await takedownService.getRepoTakedown(item.embed.record.author.did) + if (embedAuthorTakedown?.applied) { // Null out the embed if from a taken-down repo item.embed = { $type: item.embed.$type, @@ -239,10 +233,8 @@ async function filterTakenDownItems( } } else if (item.embed.record.uri) { // Check if the specific embedded content is taken down - const embedContentTakenDown = await takedownService.isTakenDown( - item.embed.record.uri, - ) - if (embedContentTakenDown) { + const embedContentTakedown = await takedownService.getTakedown(item.embed.record.uri) + if (embedContentTakedown?.applied) { item.embed = { $type: item.embed.$type, takenDown: true, @@ -268,8 +260,8 @@ async function filterTakenDownRepos( for (const profile of profiles) { if (profile.did) { - const isRepoTakenDown = await takedownService.isRepoTakenDown(profile.did) - if (!isRepoTakenDown) { + const repoTakedown = await takedownService.getRepoTakedown(profile.did) + if (!repoTakedown?.applied) { filteredProfiles.push(profile) } else { // For UI consistency, push a minimal placeholder for taken-down profiles @@ -304,11 +296,8 @@ async function filterTakenDownBlobs( for (const image of images) { // Check if the image is taken down based on blob CID if (image.cid && image.did) { - const isBlobTakenDown = await takedownService.isBlobTakenDown( - image.did, - image.cid, - ) - if (!isBlobTakenDown) { + const blobTakedown = await takedownService.getBlobTakedown(image.did, image.cid) + if (!blobTakedown?.applied) { filteredImages.push(image) } } else { @@ -331,14 +320,14 @@ async function filterReplies( for (const reply of replies) { if (reply.post && reply.post.uri) { - const isTakenDown = await takedownService.isTakenDown(reply.post.uri) + const takedown = await takedownService.getTakedown(reply.post.uri) + const isTakenDown = takedown?.applied ?? false // Check if author's repo is taken down let isAuthorTakenDown = false if (reply.post.author?.did) { - isAuthorTakenDown = await takedownService.isRepoTakenDown( - reply.post.author.did, - ) + const repoTakedown = await takedownService.getRepoTakedown(reply.post.author.did) + isAuthorTakenDown = repoTakedown?.applied ?? false } if (!isTakenDown && !isAuthorTakenDown) { diff --git a/services/appview/src/routes/com/atproto/admin/getAccountInfos.ts b/services/appview/src/routes/com/atproto/admin/getAccountInfos.ts index 75839e0..a48462a 100644 --- a/services/appview/src/routes/com/atproto/admin/getAccountInfos.ts +++ b/services/appview/src/routes/com/atproto/admin/getAccountInfos.ts @@ -1,30 +1,45 @@ import { Hono } from 'hono' -import { zValidator } from '@hono/zod-validator' -import { z } from 'zod' -import { HTTPException } from 'hono/http-exception' -import { TakedownService } from '../../../../services/takedown.js' import { authMiddleware } from '../../../../auth/middleware.js' import { AppContext } from '../../../../index.js' -import type * as ComAtprotoAdminUpdateSubjectStatus from '../../../../lexicon/types/com/atproto/admin/updateSubjectStatus.js' -import type * as ComAtprotoAdminDefs from '../../../../lexicon/types/com/atproto/admin/defs.js' -import type * as ComAtprotoRepoStrongRef from '../../../../lexicon/types/com/atproto/repo/strongRef.js' +import { mapDefined } from '@atproto/common' +import { INVALID_HANDLE } from '@atproto/syntax' -export const createGetAccountInfosRouter = (_ctx: AppContext) => { +export const createGetAccountInfosRouter = (ctx: AppContext) => { const router = new Hono() // XRPC endpoint for Ozone integration: com.atproto.admin.getAccountInfos router.get( '/xrpc/com.atproto.admin.getAccountInfos', (c, next) => authMiddleware(c, next, true), - zValidator( - 'json', - z.object({ - dids: z.array(z.string()), - }), - ), async (c) => { - const { dids } = c.req.valid('json') - } + const dids = c.req.queries('dids[]') + if (!dids || dids.length === 0) { + return c.json({ error: 'Missing or empty dids parameter' }, 400) + } + + const timestamp = new Date().toISOString() + + const infos = await Promise.all( + mapDefined(dids, async (did) => { + await ctx.indexingService.indexHandle(did, timestamp) + const actor = await ctx.db.models.Actor.findOne({ did }) + if (!actor) return + + const profile = await ctx.db.models.Profile.findOne({ + did: actor.did, + }) + + return { + did: actor.did, + handle: actor.handle ?? INVALID_HANDLE, + relatedRecords: [profile], + indexedAt: actor.indexedAt, + } + }), + ) + + return c.json(infos) + }, ) return router diff --git a/services/appview/src/routes/com/atproto/admin/getSubjectStatus.ts b/services/appview/src/routes/com/atproto/admin/getSubjectStatus.ts new file mode 100644 index 0000000..72b6d70 --- /dev/null +++ b/services/appview/src/routes/com/atproto/admin/getSubjectStatus.ts @@ -0,0 +1,86 @@ +import { Hono } from 'hono' +import { authMiddleware, optionalAuthMiddleware } from '../../../../auth/middleware.js' +import { AppContext } from '../../../../index.js' + +export const createGetSubjectStatusRouter = (ctx: AppContext) => { + const router = new Hono() + + router.get('/xrpc/com.atproto.admin.getSubjectStatus', (c, next) => optionalAuthMiddleware(c, next), async (c) => { + const did = c.req.query('did') + const uri = c.req.query('uri') + const blob = c.req.query('blob') + + if (!did && !uri && !blob) { + return c.json({ error: 'Missing required parameter' }, 400) + } + + let subject + let takedown + if (did) { + const actor = await ctx.db.models.Actor.findOne({ did }) + const repoTakedown = await ctx.db.models.RepoTakedown.findOne({ + subjectDid: did + }) + if (!actor) { + return c.json({ error: 'Actor not found' }, 404) + } + subject = { + did: actor.did, + } + if (repoTakedown) { + takedown = { + applied: repoTakedown.applied, + ref: repoTakedown.ref, + } + } + } else if (uri) { + const record = + (await ctx.db.models.Profile.findOne({ uri })) ?? + (await ctx.db.models.Post.findOne({ uri })) ?? + (await ctx.db.models.Audio.findOne({ uri })) + const recordTakedown = await ctx.db.models.Takedown.findOne({ + subjectUri: uri, + }) + if (!record) { + return c.json({ error: 'Record not found' }, 404) + } + subject = { + uri: record.uri, + cid: record.cid, + } + if (recordTakedown) { + takedown = { + applied: recordTakedown.applied, + ref: recordTakedown.ref, + } + } + } else if (blob) { + const blobRecord = + (await ctx.db.models.Profile.findOne({ blob })) ?? + (await ctx.db.models.Post.findOne({ blob })) ?? + (await ctx.db.models.Audio.findOne({ blob })) + if (!blobRecord) { + return c.json({ error: 'Blob record not found' }, 404) + } + subject = { + did: blobRecord.authorDid, + cid: blobRecord.cid, + recordUri: blobRecord.uri, + } + const blobTakedown = await ctx.db.models.BlobTakedown.findOne({ + subjectDid: blobRecord.authorDid, + subjectCid: blobRecord.cid, + }) + if (blobTakedown) { + takedown = { + applied: blobTakedown.applied, + ref: blobTakedown.ref, + } + } + } + + return c.json({ subject, takedown }) + }) + + return router +} diff --git a/services/appview/src/routes/com/atproto/admin/updateSubjectStatus.ts b/services/appview/src/routes/com/atproto/admin/updateSubjectStatus.ts index c1caa22..1db898c 100644 --- a/services/appview/src/routes/com/atproto/admin/updateSubjectStatus.ts +++ b/services/appview/src/routes/com/atproto/admin/updateSubjectStatus.ts @@ -59,6 +59,7 @@ export const createUpdateSubjectStatusRouter = ( adminDid, ref: takedown.ref, }) + await takedownService.updateRepoTakedownApplied(subject.did, takedown.applied) } else { // Remove takedown await takedownService.removeRepoTakedown(subject.did) @@ -79,6 +80,7 @@ export const createUpdateSubjectStatusRouter = ( reason: 'Moderation via Ozone', adminDid, }) + await takedownService.updateTakedownApplied(subject.uri, takedown.applied) } else { // Remove takedown await takedownService.removeTakedown(subject.uri) @@ -100,6 +102,7 @@ export const createUpdateSubjectStatusRouter = ( adminDid, ref: takedown.ref, }) + await takedownService.updateBlobTakedownApplied(subject.did, subject.cid, takedown.applied) } else { // Remove takedown await takedownService.removeBlobTakedown(subject.did, subject.cid) diff --git a/services/appview/src/services/takedown.ts b/services/appview/src/services/takedown.ts index 88c3ac9..ee72792 100644 --- a/services/appview/src/services/takedown.ts +++ b/services/appview/src/services/takedown.ts @@ -18,6 +18,7 @@ export class TakedownService { reason, takenDownBy: adminDid, takenDownAt: new Date().toISOString(), + applied: true, }) } @@ -37,6 +38,7 @@ export class TakedownService { takenDownBy: adminDid, takenDownAt: new Date().toISOString(), ref: ref || null, + applied: false, }) } @@ -58,6 +60,7 @@ export class TakedownService { takenDownBy: adminDid, takenDownAt: new Date().toISOString(), ref: ref || null, + applied: false, }) } @@ -77,6 +80,7 @@ export class TakedownService { reason: string takenDownBy: string takenDownAt: string + applied: boolean } | null> { const takedown = await this.db.models.Takedown.findOne({ targetUri: uri }).lean() return takedown @@ -216,4 +220,50 @@ export class TakedownService { cursor: takedowns.length > limit ? takedowns[limit - 1].did : undefined } } + + async updateTakedownApplied(targetUri: string, applied: boolean): Promise { + await this.db.models.Takedown.updateOne( + { targetUri }, + { $set: { applied } } + ) + } + + async updateRepoTakedownApplied(did: string, applied: boolean): Promise { + await this.db.models.RepoTakedown.updateOne( + { did }, + { $set: { applied } } + ) + } + + async updateBlobTakedownApplied(did: string, cid: string, applied: boolean): Promise { + await this.db.models.BlobTakedown.updateOne( + { did, cid }, + { $set: { applied } } + ) + } + + async getRepoTakedown(did: string): Promise<{ + did: string + reason: string + takenDownBy: string + takenDownAt: string + ref: string | null + applied: boolean + } | null> { + const takedown = await this.db.models.RepoTakedown.findOne({ did }).lean() + return takedown + } + + async getBlobTakedown(did: string, cid: string): Promise<{ + did: string + cid: string + reason: string + takenDownBy: string + takenDownAt: string + ref: string | null + applied: boolean + } | null> { + const takedown = await this.db.models.BlobTakedown.findOne({ did, cid }).lean() + return takedown + } } \ No newline at end of file -- 2.51.2