diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 09699b8914b..0dacf02bcf5 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -11,6 +11,7 @@ import { } from '../../../../services/actor' import { setRepoRev } from '../../../util' import { createPipeline, noRules } from '../../../../pipeline' +import { ModerationService } from '../../../../services/moderation' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) @@ -19,6 +20,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params, res }) => { const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) + const modService = ctx.services.moderation(ctx.db.getPrimary()) const viewer = 'did' in auth.credentials ? auth.credentials.did : null const canViewTakendownProfile = auth.credentials.type === 'role' && auth.credentials.triage @@ -26,7 +28,7 @@ export default function (server: Server, ctx: AppContext) { const [result, repoRev] = await Promise.allSettled([ getProfile( { ...params, viewer, canViewTakendownProfile }, - { db, actorService }, + { db, actorService, modService }, ), actorService.getRepoRev(viewer), ]) @@ -50,17 +52,25 @@ const skeleton = async ( params: Params, ctx: Context, ): Promise => { - const { actorService } = ctx + const { actorService, modService } = ctx const { canViewTakendownProfile } = params const actor = await actorService.getActor(params.actor, true) if (!actor) { throw new InvalidRequestError('Profile not found') } if (!canViewTakendownProfile && softDeleted(actor)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) + const isSuspended = await modService.isSubjectSuspended(actor.did) + if (isSuspended) { + throw new InvalidRequestError( + 'Account has been temporarily suspended', + 'AccountTakedown', + ) + } else { + throw new InvalidRequestError( + 'Account has been taken down', + 'AccountTakedown', + ) + } } return { params, actor } } @@ -95,6 +105,7 @@ const presentation = (state: HydrationState, ctx: Context) => { type Context = { db: Database actorService: ActorService + modService: ModerationService } type Params = QueryParams & { diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index ff22d96ea7e..717155d0317 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -323,6 +323,18 @@ export class ModerationService { return subjectsDueForReversal } + async isSubjectSuspended(did: string): Promise { + const res = await this.db.db + .selectFrom('moderation_subject_status') + .where('did', '=', did) + .where('recordPath', '=', '') + .where('suspendUntil', '>', new Date().toISOString()) + .select('did') + .limit(1) + .executeTakeFirst() + return !!res + } + async revertState({ createdBy, createdAt, diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index 726fb990a0d..fe3f689894b 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -224,6 +224,52 @@ describe('pds profile views', () => { ) }) + it('blocked by actor suspension', async () => { + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { + $type: 'com.atproto.admin.defs#modEventTakedown', + durationInHours: 1, + }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: alice, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + const promise = agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: await network.serviceHeaders(bob) }, + ) + + await expect(promise).rejects.toThrow( + 'Account has been temporarily suspended', + ) + + // Cleanup + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventReverseTakedown' }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: alice, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + }) + async function updateProfile(did: string, record: Record) { return await pdsAgent.api.com.atproto.repo.putRecord( {