From 31f8943a3ccf2ce7cdda7a511af1f0abcc6e6070 Mon Sep 17 00:00:00 2001 From: Daniel Cadenas Date: Wed, 27 Mar 2024 14:00:33 -0300 Subject: [PATCH] Refactor and use same schema as reportinator server --- index.js | 31 ++++------- package.json | 4 +- pnpm-lock.yaml | 29 ++++------ src/lib/reportRequest.js | 27 ++++++++++ src/lib/slack.js | 9 +++- test/moderationFunction.test.js | 94 ++++++++++++++++----------------- 6 files changed, 101 insertions(+), 93 deletions(-) create mode 100644 src/lib/reportRequest.js diff --git a/index.js b/index.js index 6cfa83b..4242f53 100644 --- a/index.js +++ b/index.js @@ -6,14 +6,16 @@ import RateLimiting from "./src/lib/rateLimiting.js"; import Nostr, { REPORT_KIND } from "./src/lib/nostr.js"; import Slack from "./src/lib/slack.js"; import DuplicationHandling from "./src/lib/duplicationHandling.js"; +import ReportRequest from "./src/lib/reportRequest.js"; functions.cloudEvent("nostrEventsPubSub", async (cloudEvent) => { //The nostr event can either directly be the object or be encapsulated within - //a nostrEvent key, if present. A nostrEvent key indicates a user-initiated - //manual report request originating from the reportinator server. These events - //require Slack-based verification, except when they get auto-flagged." - const [nostrEventJson, userReportRequest] = getJSONFromCloudEvent(cloudEvent); - const nostrEvent = Nostr.getVerifiedEvent(nostrEventJson); + //a reportedEvent key, if present. A reportedEvent key indicates a + //user-initiated manual report request originating from the reportinator + //server. These events require Slack-based verification, except when they get + //auto-flagged. They also include a test and a pubkey of the reporter user. + const reportRequest = ReportRequest.fromCloudEvent(cloudEvent); + const nostrEvent = Nostr.getVerifiedEvent(reportRequest.reportedEvent); if (!nostrEvent) { return; @@ -41,15 +43,12 @@ functions.cloudEvent("nostrEventsPubSub", async (cloudEvent) => { ); if (!moderation) { - if (!userReportRequest) { + if (!reportRequest.reporterPubkey) { console.log(skipMessage); return; } - await Slack.postManualVerification(nostrEventJson, userReportRequest); - console.log( - `Event ${eventToModerate.id} reported by ${userReportRequest.pubkey} not flagged. Sending to Slack` - ); + await Slack.postManualVerification(reportRequest); return; } @@ -58,15 +57,3 @@ functions.cloudEvent("nostrEventsPubSub", async (cloudEvent) => { ); }); }); - -function getJSONFromCloudEvent(cloudEvent) { - const data = cloudEvent.data.message.data; - const jsonString = data ? Buffer.from(data, "base64").toString() : "{}"; - const json = JSON.parse(jsonString); - - if (json?.userReportRequest) { - return [json?.nostrEvent, json.userReportRequest]; - } - - return [json, null]; -} diff --git a/package.json b/package.json index 72791d0..6d8c59f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "node": "~20" }, "scripts": { - "test": "c8 --reporter=lcov --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 mocha --timeout=5000 --exit", + "test": "c8 --reporter=lcov --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 mocha --timeout=10000 --exit", "test:integration": "c8 mocha -j 2 test/integration.test.js --timeout=5000", "test:grep": "c8 mocha -j 2 --timeout=5000 --grep", "test:rateLimit": "node runArtilleryTest.mjs", @@ -33,7 +33,7 @@ "c8": "^8.0.0", "chai": "^4.3.10", "chai-as-promised": "^7.1.1", - "mocha": "^10.0.0", + "mocha": "^10.4.0", "sinon": "^15.0.0", "supertest": "^6.0.0", "uuid": "^9.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc41af0..1476d65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ devDependencies: specifier: ^7.1.1 version: 7.1.1(chai@4.3.10) mocha: - specifier: ^10.0.0 - version: 10.2.0 + specifier: ^10.4.0 + version: 10.4.0 sinon: specifier: ^15.0.0 version: 15.2.0 @@ -1220,8 +1220,8 @@ packages: is-glob: 4.0.3 dev: true - /glob@7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1231,15 +1231,15 @@ packages: path-is-absolute: 1.0.1 dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 5.0.1 once: 1.4.0 - path-is-absolute: 1.0.1 dev: true /google-auth-library@9.1.0: @@ -1683,8 +1683,8 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false - /mocha@10.2.0: - resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==} + /mocha@10.4.0: + resolution: {integrity: sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==} engines: {node: '>= 14.0.0'} hasBin: true dependencies: @@ -1695,13 +1695,12 @@ packages: diff: 5.0.0 escape-string-regexp: 4.0.0 find-up: 5.0.0 - glob: 7.2.0 + glob: 8.1.0 he: 1.2.0 js-yaml: 4.1.0 log-symbols: 4.1.0 minimatch: 5.0.1 ms: 2.1.3 - nanoid: 3.3.3 serialize-javascript: 6.0.0 strip-json-comments: 3.1.1 supports-color: 8.1.1 @@ -1721,12 +1720,6 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /nanoid@3.3.3: - resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} diff --git a/src/lib/reportRequest.js b/src/lib/reportRequest.js new file mode 100644 index 0000000..ac41ab0 --- /dev/null +++ b/src/lib/reportRequest.js @@ -0,0 +1,27 @@ +export default class ReportRequest { + constructor(reportedEvent, reporterPubkey, reporterText) { + this.reportedEvent = reportedEvent; + this.reporterPubkey = reporterPubkey; + this.reporterText = reporterText; + } + + static fromCloudEvent(cloudEvent) { + const data = cloudEvent.data.message.data; + const jsonString = data ? Buffer.from(data, "base64").toString() : "{}"; + const json = JSON.parse(jsonString); + + if (json?.reportedEvent) { + return new ReportRequest( + json.reportedEvent, + json?.reporterPubkey, + json?.reporterText + ); + } + + return new ReportRequest(json, null, null); + } + + canBeManualVerified() { + return Boolean(this.reporterPubkey); + } +} diff --git a/src/lib/slack.js b/src/lib/slack.js index c36d26b..20a09da 100644 --- a/src/lib/slack.js +++ b/src/lib/slack.js @@ -1,5 +1,10 @@ export default class Slack { - static async postManualVerification(nostrEventJson, userReportRequest) { - console.log(`userReportRequest: ${userReportRequest}`); + static async postManualVerification(reportRequest) { + console.log( + `TODO: Implement Slack.postManualVerification.\n + Event payload: ${JSON.stringify(reportRequest.reportedEvent)}\n + Reporter pubkey: ${reportRequest.reporterPubkey}\n + Reporter text: ${reportRequest.reporterText}` + ); } } diff --git a/test/moderationFunction.test.js b/test/moderationFunction.test.js index 949ae2d..e0b0bab 100644 --- a/test/moderationFunction.test.js +++ b/test/moderationFunction.test.js @@ -54,8 +54,8 @@ const reportNostrEvent = { sig: "df77b254f086ba6065ee2ff828601c84836bf6df13a59e0dc49e01b828e3da08cd184c18c26e85736f17ff39241eef894ceae25de1402b2c5ff5432ec656908d", }; -describe("Moderation Cloud Function", () => { - beforeEach(async function () { +describe("Moderation Cloud Function", async () => { + beforeEach(async () => { sinon.spy(console, "error"); sinon.spy(console, "log"); sinon.stub(Nostr, "publishNostrEvent").returns(Promise.resolve()); @@ -64,7 +64,7 @@ describe("Moderation Cloud Function", () => { sinon.stub(Datastore.prototype, "save").resolves(); }); - afterEach(function () { + afterEach(async () => { sinon.restore(); }); @@ -114,8 +114,8 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishModeration.notCalled); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(Nostr.publishNostrEvent); + sinon.assert.notCalled(Slack.postManualVerification); }); it("should send to Slack a valid event that is not flagged sent from the reportinator server", async () => { @@ -158,8 +158,9 @@ describe("Moderation Cloud Function", () => { message: { data: Buffer.from( JSON.stringify({ - nostrEvent, - userReportRequest: {}, + reportedEvent: nostrEvent, + reporterPubkey: + "npub1a8ekuuuwdsrnq68s0vv9rdqxletn2j2s0hwrctqq0wggac3mh4fqth5p88", }) ).toString("base64"), }, @@ -169,8 +170,8 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishModeration.notCalled); - assert.ok(Slack.postManualVerification.called); + sinon.assert.notCalled(Nostr.publishNostrEvent); + sinon.assert.called(Slack.postManualVerification); }); it("should publish a report event for a valid event that is flagged coming from reportinator server", async () => { @@ -179,8 +180,7 @@ describe("Moderation Cloud Function", () => { message: { data: Buffer.from( JSON.stringify({ - nostrEvent: flaggedNostrEvent, - userReportRequest: {}, + reportedEvent: flaggedNostrEvent, }) ).toString("base64"), }, @@ -226,7 +226,6 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishNostrEvent.called); sinon.assert.calledWithMatch(Nostr.publishNostrEvent, { kind: 1984, tags: [ @@ -238,7 +237,7 @@ describe("Moderation Cloud Function", () => { }); sinon.assert.notCalled(waitMillisStub); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(Slack.postManualVerification); }); it("should publish a report event for a valid event that is flagged", async () => { @@ -291,7 +290,7 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishNostrEvent.called); + sinon.assert.calledOnce(Nostr.publishNostrEvent); sinon.assert.calledWithMatch(Nostr.publishNostrEvent, { kind: 1984, tags: [ @@ -301,9 +300,9 @@ describe("Moderation Cloud Function", () => { ["l", "MOD>IL-har", "MOD", sinon.match.string], ], }); - sinon.assert.notCalled(waitMillisStub); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(waitMillisStub); + sinon.assert.notCalled(Slack.postManualVerification); }); it("should publish a reporting event for a valid report event that is flagged coming from reportinator server", async () => { @@ -312,8 +311,7 @@ describe("Moderation Cloud Function", () => { message: { data: Buffer.from( JSON.stringify({ - nostrEvent: reportNostrEvent, - userReportRequest: {}, + reportedEvent: reportNostrEvent, }) ).toString("base64"), }, @@ -359,7 +357,6 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishNostrEvent.called); sinon.assert.calledWithMatch(Nostr.publishNostrEvent, { kind: 1984, tags: [ @@ -370,10 +367,10 @@ describe("Moderation Cloud Function", () => { ], }); sinon.assert.notCalled(waitMillisStub); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(Slack.postManualVerification); }); - it("should publish a reporting event for a valid report event that is flagged coming from reportinator server", async () => { + it("should publish a reporting event for a valid report event that is flagged", async () => { const cloudEvent = { data: { message: { @@ -423,7 +420,6 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(Nostr.publishNostrEvent.called); sinon.assert.calledWithMatch(Nostr.publishNostrEvent, { kind: 1984, tags: [ @@ -434,7 +430,7 @@ describe("Moderation Cloud Function", () => { ], }); sinon.assert.notCalled(waitMillisStub); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(Slack.postManualVerification); }); it("should detect and invalid event", async () => { @@ -453,9 +449,9 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(console.error.calledWith("Invalid Nostr Event")); - assert.ok(Nostr.publishNostrEvent.notCalled); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.calledWith(console.error, "Invalid Nostr Event"); + sinon.assert.notCalled(Nostr.publishNostrEvent); + sinon.assert.notCalled(Slack.postManualVerification); }); it("should detect and invalid event coming from reportinator server", async () => { @@ -477,29 +473,29 @@ describe("Moderation Cloud Function", () => { const nostrEventsPubSub = getFunction("nostrEventsPubSub"); await nostrEventsPubSub(cloudEvent); - assert.ok(console.error.calledWith("Invalid Nostr Event")); - assert.ok(Nostr.publishNostrEvent.notCalled); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.calledWith(console.error, "Invalid Nostr Event"); + sinon.assert.notCalled(Nostr.publishNostrEvent); + sinon.assert.notCalled(Slack.postManualVerification); }); - xit("should detect and invalid signature", async () => { - const nEvent = { ...nostrEvent }; - nEvent.id = - "1111c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65"; - const cloudEvent = { - data: { - message: { - data: Buffer.from(JSON.stringify(nEvent)).toString("base64"), - }, - }, - }; - - const nostrEventsPubSub = getFunction("nostrEventsPubSub"); - await nostrEventsPubSub(cloudEvent); - - assert.ok(console.error.calledWith("Invalid Nostr Event Signature")); - assert.ok(Nostr.publishNostrEvent.notCalled); - }); + // xit("should detect and invalid signature", async () => { + // const nEvent = { ...nostrEvent }; + // nEvent.id = + // "1111c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65"; + // const cloudEvent = { + // data: { + // message: { + // data: Buffer.from(JSON.stringify(nEvent)).toString("base64"), + // }, + // }, + // }; + + // const nostrEventsPubSub = getFunction("nostrEventsPubSub"); + // await nostrEventsPubSub(cloudEvent); + + // assert.ok(console.error.calledWith("Invalid Nostr Event Signature")); + // assert.ok(Nostr.publishNostrEvent.notCalled); + // }); it("should add jitter pause after a rate limit error", async () => { const nEvent = { ...nostrEvent }; @@ -528,8 +524,8 @@ describe("Moderation Cloud Function", () => { ); }); - assert.ok(Nostr.publishNostrEvent.notCalled); - assert.ok(Slack.postManualVerification.notCalled); + sinon.assert.notCalled(Nostr.publishNostrEvent); + sinon.assert.notCalled(Slack.postManualVerification); sinon.assert.calledWithMatch( waitMillisStub,