diff --git a/nixos/modules/services/matrix/mjolnir.nix b/nixos/modules/services/matrix/mjolnir.nix index e00dece33cab..085f38e522e6 100644 --- a/nixos/modules/services/matrix/mjolnir.nix +++ b/nixos/modules/services/matrix/mjolnir.nix @@ -186,6 +186,10 @@ in } ]; + # This defaults to true in the application, + # which breaks older configs using pantalaimon or access tokens + services.mjolnir.settings.encryption.use = lib.mkDefault false; + services.pantalaimon-headless.instances."mjolnir" = lib.mkIf cfg.pantalaimon.enable { homeserver = cfg.homeserverUrl; diff --git a/pkgs/servers/mjolnir/001-disable-nsfwprotection.patch b/pkgs/servers/mjolnir/001-disable-nsfwprotection.patch new file mode 100644 index 000000000000..ceeaa90f6351 --- /dev/null +++ b/pkgs/servers/mjolnir/001-disable-nsfwprotection.patch @@ -0,0 +1,221 @@ +diff --git a/src/protections/NsfwProtection.ts b/src/protections/NsfwProtection.ts +deleted file mode 100644 +index 8b6f8fd..0000000 +--- a/src/protections/NsfwProtection.ts ++++ /dev/null +@@ -1,99 +0,0 @@ +-/* +-Copyright 2024 The Matrix.org Foundation C.I.C. +- +-Licensed under the Apache License, Version 2.0 (the "License"); +-you may not use this file except in compliance with the License. +-You may obtain a copy of the License at +- +- http://www.apache.org/licenses/LICENSE-2.0 +- +-Unless required by applicable law or agreed to in writing, software +-distributed under the License is distributed on an "AS IS" BASIS, +-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-See the License for the specific language governing permissions and +-limitations under the License. +-*/ +- +-import { Protection } from "./IProtection"; +-import { Mjolnir } from "../Mjolnir"; +-import * as nsfw from 'nsfwjs'; +-import {LogLevel} from "@vector-im/matrix-bot-sdk"; +-import { node } from '@tensorflow/tfjs-node'; +- +- +-export class NsfwProtection extends Protection { +- settings = {}; +- // @ts-ignore +- private model: any; +- +- constructor() { +- super(); +- } +- +- async initialize() { +- this.model = await nsfw.load(); +- } +- +- public get name(): string { +- return 'NsfwProtection'; +- } +- +- public get description(): string { +- return "Scans all images sent into a protected room to determine if the image is " + +- "NSFW. If it is, the image will automatically be redacted."; +- } +- +- public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise { +- if (event['type'] === 'm.room.message') { +- let content = JSON.stringify(event['content']); +- if (!content.toLowerCase().includes("mxc")) { +- return; +- } +- // try and grab a human-readable alias for more helpful management room output +- const maybeAlias = await mjolnir.client.getPublishedAlias(roomId) +- const room = maybeAlias ? maybeAlias : roomId +- +- const mxcs = content.match(/(mxc?:\/\/[^\s'"]+)/gim); +- if (!mxcs) { +- //something's gone wrong with the regex +- await mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "NSFWProtection", `Unable to find any mxcs in ${event["event_id"]} in ${room}`); +- return; +- } +- +- // @ts-ignore - see null check immediately above +- for (const mxc of mxcs) { +- const image = await mjolnir.client.downloadContent(mxc); +- const decodedImage = await node.decodeImage(image.data, 3); +- const predictions = await this.model.classify(decodedImage); +- +- +- for (const prediction of predictions) { +- if (["Hentai", "Porn"].includes(prediction["className"])) { +- if (prediction["probability"] > mjolnir.config.nsfwSensitivity) { +- try { +- await mjolnir.client.redactEvent(roomId, event["event_id"]); +- } catch (err) { +- await mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, "NSFWProtection", `There was an error redacting ${event["event_id"]} in ${room}: ${err}`); +- } +- let eventId = event["event_id"] +- let body = `Redacted an image in ${room} ${eventId}` +- let formatted_body = `
+- Redacted an image in ${room} +-
${eventId}
${room}
+-                                                  
` +- const msg = { +- msgtype: "m.notice", +- body: body, +- format: "org.matrix.custom.html", +- formatted_body: formatted_body +- }; +- await mjolnir.client.sendMessage(mjolnir.managementRoomId, msg); +- break +- } +- } +- } +- decodedImage.dispose(); +- } +- } +- } +-} +\ No newline at end of file +diff --git a/src/protections/ProtectionManager.ts b/src/protections/ProtectionManager.ts +index 9b84318..67f10dc 100644 +--- a/src/protections/ProtectionManager.ts ++++ b/src/protections/ProtectionManager.ts +@@ -31,7 +31,6 @@ import { htmlEscape } from "../utils"; + import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; + import { RoomUpdateError } from "../models/RoomUpdateError"; + import { LocalAbuseReports } from "./LocalAbuseReports"; +-import {NsfwProtection} from "./NsfwProtection"; + import { MentionSpam } from "./MentionSpam"; + + const PROTECTIONS: Protection[] = [ +@@ -44,7 +43,6 @@ const PROTECTIONS: Protection[] = [ + new DetectFederationLag(), + new JoinWaveShortCircuit(), + new LocalAbuseReports(), +- new NsfwProtection(), + new MentionSpam() + ]; + +@@ -104,9 +102,6 @@ export class ProtectionManager { + protection.settings[key].setValue(value); + } + if (protection.enabled) { +- if (protection.name === "NsfwProtection") { +- (protection as NsfwProtection).initialize(); +- } + for (let roomId of this.mjolnir.protectedRoomsTracker.getProtectedRooms()) { + await protection.startProtectingRoom(this.mjolnir, roomId); + } +diff --git a/test/integration/nsfwProtectionTest.ts b/test/integration/nsfwProtectionTest.ts +deleted file mode 100644 +index c86fd38..0000000 +--- a/test/integration/nsfwProtectionTest.ts ++++ /dev/null +@@ -1,78 +0,0 @@ +-import {newTestUser} from "./clientHelper"; +- +-import {MatrixClient} from "@vector-im/matrix-bot-sdk"; +-import {getFirstReaction} from "./commands/commandUtils"; +-import {strict as assert} from "assert"; +-import { readFileSync } from 'fs'; +- +-describe("Test: NSFW protection", function () { +- let client: MatrixClient; +- let room: string; +- this.beforeEach(async function () { +- client = await newTestUser(this.config.homeserverUrl, {name: {contains: "nsfw-protection"}}); +- await client.start(); +- const mjolnirId = await this.mjolnir.client.getUserId(); +- room = await client.createRoom({ invite: [mjolnirId] }); +- await client.joinRoom(room); +- await client.joinRoom(this.config.managementRoom); +- await client.setUserPowerLevel(mjolnirId, room, 100); +- }) +- this.afterEach(async function () { +- await client.stop(); +- }) +- +- function delay(ms: number) { +- return new Promise(resolve => setTimeout(resolve, ms)); +- } +- +- +- it("Nsfw protection doesn't redact sfw images", async function() { +- this.timeout(20000); +- +- await client.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${room}` }); +- await getFirstReaction(client, this.mjolnir.managementRoomId, '✅', async () => { +- return await client.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir enable NsfwProtection` }); +- }); +- +- const data = readFileSync('test_tree.jpg'); +- const mxc = await client.uploadContent(data, 'image/png'); +- let content = {"msgtype": "m.image", "body": "test.jpeg", "url": mxc}; +- let imageMessage = await client.sendMessage(room, content); +- +- await delay(500); +- let processedImage = await client.getEvent(room, imageMessage); +- assert.equal(Object.keys(processedImage.content).length, 3, "This event should not have been redacted"); +- }); +- +- it("Nsfw protection redacts nsfw images", async function() { +- this.timeout(20000); +- // dial the sensitivity on the protection way up so that all images are flagged as NSFW +- this.mjolnir.config.nsfwSensitivity = 0.0; +- +- await client.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${room}` }); +- await getFirstReaction(client, this.mjolnir.managementRoomId, '✅', async () => { +- return await client.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir enable NsfwProtection` }); +- }); +- +- const data = readFileSync('test_tree.jpg'); +- const mxc = await client.uploadContent(data, 'image/png'); +- let content = {"msgtype": "m.image", "body": "test.jpeg", "url": mxc}; +- let imageMessage = await client.sendMessage(room, content); +- +- let formatted_body = `` +- let htmlContent = { +- msgtype: "m.image", +- body: formatted_body, +- format: "org.matrix.custom.html", +- formatted_body: formatted_body +- }; +- let htmlMessage = await client.sendMessage(room, htmlContent) +- +- await delay(500); +- let processedImage = await client.getEvent(room, imageMessage); +- assert.equal(Object.keys(processedImage.content).length, 0, "This event should have been redacted"); +- +- let processedHtml = await client.getEvent(room, htmlMessage) +- assert.equal(Object.keys(processedHtml.content).length, 0, "This html image event should have been redacted") +- }); +-}); +\ No newline at end of file diff --git a/pkgs/servers/mjolnir/default.nix b/pkgs/servers/mjolnir/default.nix index 7cecfa14923c..dd1404b4b5b4 100644 --- a/pkgs/servers/mjolnir/default.nix +++ b/pkgs/servers/mjolnir/default.nix @@ -10,20 +10,25 @@ mkYarnPackage rec { pname = "mjolnir"; - version = "1.6.5"; + version = "1.8.3"; src = fetchFromGitHub { owner = "matrix-org"; repo = "mjolnir"; rev = "refs/tags/v${version}"; - hash = "sha256-xejFKz2MmdjMFU0X0SdI+qXTBRAwIvkcfZPQqXB9LV0="; + hash = "sha256-yD7QGsS2Em8Z95po9pGRUDmHgHe4z0j0Jnvy3IG7xKY="; }; + patches = [ + # TODO: Fix tfjs-node dependency + ./001-disable-nsfwprotection.patch + ]; + packageJSON = ./package.json; offlineCache = fetchYarnDeps { yarnLock = src + "/yarn.lock"; - hash = "sha256-RpvdyxJj92k4wFjBBmWCnEpFVOXVWlHEm0SmEBUlnTM="; + hash = "sha256-05DqddK8+136Qq/JGeiITZkVJ8Dw9K9HfACKW86989U="; }; packageResolutions = { diff --git a/pkgs/servers/mjolnir/package.json b/pkgs/servers/mjolnir/package.json index 4b24df828031..fa276e8569a2 100644 --- a/pkgs/servers/mjolnir/package.json +++ b/pkgs/servers/mjolnir/package.json @@ -1,6 +1,6 @@ { "name": "mjolnir", - "version": "1.6.5", + "version": "1.8.3", "description": "A moderation tool for Matrix", "main": "lib/index.js", "repository": "git@github.com:matrix-org/mjolnir.git", @@ -34,7 +34,7 @@ "@types/pg": "^8.6.5", "@types/request": "^2.48.8", "@types/shell-quote": "1.7.1", - "crypto-js": "^4.1.1", + "crypto-js": "^4.2.0", "eslint": "^7.32", "expect": "^27.0.6", "mocha": "^9.0.1", @@ -46,16 +46,20 @@ "dependencies": { "@sentry/node": "^7.17.2", "@sentry/tracing": "^7.17.2", + "@tensorflow/tfjs-node": "^4.21.0", + "@vector-im/matrix-bot-sdk": "^0.7.1-element.6", "await-lock": "^2.2.2", + "axios": "^1.7.6", "body-parser": "^1.20.1", "config": "^3.3.8", - "express": "^4.17", + "express": "^4.20", "html-to-text": "^8.0.0", "humanize-duration": "^3.27.1", "humanize-duration-ts": "^2.1.1", "js-yaml": "^4.1.0", "jsdom": "^16.6.0", - "matrix-appservice-bridge": "8.1.2", + "matrix-appservice-bridge": "10.3.1", + "nsfwjs": "^4.1.0", "parse-duration": "^1.0.2", "pg": "^8.8.0", "prom-client": "^14.1.0", @@ -64,6 +68,6 @@ "yaml": "^2.2.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 2a9e5eb64e4c..e55c14f60c72 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -9379,9 +9379,7 @@ with pkgs; mitm6 = callPackage ../tools/security/mitm6 { }; - mjolnir = callPackage ../servers/mjolnir { - matrix-sdk-crypto-nodejs = matrix-sdk-crypto-nodejs-0_1_0-beta_3; - }; + mjolnir = callPackage ../servers/mjolnir { }; mmutils = callPackage ../tools/X11/mmutils { };