diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index e4c15bb6940..118a735f731 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -37,6 +37,7 @@ const run = async () => { `🌞 Personal Data server started http://localhost:${network.pds.port}`, ) console.log(`🗼 Ozone server started http://localhost:${network.ozone.port}`) + console.log(`🗼 Ozone service DID ${network.ozone.ctx.cfg.service.did}`) console.log(`🌅 Bsky Appview started http://localhost:${network.bsky.port}`) for (const fg of network.feedGens) { console.log(`🤖 Feed Generator started http://localhost:${fg.port}`) diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index dbbd9915339..edb18d7ca39 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -350,328 +350,6 @@ export async function generateMockSetup(env: TestNetwork) { }, ) - // create the dev-env moderator - { - const res = await clients.loggedout.api.com.atproto.server.createAccount({ - email: 'mod-authority@test.com', - handle: 'mod-authority.test', - password: 'hunter2', - }) - const agent = env.pds.getClient() - agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) - await agent.api.app.bsky.actor.profile.create( - { repo: res.data.did }, - { - displayName: 'Dev-env Moderation', - description: `The pretend version of mod.bsky.app`, - }, - ) - - await agent.api.app.bsky.labeler.service.create( - { repo: res.data.did, rkey: 'self' }, - { - policies: { - labelValues: [ - '!hide', - '!warn', - 'porn', - 'sexual', - 'nudity', - 'sexual-figurative', - 'graphic-media', - 'self-harm', - 'sensitive', - 'extremist', - 'intolerant', - 'threat', - 'rude', - 'illicit', - 'security', - 'unsafe-link', - 'impersonation', - 'misinformation', - 'scam', - 'engagement-farming', - 'spam', - 'rumor', - 'misleading', - 'inauthentic', - ], - labelValueDefinitions: [ - { - identifier: 'spam', - blurs: 'content', - severity: 'inform', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Spam', - description: - 'Unwanted, repeated, or unrelated actions that bother users.', - }, - ], - }, - { - identifier: 'impersonation', - blurs: 'none', - severity: 'inform', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Impersonation', - description: - 'Pretending to be someone else without permission.', - }, - ], - }, - { - identifier: 'scam', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Scam', - description: 'Scams, phishing & fraud.', - }, - ], - }, - { - identifier: 'intolerant', - blurs: 'content', - severity: 'alert', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Intolerance', - description: 'Discrimination against protected groups.', - }, - ], - }, - { - identifier: 'self-harm', - blurs: 'content', - severity: 'alert', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Self-Harm', - description: - 'Promotes self-harm, including graphic images, glorifying discussions, or triggering stories.', - }, - ], - }, - { - identifier: 'security', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Security Concerns', - description: - 'May be unsafe and could harm your device, steal your info, or get your account hacked.', - }, - ], - }, - { - identifier: 'misleading', - blurs: 'content', - severity: 'alert', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Misleading', - description: - 'Altered images/videos, deceptive links, or false statements.', - }, - ], - }, - { - identifier: 'threat', - blurs: 'content', - severity: 'inform', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Threats', - description: - 'Promotes violence or harm towards others, including threats, incitement, or advocacy of harm.', - }, - ], - }, - { - identifier: 'unsafe-link', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Unsafe link', - description: - 'Links to harmful sites with malware, phishing, or violating content that risk security and privacy.', - }, - ], - }, - { - identifier: 'illicit', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Illicit', - description: - 'Promoting or selling potentially illicit goods, services, or activities.', - }, - ], - }, - { - identifier: 'misinformation', - blurs: 'content', - severity: 'inform', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Misinformation', - description: - 'Spreading false or misleading info, including unverified claims and harmful conspiracy theories.', - }, - ], - }, - { - identifier: 'rumor', - blurs: 'content', - severity: 'inform', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Rumor', - description: - 'Approach with caution, as these claims lack evidence from credible sources.', - }, - ], - }, - { - identifier: 'rude', - blurs: 'content', - severity: 'inform', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Rude', - description: - 'Rude or impolite, including crude language and disrespectful comments, without constructive purpose.', - }, - ], - }, - { - identifier: 'extremist', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Extremist', - description: - 'Radical views advocating violence, hate, or discrimination against individuals or groups.', - }, - ], - }, - { - identifier: 'sensitive', - blurs: 'content', - severity: 'alert', - defaultSetting: 'warn', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Sensitive', - description: - 'May be upsetting, covering topics like substance abuse or mental health issues, cautioning sensitive viewers.', - }, - ], - }, - { - identifier: 'engagement-farming', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Engagement Farming', - description: - 'Insincere content or bulk actions aimed at gaining followers, including frequent follows, posts, and likes.', - }, - ], - }, - { - identifier: 'inauthentic', - blurs: 'content', - severity: 'alert', - defaultSetting: 'hide', - adultOnly: false, - locales: [ - { - lang: 'en', - name: 'Inauthentic Account', - description: 'Bot or a person pretending to be someone else.', - }, - ], - }, - { - identifier: 'sexual-figurative', - blurs: 'media', - severity: 'none', - defaultSetting: 'show', - adultOnly: true, - locales: [ - { - lang: 'en', - name: 'Sexually Suggestive (Cartoon)', - description: - 'Art with explicit or suggestive sexual themes, including provocative imagery or partial nudity.', - }, - ], - }, - ], - }, - createdAt: date.next().value, - }, - ) - } - // create a labeler account { const res = await clients.loggedout.api.com.atproto.server.createAccount({ diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 11d18e24224..2568b73cc69 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -7,10 +7,10 @@ import { TestServerParams } from './types' import { TestPlc } from './plc' import { TestPds } from './pds' import { TestBsky } from './bsky' -import { TestOzone, createOzoneDid } from './ozone' +import { TestOzone } from './ozone' +import { OzoneServiceProfile } from './ozone-service-profile' import { mockNetworkUtilities } from './util' import { TestNetworkNoAppView } from './network-no-appview' -import { Secp256k1Keypair } from '@atproto/crypto' import { EXAMPLE_LABELER } from './const' const ADMIN_USERNAME = 'admin' @@ -42,8 +42,16 @@ export class TestNetwork extends TestNetworkNoAppView { const pdsPort = params.pds?.port ?? (await getPort()) const ozonePort = params.ozone?.port ?? (await getPort()) - const ozoneKey = await Secp256k1Keypair.create({ exportable: true }) - const ozoneDid = await createOzoneDid(plc.url, ozoneKey) + const thirdPartyPdsProps = { + didPlcUrl: plc.url, + ...params.pds, + inviteRequired: false, + port: await getPort(), + } + const thirdPartyPds = await TestPds.create(thirdPartyPdsProps) + const ozoneServiceProfile = new OzoneServiceProfile(thirdPartyPds) + const { did: ozoneDid, key: ozoneKey } = + await ozoneServiceProfile.createDidAndKey() const bsky = await TestBsky.create({ port: bskyPort, @@ -58,22 +66,25 @@ export class TestNetwork extends TestNetworkNoAppView { ...params.bsky, }) - const pds = await TestPds.create({ + const modServiceUrl = `http://localhost:${ozonePort}` + const pdsProps = { port: pdsPort, didPlcUrl: plc.url, bskyAppViewUrl: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, - modServiceUrl: `http://localhost:${ozonePort}`, + modServiceUrl, modServiceDid: ozoneDid, ...params.pds, - }) + } + + const pds = await TestPds.create(pdsProps) const ozone = await TestOzone.create({ port: ozonePort, plcUrl: plc.url, signingKey: ozoneKey, serverDid: ozoneDid, - dbPostgresSchema: `ozone_${dbPostgresSchema}`, + dbPostgresSchema: `ozone_${dbPostgresSchema || 'db'}`, dbPostgresUrl, appviewUrl: bsky.url, appviewDid: bsky.ctx.cfg.serverDid, @@ -83,7 +94,29 @@ export class TestNetwork extends TestNetworkNoAppView { ...params.ozone, }) + let inviteCode: string | undefined + if (pdsProps.inviteRequired) { + const { data: invite } = await pds + .getClient() + .api.com.atproto.server.createInviteCode( + { useCount: 1 }, + { + encoding: 'application/json', + headers: pds.adminAuthHeaders(), + }, + ) + inviteCode = invite.code + } + await ozoneServiceProfile.createServiceDetails(pds, modServiceUrl, { + inviteCode, + }) + + ozone.addAdminDid(ozoneDid) + mockNetworkUtilities(pds, bsky) + await pds.processAll() + await bsky.sub.background.processAll() + await thirdPartyPds.close() return new TestNetwork(plc, pds, bsky, ozone) } diff --git a/packages/dev-env/src/ozone-service-profile.ts b/packages/dev-env/src/ozone-service-profile.ts new file mode 100644 index 00000000000..325e95ee1ce --- /dev/null +++ b/packages/dev-env/src/ozone-service-profile.ts @@ -0,0 +1,420 @@ +import { TestPds } from './pds' +import { AtpAgent } from '@atproto/api' +import { Secp256k1Keypair } from '@atproto/crypto' + +export class OzoneServiceProfile { + did?: string + key?: Secp256k1Keypair + thirdPartyPdsClient: AtpAgent + + modUserDetails = { + email: 'mod-authority@test.com', + handle: 'mod-authority.test', + password: 'hunter2', + } + + public constructor(public thirdPartyPds: TestPds) { + this.thirdPartyPdsClient = this.thirdPartyPds.getClient() + } + + async createDidAndKey() { + const modUser = + await this.thirdPartyPdsClient.api.com.atproto.server.createAccount( + this.modUserDetails, + ) + await this.thirdPartyPdsClient.login({ + identifier: this.modUserDetails.handle, + password: this.modUserDetails.password, + }) + + this.did = modUser.data.did + this.key = await Secp256k1Keypair.create({ exportable: true }) + return { did: this.did, key: this.key } + } + + async createServiceDetails( + pds: TestPds, + ozoneUrl: string, + userDetails: { inviteCode?: string } = {}, + ) { + if (!this.did || !this.key) { + throw new Error('No DID/key found!') + } + const pdsClient = pds.getClient() + const describeRes = await pdsClient.api.com.atproto.server.describeServer() + const newServerDid = describeRes.data.did + + const serviceJwtRes = + await this.thirdPartyPdsClient.com.atproto.server.getServiceAuth({ + aud: newServerDid, + }) + const serviceJwt = serviceJwtRes.data.token + + const accountResponse = + await pdsClient.api.com.atproto.server.createAccount( + { + ...this.modUserDetails, + ...userDetails, + did: this.did, + }, + { + headers: { authorization: `Bearer ${serviceJwt}` }, + encoding: 'application/json', + }, + ) + + pdsClient.api.setHeader( + 'Authorization', + `Bearer ${accountResponse.data.accessJwt}`, + ) + + const getDidCredentials = + await pdsClient.com.atproto.identity.getRecommendedDidCredentials() + + await this.thirdPartyPdsClient.com.atproto.identity.requestPlcOperationSignature() + + const tokenRes = await this.thirdPartyPds.ctx.accountManager.db.db + .selectFrom('email_token') + .selectAll() + .where('did', '=', this.did) + .where('purpose', '=', 'plc_operation') + .executeTakeFirst() + const token = tokenRes?.token + const plcOperationData = { + token, + ...getDidCredentials.data, + } + + if (!plcOperationData.services) plcOperationData.services = {} + plcOperationData.services['atproto_labeler'] = { + type: 'AtprotoLabeler', + endpoint: ozoneUrl, + } + if (!plcOperationData.verificationMethods) + plcOperationData.verificationMethods = {} + plcOperationData.verificationMethods['atproto_label'] = this.key.did() + + const plcOp = + await this.thirdPartyPdsClient.com.atproto.identity.signPlcOperation( + plcOperationData, + ) + + await pdsClient.com.atproto.identity.submitPlcOperation({ + operation: plcOp.data.operation, + }) + + await pdsClient.api.com.atproto.server.activateAccount() + + await pdsClient.api.app.bsky.actor.profile.create( + { repo: this.did }, + { + displayName: 'Dev-env Moderation', + description: `The pretend version of mod.bsky.app`, + }, + ) + + await pdsClient.api.app.bsky.labeler.service.create( + { repo: this.did, rkey: 'self' }, + { + policies: { + labelValues: [ + '!hide', + '!warn', + 'porn', + 'sexual', + 'nudity', + 'sexual-figurative', + 'graphic-media', + 'self-harm', + 'sensitive', + 'extremist', + 'intolerant', + 'threat', + 'rude', + 'illicit', + 'security', + 'unsafe-link', + 'impersonation', + 'misinformation', + 'scam', + 'engagement-farming', + 'spam', + 'rumor', + 'misleading', + 'inauthentic', + ], + labelValueDefinitions: [ + { + identifier: 'spam', + blurs: 'content', + severity: 'inform', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Spam', + description: + 'Unwanted, repeated, or unrelated actions that bother users.', + }, + ], + }, + { + identifier: 'impersonation', + blurs: 'none', + severity: 'inform', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Impersonation', + description: + 'Pretending to be someone else without permission.', + }, + ], + }, + { + identifier: 'scam', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Scam', + description: 'Scams, phishing & fraud.', + }, + ], + }, + { + identifier: 'intolerant', + blurs: 'content', + severity: 'alert', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Intolerance', + description: 'Discrimination against protected groups.', + }, + ], + }, + { + identifier: 'self-harm', + blurs: 'content', + severity: 'alert', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Self-Harm', + description: + 'Promotes self-harm, including graphic images, glorifying discussions, or triggering stories.', + }, + ], + }, + { + identifier: 'security', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Security Concerns', + description: + 'May be unsafe and could harm your device, steal your info, or get your account hacked.', + }, + ], + }, + { + identifier: 'misleading', + blurs: 'content', + severity: 'alert', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Misleading', + description: + 'Altered images/videos, deceptive links, or false statements.', + }, + ], + }, + { + identifier: 'threat', + blurs: 'content', + severity: 'inform', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Threats', + description: + 'Promotes violence or harm towards others, including threats, incitement, or advocacy of harm.', + }, + ], + }, + { + identifier: 'unsafe-link', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Unsafe link', + description: + 'Links to harmful sites with malware, phishing, or violating content that risk security and privacy.', + }, + ], + }, + { + identifier: 'illicit', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Illicit', + description: + 'Promoting or selling potentially illicit goods, services, or activities.', + }, + ], + }, + { + identifier: 'misinformation', + blurs: 'content', + severity: 'inform', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Misinformation', + description: + 'Spreading false or misleading info, including unverified claims and harmful conspiracy theories.', + }, + ], + }, + { + identifier: 'rumor', + blurs: 'content', + severity: 'inform', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Rumor', + description: + 'Approach with caution, as these claims lack evidence from credible sources.', + }, + ], + }, + { + identifier: 'rude', + blurs: 'content', + severity: 'inform', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Rude', + description: + 'Rude or impolite, including crude language and disrespectful comments, without constructive purpose.', + }, + ], + }, + { + identifier: 'extremist', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Extremist', + description: + 'Radical views advocating violence, hate, or discrimination against individuals or groups.', + }, + ], + }, + { + identifier: 'sensitive', + blurs: 'content', + severity: 'alert', + defaultSetting: 'warn', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Sensitive', + description: + 'May be upsetting, covering topics like substance abuse or mental health issues, cautioning sensitive viewers.', + }, + ], + }, + { + identifier: 'engagement-farming', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Engagement Farming', + description: + 'Insincere content or bulk actions aimed at gaining followers, including frequent follows, posts, and likes.', + }, + ], + }, + { + identifier: 'inauthentic', + blurs: 'content', + severity: 'alert', + defaultSetting: 'hide', + adultOnly: false, + locales: [ + { + lang: 'en', + name: 'Inauthentic Account', + description: 'Bot or a person pretending to be someone else.', + }, + ], + }, + { + identifier: 'sexual-figurative', + blurs: 'media', + severity: 'none', + defaultSetting: 'show', + adultOnly: true, + locales: [ + { + lang: 'en', + name: 'Sexually Suggestive (Cartoon)', + description: + 'Art with explicit or suggestive sexual themes, including provocative imagery or partial nudity.', + }, + ], + }, + ], + }, + createdAt: new Date().toISOString(), + }, + ) + } +} diff --git a/packages/dev-env/src/ozone.ts b/packages/dev-env/src/ozone.ts index 435a8cf20a2..8054170f950 100644 --- a/packages/dev-env/src/ozone.ts +++ b/packages/dev-env/src/ozone.ts @@ -56,7 +56,7 @@ export class TestOzone { version: '0.0.0', port, didPlcUrl: config.plcUrl, - publicUrl: 'https://ozone.public.url', + publicUrl: url, serverDid, signingKeyHex, ...config, diff --git a/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap index db3f1a75ecd..796b3c6daca 100644 --- a/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap +++ b/packages/ozone/tests/__snapshots__/moderation-events.test.ts.snap @@ -92,6 +92,7 @@ Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(2)", + "creatorHandle": "mod-authority.test", "event": Object { "$type": "tools.ozone.moderation.defs#modEventTag", "add": Array [ @@ -153,6 +154,7 @@ Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(1)", + "creatorHandle": "mod-authority.test", "event": Object { "$type": "tools.ozone.moderation.defs#modEventTag", "add": Array [ diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index b19b7645406..2be2a8e50ee 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -64,6 +64,7 @@ Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(3)", + "creatorHandle": "mod-authority.test", "event": Object { "$type": "tools.ozone.moderation.defs#modEventTag", "add": Array [ @@ -166,6 +167,7 @@ Array [ Object { "createdAt": "1970-01-01T00:00:00.000Z", "createdBy": "user(0)", + "creatorHandle": "mod-authority.test", "event": Object { "$type": "tools.ozone.moderation.defs#modEventTag", "add": Array [ diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index f95c5193eb0..637f2961f55 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -203,6 +203,21 @@ Array [ "muted": false, }, }, + Object { + "associated": Object { + "labeler": true, + }, + "description": "The pretend version of mod.bsky.app", + "did": "user(6)", + "displayName": "Dev-env Moderation", + "handle": "mod-authority.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, ] `; @@ -275,6 +290,19 @@ Array [ "muted": false, }, }, + Object { + "associated": Object { + "labeler": true, + }, + "did": "user(6)", + "displayName": "Dev-env Moderation", + "handle": "mod-authority.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, ] `;