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

feedback

+357 -302
+7 -9
services/appview/src/index.ts
··· 20 20 import { createGetFollowsRouter } from './routes/graph/getFollows.js' 21 21 import wellKnownRouter from './well-known.js' 22 22 import { TakedownService } from './services/takedown.js' 23 - import takedownRoutes from './routes/admin/takedowns.js' 23 + import { createTakedownRouter } from './routes/admin/takedowns.js' 24 24 import { takedownFilterMiddleware } from './middleware/takedown-filter.js' 25 25 26 26 export type AppContext = { ··· 76 76 await next() 77 77 }) 78 78 79 + // Apply takedown filter middleware to all routes 80 + app.use('*', takedownFilterMiddleware) 81 + 79 82 // TODO: Remove this after getAuthorFeedRouter is properly implemented on frontend 80 83 const feedRouter = createFeedRouter(ctx) 81 84 app.route('/', feedRouter) ··· 87 90 const getFollowsRouter = createGetFollowsRouter(ctx) 88 91 const getAuthorFeedRouter = createGetAuthorFeedRouter(ctx) 89 92 90 - // Apply takedown filter middleware to content routes 91 - app.use('/', takedownFilterMiddleware) 92 - 93 93 app.route('/', getPostsRouter) 94 94 app.route('/', getPostThreadRouter) 95 95 app.route('/', getProfileRouter) ··· 97 97 app.route('/', getFollowsRouter) 98 98 app.route('/', getAuthorFeedRouter) 99 99 100 - // Admin routes 101 - app.route('/admin/takedowns', takedownRoutes) 102 - 103 - // XRPC routes - make sure the Ozone endpoint is accessible 104 - app.route('/xrpc', takedownRoutes) 100 + // Create and configure the takedown router 101 + const takedownRouter = createTakedownRouter({ takedownService }) 102 + app.route('/', takedownRouter) 105 103 106 104 app.route('/', wellKnownRouter()) 107 105
+66 -1
services/appview/src/middleware/takedown-filter.ts
··· 7 7 * that might have been taken down by admins 8 8 */ 9 9 export const takedownFilterMiddleware = async (c: Context, next: Next) => { 10 + // Skip filtering for admin routes and non-content routes 11 + const path = c.req.path 12 + if (path.startsWith('/admin/') || path === '/' || path.includes('favicon') || path.includes('xrpc/com.atproto.admin.updateSubjectStatus')) { 13 + await next() 14 + return 15 + } 16 + 10 17 // Call the next middleware/route handler first 11 18 await next() 12 19 ··· 49 56 const filteredProfiles = await filterTakenDownRepos(body.profiles, takedownService) 50 57 body.profiles = filteredProfiles 51 58 } else if (body.profile) { 52 - // For single profile view 53 59 if (body.profile.did) { 54 60 const isRepoTakenDown = await takedownService.isRepoTakenDown(body.profile.did) 55 61 if (isRepoTakenDown) { 56 62 body.profile = null 57 63 } 58 64 } 65 + } else if (body.did && body.$type && body.$type.includes('profileView')) { 66 + // For direct ProfileViewDetailed objects (so.sprk.actor.getProfile) 67 + const isRepoTakenDown = await takedownService.isRepoTakenDown(body.did) 68 + if (isRepoTakenDown) { 69 + // Return a minimal placeholder object for taken-down profiles 70 + const takenDownProfile = { 71 + $type: body.$type, 72 + did: body.did, 73 + handle: body.handle || 'unavailable', 74 + moderation: { 75 + takenDown: true 76 + } 77 + } 78 + 79 + // Create a new response with the placeholder instead of trying to modify body 80 + c.res = new Response(JSON.stringify(takenDownProfile), { 81 + status: c.res.status, 82 + headers: c.res.headers, 83 + }) 84 + 85 + // Skip the rest of the processing 86 + return 87 + } 88 + } else if (body.subject) { 89 + // For followers/follows response that has a subject profile 90 + if (body.subject.did) { 91 + const isRepoTakenDown = await takedownService.isRepoTakenDown(body.subject.did) 92 + if (isRepoTakenDown) { 93 + // Keep minimal info about the profile but mark it as taken down 94 + body.subject = { 95 + $type: body.subject.$type, 96 + did: body.subject.did, 97 + handle: body.subject.handle || 'unavailable', 98 + moderation: { 99 + takenDown: true 100 + } 101 + } 102 + } 103 + } 104 + 105 + // Also filter any followers/follows list 106 + if (body.followers && Array.isArray(body.followers)) { 107 + body.followers = await filterTakenDownRepos(body.followers, takedownService) 108 + } 109 + 110 + if (body.follows && Array.isArray(body.follows)) { 111 + body.follows = await filterTakenDownRepos(body.follows, takedownService) 112 + } 59 113 } 60 114 61 115 // Set the filtered response ··· 125 179 const isRepoTakenDown = await takedownService.isRepoTakenDown(profile.did) 126 180 if (!isRepoTakenDown) { 127 181 filteredProfiles.push(profile) 182 + } else { 183 + // For UI consistency, push a minimal placeholder for taken-down profiles 184 + // if they need to be represented in lists (follows, followers, etc.) 185 + filteredProfiles.push({ 186 + $type: profile.$type, 187 + did: profile.did, 188 + handle: profile.handle || 'unavailable', 189 + moderation: { 190 + takenDown: true 191 + } 192 + }) 128 193 } 129 194 } else { 130 195 // If no DID, keep the profile
+284 -292
services/appview/src/routes/admin/takedowns.ts
··· 5 5 import { TakedownService } from '../../services/takedown.js' 6 6 import { adminAuthMiddleware } from '../../auth/middleware.js' 7 7 8 - const takedownRoutes = new Hono() 8 + type TakedownContext = { 9 + takedownService: TakedownService 10 + } 9 11 10 - // Apply admin auth middleware to all routes 11 - takedownRoutes.use('*', adminAuthMiddleware) 12 + export const createTakedownRouter = (ctx: TakedownContext) => { 13 + const takedownRoutes = new Hono() 14 + const takedownService = ctx.takedownService 12 15 13 - // Create a takedown 14 - takedownRoutes.post('/', zValidator('json', z.object({ 15 - targetUri: z.string(), 16 - targetCid: z.string(), 17 - reason: z.string(), 18 - })), async (c) => { 19 - const { targetUri, targetCid, reason } = c.req.valid('json') 20 - const adminDid = c.get('did') 21 - 22 - const takedownService = c.get('takedownService') as TakedownService 23 - 24 - try { 25 - await takedownService.takedownContent({ 26 - targetUri, 27 - targetCid, 28 - reason, 29 - adminDid, 30 - }) 31 - 32 - return c.json({ success: true }, 201) 33 - } catch (error) { 34 - throw new HTTPException(500, { message: 'Failed to create takedown' }) 35 - } 36 - }) 16 + // Apply admin auth middleware to all admin routes 17 + takedownRoutes.use('/admin/*', adminAuthMiddleware) 37 18 38 - // Remove a takedown (record/post) 39 - takedownRoutes.delete('/:uri', async (c) => { 40 - const uri = c.req.param('uri') 41 - const takedownService = c.get('takedownService') as TakedownService 42 - 43 - try { 44 - const removed = await takedownService.removeTakedown(uri) 19 + // No auth needed for the Ozone integration endpoint as it will use its own auth 20 + // This will be protected by adminAuthMiddleware before processing 21 + 22 + takedownRoutes.post('/admin/takedowns', zValidator('json', z.object({ 23 + targetUri: z.string(), 24 + targetCid: z.string(), 25 + reason: z.string(), 26 + })), async (c) => { 27 + const { targetUri, targetCid, reason } = c.req.valid('json') 28 + const adminDid = c.get('did') 45 29 46 - if (!removed) { 47 - return c.json({ success: false, message: 'Takedown not found' }, 404) 30 + try { 31 + await takedownService.takedownContent({ 32 + targetUri, 33 + targetCid, 34 + reason, 35 + adminDid, 36 + }) 37 + 38 + return c.json({ success: true }, 201) 39 + } catch (error) { 40 + throw new HTTPException(500, { message: 'Failed to create takedown' }) 48 41 } 49 - 50 - return c.json({ success: true }) 51 - } catch (error) { 52 - throw new HTTPException(500, { message: 'Failed to remove takedown' }) 53 - } 54 - }) 42 + }) 55 43 56 - // Create a repo takedown 57 - takedownRoutes.post('/repo', zValidator('json', z.object({ 58 - did: z.string(), 59 - reason: z.string(), 60 - ref: z.string().optional(), 61 - })), async (c) => { 62 - const { did, reason, ref } = c.req.valid('json') 63 - const adminDid = c.get('did') 64 - 65 - const takedownService = c.get('takedownService') as TakedownService 66 - 67 - try { 68 - await takedownService.takedownRepo({ 69 - did, 70 - reason, 71 - adminDid, 72 - ref, 73 - }) 44 + // Remove a takedown (record/post) 45 + takedownRoutes.delete('/admin/takedowns/:uri', async (c) => { 46 + const uri = c.req.param('uri') 74 47 75 - return c.json({ success: true }, 201) 76 - } catch (error) { 77 - throw new HTTPException(500, { message: 'Failed to create repo takedown' }) 78 - } 79 - }) 48 + try { 49 + const removed = await takedownService.removeTakedown(uri) 50 + 51 + if (!removed) { 52 + return c.json({ success: false, message: 'Takedown not found' }, 404) 53 + } 54 + 55 + return c.json({ success: true }) 56 + } catch (error) { 57 + throw new HTTPException(500, { message: 'Failed to remove takedown' }) 58 + } 59 + }) 80 60 81 - // Remove a repo takedown 82 - takedownRoutes.delete('/repo/:did', async (c) => { 83 - const did = c.req.param('did') 84 - const takedownService = c.get('takedownService') as TakedownService 85 - 86 - try { 87 - const removed = await takedownService.removeRepoTakedown(did) 61 + // Create a repo takedown 62 + takedownRoutes.post('/admin/takedowns/repo', zValidator('json', z.object({ 63 + did: z.string(), 64 + reason: z.string(), 65 + ref: z.string().optional(), 66 + })), async (c) => { 67 + const { did, reason, ref } = c.req.valid('json') 68 + const adminDid = c.get('did') 88 69 89 - if (!removed) { 90 - return c.json({ success: false, message: 'Repo takedown not found' }, 404) 70 + try { 71 + await takedownService.takedownRepo({ 72 + did, 73 + reason, 74 + adminDid, 75 + ref, 76 + }) 77 + 78 + return c.json({ success: true }, 201) 79 + } catch (error) { 80 + throw new HTTPException(500, { message: 'Failed to create repo takedown' }) 91 81 } 92 - 93 - return c.json({ success: true }) 94 - } catch (error) { 95 - throw new HTTPException(500, { message: 'Failed to remove repo takedown' }) 96 - } 97 - }) 82 + }) 98 83 99 - // Create a blob takedown 100 - takedownRoutes.post('/blob', zValidator('json', z.object({ 101 - did: z.string(), 102 - cid: z.string(), 103 - reason: z.string(), 104 - ref: z.string().optional(), 105 - })), async (c) => { 106 - const { did, cid, reason, ref } = c.req.valid('json') 107 - const adminDid = c.get('did') 108 - 109 - const takedownService = c.get('takedownService') as TakedownService 110 - 111 - try { 112 - await takedownService.takedownBlob({ 113 - did, 114 - cid, 115 - reason, 116 - adminDid, 117 - ref, 118 - }) 84 + // Remove a repo takedown 85 + takedownRoutes.delete('/admin/takedowns/repo/:did', async (c) => { 86 + const did = c.req.param('did') 119 87 120 - return c.json({ success: true }, 201) 121 - } catch (error) { 122 - throw new HTTPException(500, { message: 'Failed to create blob takedown' }) 123 - } 124 - }) 88 + try { 89 + const removed = await takedownService.removeRepoTakedown(did) 90 + 91 + if (!removed) { 92 + return c.json({ success: false, message: 'Repo takedown not found' }, 404) 93 + } 94 + 95 + return c.json({ success: true }) 96 + } catch (error) { 97 + throw new HTTPException(500, { message: 'Failed to remove repo takedown' }) 98 + } 99 + }) 125 100 126 - // Remove a blob takedown 127 - takedownRoutes.delete('/blob/:did/:cid', async (c) => { 128 - const did = c.req.param('did') 129 - const cid = c.req.param('cid') 130 - const takedownService = c.get('takedownService') as TakedownService 131 - 132 - try { 133 - const removed = await takedownService.removeBlobTakedown(did, cid) 101 + // Create a blob takedown 102 + takedownRoutes.post('/admin/takedowns/blob', zValidator('json', z.object({ 103 + did: z.string(), 104 + cid: z.string(), 105 + reason: z.string(), 106 + ref: z.string().optional(), 107 + })), async (c) => { 108 + const { did, cid, reason, ref } = c.req.valid('json') 109 + const adminDid = c.get('did') 134 110 135 - if (!removed) { 136 - return c.json({ success: false, message: 'Blob takedown not found' }, 404) 111 + try { 112 + await takedownService.takedownBlob({ 113 + did, 114 + cid, 115 + reason, 116 + adminDid, 117 + ref, 118 + }) 119 + 120 + return c.json({ success: true }, 201) 121 + } catch (error) { 122 + throw new HTTPException(500, { message: 'Failed to create blob takedown' }) 137 123 } 124 + }) 125 + 126 + // Remove a blob takedown 127 + takedownRoutes.delete('/admin/takedowns/blob/:did/:cid', async (c) => { 128 + const did = c.req.param('did') 129 + const cid = c.req.param('cid') 138 130 139 - return c.json({ success: true }) 140 - } catch (error) { 141 - throw new HTTPException(500, { message: 'Failed to remove blob takedown' }) 142 - } 143 - }) 131 + try { 132 + const removed = await takedownService.removeBlobTakedown(did, cid) 133 + 134 + if (!removed) { 135 + return c.json({ success: false, message: 'Blob takedown not found' }, 404) 136 + } 137 + 138 + return c.json({ success: true }) 139 + } catch (error) { 140 + throw new HTTPException(500, { message: 'Failed to remove blob takedown' }) 141 + } 142 + }) 144 143 145 - // List takedowns 146 - takedownRoutes.get('/', zValidator('query', z.object({ 147 - limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 148 - cursor: z.string().optional(), 149 - })), async (c) => { 150 - const { limit, cursor } = c.req.valid('query') 151 - const takedownService = c.get('takedownService') as TakedownService 152 - 153 - try { 154 - const result = await takedownService.listTakedowns(limit, cursor) 155 - return c.json(result) 156 - } catch (error) { 157 - throw new HTTPException(500, { message: 'Failed to list takedowns' }) 158 - } 159 - }) 144 + // List takedowns 145 + takedownRoutes.get('/admin/takedowns', zValidator('query', z.object({ 146 + limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 147 + cursor: z.string().optional(), 148 + })), async (c) => { 149 + const { limit, cursor } = c.req.valid('query') 150 + 151 + try { 152 + const result = await takedownService.listTakedowns(limit, cursor) 153 + return c.json(result) 154 + } catch (error) { 155 + throw new HTTPException(500, { message: 'Failed to list takedowns' }) 156 + } 157 + }) 160 158 161 - // List repo takedowns 162 - takedownRoutes.get('/repo', zValidator('query', z.object({ 163 - limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 164 - cursor: z.string().optional(), 165 - })), async (c) => { 166 - const { limit, cursor } = c.req.valid('query') 167 - const takedownService = c.get('takedownService') as TakedownService 168 - 169 - try { 170 - const result = await takedownService.listRepoTakedowns(limit, cursor) 171 - return c.json(result) 172 - } catch (error) { 173 - throw new HTTPException(500, { message: 'Failed to list repo takedowns' }) 174 - } 175 - }) 159 + // List repo takedowns 160 + takedownRoutes.get('/admin/takedowns/repo', zValidator('query', z.object({ 161 + limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 162 + cursor: z.string().optional(), 163 + })), async (c) => { 164 + const { limit, cursor } = c.req.valid('query') 165 + 166 + try { 167 + const result = await takedownService.listRepoTakedowns(limit, cursor) 168 + return c.json(result) 169 + } catch (error) { 170 + throw new HTTPException(500, { message: 'Failed to list repo takedowns' }) 171 + } 172 + }) 176 173 177 - // List blob takedowns 178 - takedownRoutes.get('/blob', zValidator('query', z.object({ 179 - limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 180 - cursor: z.string().optional(), 181 - })), async (c) => { 182 - const { limit, cursor } = c.req.valid('query') 183 - const takedownService = c.get('takedownService') as TakedownService 184 - 185 - try { 186 - const result = await takedownService.listBlobTakedowns(limit, cursor) 187 - return c.json(result) 188 - } catch (error) { 189 - throw new HTTPException(500, { message: 'Failed to list blob takedowns' }) 190 - } 191 - }) 174 + // List blob takedowns 175 + takedownRoutes.get('/admin/takedowns/blob', zValidator('query', z.object({ 176 + limit: z.string().optional().transform(val => val ? parseInt(val) : 50), 177 + cursor: z.string().optional(), 178 + })), async (c) => { 179 + const { limit, cursor } = c.req.valid('query') 180 + 181 + try { 182 + const result = await takedownService.listBlobTakedowns(limit, cursor) 183 + return c.json(result) 184 + } catch (error) { 185 + throw new HTTPException(500, { message: 'Failed to list blob takedowns' }) 186 + } 187 + }) 192 188 193 - // Check if content is taken down 194 - takedownRoutes.get('/check/:uri', async (c) => { 195 - const uri = c.req.param('uri') 196 - const takedownService = c.get('takedownService') as TakedownService 197 - 198 - try { 199 - const isTakenDown = await takedownService.isTakenDown(uri) 200 - return c.json({ isTakenDown }) 201 - } catch (error) { 202 - throw new HTTPException(500, { message: 'Failed to check takedown status' }) 203 - } 204 - }) 189 + // Check if content is taken down 190 + takedownRoutes.get('/admin/takedowns/check/:uri', async (c) => { 191 + const uri = c.req.param('uri') 192 + 193 + try { 194 + const isTakenDown = await takedownService.isTakenDown(uri) 195 + return c.json({ isTakenDown }) 196 + } catch (error) { 197 + throw new HTTPException(500, { message: 'Failed to check takedown status' }) 198 + } 199 + }) 205 200 206 - // Check if repo is taken down 207 - takedownRoutes.get('/check/repo/:did', async (c) => { 208 - const did = c.req.param('did') 209 - const takedownService = c.get('takedownService') as TakedownService 210 - 211 - try { 212 - const isTakenDown = await takedownService.isRepoTakenDown(did) 213 - return c.json({ isTakenDown }) 214 - } catch (error) { 215 - throw new HTTPException(500, { message: 'Failed to check repo takedown status' }) 216 - } 217 - }) 201 + // Check if repo is taken down 202 + takedownRoutes.get('/admin/takedowns/check/repo/:did', async (c) => { 203 + const did = c.req.param('did') 204 + 205 + try { 206 + const isTakenDown = await takedownService.isRepoTakenDown(did) 207 + return c.json({ isTakenDown }) 208 + } catch (error) { 209 + throw new HTTPException(500, { message: 'Failed to check repo takedown status' }) 210 + } 211 + }) 218 212 219 - // Check if blob is taken down 220 - takedownRoutes.get('/check/blob/:did/:cid', async (c) => { 221 - const did = c.req.param('did') 222 - const cid = c.req.param('cid') 223 - const takedownService = c.get('takedownService') as TakedownService 224 - 225 - try { 226 - const isTakenDown = await takedownService.isBlobTakenDown(did, cid) 227 - return c.json({ isTakenDown }) 228 - } catch (error) { 229 - throw new HTTPException(500, { message: 'Failed to check blob takedown status' }) 230 - } 231 - }) 213 + // Check if blob is taken down 214 + takedownRoutes.get('/admin/takedowns/check/blob/:did/:cid', async (c) => { 215 + const did = c.req.param('did') 216 + const cid = c.req.param('cid') 217 + 218 + try { 219 + const isTakenDown = await takedownService.isBlobTakenDown(did, cid) 220 + return c.json({ isTakenDown }) 221 + } catch (error) { 222 + throw new HTTPException(500, { message: 'Failed to check blob takedown status' }) 223 + } 224 + }) 232 225 233 - // XRPC endpoint for Ozone integration: com.atproto.admin.updateSubjectStatus 234 - takedownRoutes.post('/xrpc/com.atproto.admin.updateSubjectStatus', zValidator('json', z.object({ 235 - subject: z.object({ 236 - $type: z.string(), 237 - did: z.string().optional(), 238 - uri: z.string().optional(), 239 - cid: z.string().optional(), 240 - }), 241 - takedown: z.object({ 242 - applied: z.boolean(), 243 - ref: z.string().optional(), 244 - }), 245 - })), async (c) => { 246 - const { subject, takedown } = c.req.valid('json') 247 - const adminDid = c.get('did') 248 - 249 - const takedownService = c.get('takedownService') as TakedownService 250 - 251 - try { 252 - // Handle different subject types 253 - if (subject.$type === 'com.atproto.admin.defs#repoRef') { 254 - // Repository (user account) takedown 255 - if (!subject.did) { 256 - throw new HTTPException(400, { message: 'DID is required for repo takedowns' }) 257 - } 258 - 259 - if (takedown.applied) { 260 - // Apply takedown 261 - await takedownService.takedownRepo({ 262 - did: subject.did, 263 - reason: 'Moderation via Ozone', 264 - adminDid, 265 - ref: takedown.ref, 266 - }) 267 - } else { 268 - // Remove takedown 269 - await takedownService.removeRepoTakedown(subject.did) 270 - } 271 - } else if (subject.$type === 'com.atproto.repo.strongRef') { 272 - // Record (post) takedown 273 - if (!subject.uri || !subject.cid) { 274 - throw new HTTPException(400, { message: 'URI and CID are required for record takedowns' }) 275 - } 276 - 277 - if (takedown.applied) { 278 - // Apply takedown 279 - await takedownService.takedownContent({ 280 - targetUri: subject.uri, 281 - targetCid: subject.cid, 282 - reason: 'Moderation via Ozone', 283 - adminDid, 284 - }) 226 + // XRPC endpoint for Ozone integration: com.atproto.admin.updateSubjectStatus 227 + takedownRoutes.post('/xrpc/com.atproto.admin.updateSubjectStatus', adminAuthMiddleware, zValidator('json', z.object({ 228 + subject: z.object({ 229 + $type: z.string(), 230 + did: z.string().optional(), 231 + uri: z.string().optional(), 232 + cid: z.string().optional(), 233 + }), 234 + takedown: z.object({ 235 + applied: z.boolean(), 236 + ref: z.string().optional(), 237 + }), 238 + })), async (c) => { 239 + const { subject, takedown } = c.req.valid('json') 240 + const adminDid = c.get('did') 241 + 242 + try { 243 + // Handle different subject types 244 + if (subject.$type === 'com.atproto.admin.defs#repoRef') { 245 + // Repository (user account) takedown 246 + if (!subject.did) { 247 + throw new HTTPException(400, { message: 'DID is required for repo takedowns' }) 248 + } 249 + 250 + if (takedown.applied) { 251 + // Apply takedown 252 + await takedownService.takedownRepo({ 253 + did: subject.did, 254 + reason: 'Moderation via Ozone', 255 + adminDid, 256 + ref: takedown.ref, 257 + }) 258 + } else { 259 + // Remove takedown 260 + await takedownService.removeRepoTakedown(subject.did) 261 + } 262 + } else if (subject.$type === 'com.atproto.repo.strongRef') { 263 + // Record (post) takedown 264 + if (!subject.uri || !subject.cid) { 265 + throw new HTTPException(400, { message: 'URI and CID are required for record takedowns' }) 266 + } 267 + 268 + if (takedown.applied) { 269 + // Apply takedown 270 + await takedownService.takedownContent({ 271 + targetUri: subject.uri, 272 + targetCid: subject.cid, 273 + reason: 'Moderation via Ozone', 274 + adminDid, 275 + }) 276 + } else { 277 + // Remove takedown 278 + await takedownService.removeTakedown(subject.uri) 279 + } 280 + } else if (subject.$type === 'com.atproto.admin.defs#repoBlobRef') { 281 + // Blob (image/attachment) takedown 282 + if (!subject.did || !subject.cid) { 283 + throw new HTTPException(400, { message: 'DID and CID are required for blob takedowns' }) 284 + } 285 + 286 + if (takedown.applied) { 287 + // Apply takedown 288 + await takedownService.takedownBlob({ 289 + did: subject.did, 290 + cid: subject.cid, 291 + reason: 'Moderation via Ozone', 292 + adminDid, 293 + ref: takedown.ref, 294 + }) 295 + } else { 296 + // Remove takedown 297 + await takedownService.removeBlobTakedown(subject.did, subject.cid) 298 + } 285 299 } else { 286 - // Remove takedown 287 - await takedownService.removeTakedown(subject.uri) 288 - } 289 - } else if (subject.$type === 'com.atproto.admin.defs#repoBlobRef') { 290 - // Blob (image/attachment) takedown 291 - if (!subject.did || !subject.cid) { 292 - throw new HTTPException(400, { message: 'DID and CID are required for blob takedowns' }) 300 + throw new HTTPException(400, { message: `Unsupported subject type: ${subject.$type}` }) 293 301 } 294 302 295 - if (takedown.applied) { 296 - // Apply takedown 297 - await takedownService.takedownBlob({ 298 - did: subject.did, 299 - cid: subject.cid, 300 - reason: 'Moderation via Ozone', 301 - adminDid, 302 - ref: takedown.ref, 303 - }) 304 - } else { 305 - // Remove takedown 306 - await takedownService.removeBlobTakedown(subject.did, subject.cid) 303 + // Return the response format expected by Ozone 304 + return c.json({ 305 + subject, 306 + takedown: takedown.applied ? { 307 + applied: takedown.applied, 308 + ref: takedown.ref 309 + } : undefined 310 + }) 311 + } catch (error) { 312 + if (error instanceof HTTPException) { 313 + throw error 307 314 } 308 - } else { 309 - throw new HTTPException(400, { message: `Unsupported subject type: ${subject.$type}` }) 310 - } 311 - 312 - // Return the response format expected by Ozone 313 - return c.json({ 314 - subject, 315 - takedown: takedown.applied ? { 316 - applied: takedown.applied, 317 - ref: takedown.ref 318 - } : undefined 319 - }) 320 - } catch (error) { 321 - if (error instanceof HTTPException) { 322 - throw error 315 + throw new HTTPException(500, { message: 'Failed to update subject status' }) 323 316 } 324 - throw new HTTPException(500, { message: 'Failed to update subject status' }) 325 - } 326 - }) 317 + }) 327 318 328 - export default takedownRoutes 319 + return takedownRoutes 320 + }