diff --git a/packages/api/docs/moderation.md b/packages/api/docs/moderation.md index 29d47eaab9c..6f49288775d 100644 --- a/packages/api/docs/moderation.md +++ b/packages/api/docs/moderation.md @@ -245,3 +245,14 @@ for (const inform of mod.ui('contentList').informs) { } ``` +## Sending moderation reports + +Any Labeler is capable of receiving moderation reports. As a result, you need to specify which labeler should receive the report. You do this with the `Atproto-Proxy` header: + +```typescript +agent.withProxy('atproto_labeler', 'did:web:my-labeler.com').createModerationReport({ + reasonType: 'com.atproto.moderation.defs#reasonViolation', + reason: 'They were being such a jerk to me!', + subject: {did: 'did:web:bob.com'} +}) +``` \ No newline at end of file diff --git a/packages/api/src/agent.ts b/packages/api/src/agent.ts index 5641ac6a777..87cfd4bcfb6 100644 --- a/packages/api/src/agent.ts +++ b/packages/api/src/agent.ts @@ -17,6 +17,7 @@ import { AtpAgentGlobalOpts, AtpPersistSessionHandler, AtpAgentOpts, + AtprotoServiceType, } from './types' import { BSKY_LABELER_DID } from './const' @@ -33,15 +34,12 @@ export class AtpAgent { api: AtpServiceClient session?: AtpSessionData labelersHeader: string[] = [] + proxyHeader: string | undefined + pdsUrl: URL | undefined // The PDS URL, driven by the did doc. May be undefined. - /** - * The PDS URL, driven by the did doc. May be undefined. - */ - pdsUrl: URL | undefined - - private _baseClient: AtpBaseClient - private _persistSession?: AtpPersistSessionHandler - private _refreshSessionPromise: Promise | undefined + protected _baseClient: AtpBaseClient + protected _persistSession?: AtpPersistSessionHandler + protected _refreshSessionPromise: Promise | undefined get com() { return this.api.com @@ -80,6 +78,27 @@ export class AtpAgent { this.api = this._baseClient.service(opts.service) } + clone() { + const inst = new AtpAgent({ + service: this.service, + }) + this.copyInto(inst) + return inst + } + + copyInto(inst: AtpAgent) { + inst.session = this.session + inst.labelersHeader = this.labelersHeader + inst.proxyHeader = this.proxyHeader + inst.pdsUrl = this.pdsUrl + } + + withProxy(serviceType: AtprotoServiceType, did: string) { + const inst = this.clone() + inst.configureProxyHeader(serviceType, did) + return inst + } + /** * Is there any active session? */ @@ -104,6 +123,15 @@ export class AtpAgent { this.labelersHeader = labelerDids } + /** + * Configures the atproto-proxy header to be applied on requests + */ + configureProxyHeader(serviceType: AtprotoServiceType, did: string) { + if (did.startsWith('did:')) { + this.proxyHeader = `${did}#${serviceType}` + } + } + /** * Create a new account and hydrate its session in this agent. */ @@ -224,6 +252,12 @@ export class AtpAgent { authorization: `Bearer ${this.session.accessJwt}`, } } + if (this.proxyHeader) { + reqHeaders = { + ...reqHeaders, + 'atproto-proxy': this.proxyHeader, + } + } reqHeaders = { ...reqHeaders, 'atproto-accept-labelers': AtpAgent.appLabelers diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 106167675e5..e51347bca7e 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -44,6 +44,14 @@ declare global { } export class BskyAgent extends AtpAgent { + clone() { + const inst = new BskyAgent({ + service: this.service, + }) + this.copyInto(inst) + return inst + } + get app() { return this.api.app } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 0f3c0191b33..a633ff79a33 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,6 +1,11 @@ import { AppBskyActorDefs } from './client' import { ModerationPrefs } from './moderation/types' +/** + * Supported proxy targets + */ +export type AtprotoServiceType = 'atproto_labeler' + /** * Used by the PersistSessionHandler to indicate what change occurred */ diff --git a/packages/api/tests/agent.test.ts b/packages/api/tests/agent.test.ts index 7bc1c1d1fb0..b4ad10802db 100644 --- a/packages/api/tests/agent.test.ts +++ b/packages/api/tests/agent.test.ts @@ -27,6 +27,14 @@ describe('agent', () => { await network.close() }) + it('clones correctly', () => { + const persistSession = (evt: AtpSessionEvent, sess?: AtpSessionData) => {} + const agent = new AtpAgent({ service: network.pds.url, persistSession }) + const agent2 = agent.clone() + expect(agent2 instanceof AtpAgent).toBeTruthy() + expect(agent.service).toEqual(agent2.service) + }) + it('creates a new session on account creation.', async () => { const events: string[] = [] const sessions: (AtpSessionData | undefined)[] = [] @@ -528,6 +536,31 @@ describe('agent', () => { await new Promise((r) => server.close(r)) }) }) + + describe('configureProxyHeader', () => { + it('adds the proxy header as expected', async () => { + const server = await createHeaderEchoServer(15992) + const agent = new AtpAgent({ service: 'http://localhost:15992' }) + + const res1 = await agent.com.atproto.server.describeServer() + expect(res1.data['atproto-proxy']).toBeFalsy() + + agent.configureProxyHeader('atproto_labeler', 'did:plc:test1') + const res2 = await agent.com.atproto.server.describeServer() + expect(res2.data['atproto-proxy']).toEqual( + 'did:plc:test1#atproto_labeler', + ) + + const res3 = await agent + .withProxy('atproto_labeler', 'did:plc:test2') + .com.atproto.server.describeServer() + expect(res3.data['atproto-proxy']).toEqual( + 'did:plc:test2#atproto_labeler', + ) + + await new Promise((r) => server.close(r)) + }) + }) }) const createPost = async (agent: AtpAgent) => { diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index edeed48fa89..bae98dfe65d 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -33,6 +33,13 @@ describe('agent', () => { } } + it('clones correctly', () => { + const agent = new BskyAgent({ service: network.pds.url }) + const agent2 = agent.clone() + expect(agent2 instanceof BskyAgent).toBeTruthy() + expect(agent.service).toEqual(agent2.service) + }) + it('upsertProfile correctly creates and updates profiles.', async () => { const agent = new BskyAgent({ service: network.pds.url })