mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-06-04 06:41:18 +00:00
Merge commit 'efe842cc9f6fc67185702169933ecf7408bcb2e8' into MTRNord/draupnir4all
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
development-dependencies:
|
||||
dependency-type: "development"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Unshallow for git describe so we can create version.txt
|
||||
run: git fetch --prune --unshallow --tags --all --force
|
||||
- name: Prepare version file
|
||||
@@ -29,10 +29,10 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.6.0
|
||||
uses: docker/setup-buildx-action@v2.10.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release tag
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Unshallow for git describe so we can create version.txt
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v2.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.6.0
|
||||
uses: docker/setup-buildx-action@v2.10.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
name: Build & Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Specifically use node 18 like in the readme.
|
||||
uses: actions/setup-node@v3
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Specifically use node 18 like in the readme.
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -40,12 +40,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Fetch and build mx-tester (cached across runs)
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
uses: baptiste0928/cargo-install@v2
|
||||
with:
|
||||
crate: mx-tester
|
||||
version: "0.3.3"
|
||||
@@ -62,12 +62,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Fetch and build mx-tester (cached across runs)
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
uses: baptiste0928/cargo-install@v2
|
||||
with:
|
||||
crate: mx-tester
|
||||
version: "0.3.3"
|
||||
|
||||
+63
-6
@@ -4,13 +4,15 @@ Draupnir is licensed under the Cooperative Software License.
|
||||
The reason for the license is simply because this project
|
||||
was something that I was previously employed to work on.
|
||||
|
||||
I have not decided whether I will accept contributions under the same license
|
||||
or another license or a CLA yet.
|
||||
I will welcome advice, but I will not welcome attempts to parsuade me to re-adopt
|
||||
and relicense under Apache-2.0 unless being offered compensation.
|
||||
I have not decided whether I will accept contributions under the
|
||||
same license or another license or a CLA yet.
|
||||
I will welcome advice, but I will not welcome attempts to parsuade
|
||||
me to re-adopt and relicense under Apache-2.0 unless being offered
|
||||
compensation.
|
||||
|
||||
The easiest way forwards would be for me to accept individual contributions under
|
||||
Apache-2.0.
|
||||
As of now, I am accepting contributions under the Apache-2.0 license
|
||||
in the same way as Mjolnir. This allows me the option to relicense
|
||||
Draupnir under Apache-2.0 without needing to chase up all contributors.
|
||||
|
||||
## How to contribute
|
||||
|
||||
@@ -185,3 +187,58 @@ matrix together all the fragmented communication technologies out there we are
|
||||
reliant on contributions and collaboration from the community to do so. So
|
||||
please get involved - and we hope you have as much fun hacking on Matrix as we
|
||||
do!
|
||||
|
||||
## Further notes on license and its relation to business in general
|
||||
|
||||
Ultimately most open source software contributions start by gifting
|
||||
labour without any obligation or transaction.
|
||||
|
||||
There is no ethical way to directly sell this labour.
|
||||
|
||||
Many so called post open source[^post-open-source] ideas fixate on
|
||||
finding a way to conduct business in an ethical way,
|
||||
and this is problematic.
|
||||
|
||||
Once you start working within capitalism with capitalism, and exchange
|
||||
your power and influence over a work to monitize the work itself,
|
||||
the work will gain inertia and a power of its own that you cannot control.
|
||||
You will work for the work, for external interests, and these won't
|
||||
be the interests of your powerless users who you were among to begin with.
|
||||
|
||||
It would be extreme, but I am tempted to suggest that by performing a
|
||||
buisness this way, you are part of an effort
|
||||
which not only reinforces capitalism but works to make it more
|
||||
efficient. Effectively working to make capitalism more powerful.
|
||||
Congratulations.
|
||||
|
||||
Another point that is often brought up in these discussions is how
|
||||
software licensing relies on an appeal to state power, the power of
|
||||
the law.
|
||||
|
||||
Therefore I propose a new licensing model, one which appeals
|
||||
to the power of public pressure rather than the law.
|
||||
|
||||
Such a license would be liberal, allowing incorperation into
|
||||
proprietary works provided it retained a notice.
|
||||
However, any work which is used in any way to conduct business must
|
||||
report all software being used by the buisness with this license,
|
||||
all turnover made by the buisness, all profit made by the buisness
|
||||
and an estimation of both profit and turnover made by the buisness in
|
||||
relation to the collection of software reported.
|
||||
|
||||
It is not clear to me how often these figures should be reported
|
||||
and when, or even where they should be reported to (ideally they could
|
||||
be found centrally). It is also unclear how to create the legalise
|
||||
required.
|
||||
|
||||
With the information these licenses would provide, public pressure
|
||||
could then be used to demand reperations for the profits made by
|
||||
pillaging and destructive businesses.
|
||||
It is not clear yet how any reperations would be distributed,
|
||||
probably through some system of
|
||||
[venture communes](https://wiki.p2pfoundation.net/Venture_Commune).
|
||||
The idea is to ensure that the developers and users of projects
|
||||
would not be distracted from providing each other mutual
|
||||
support and to give them a hope of escaping.
|
||||
|
||||
[^post-open-source] https://applied-langua.ge/posts/the-poverty-of-post-open-source.html.
|
||||
|
||||
@@ -5,34 +5,59 @@ for more information.
|
||||
|
||||
> I offer you the ring, which was burned, laid upon the pyre of Baldr by Odin.
|
||||
|
||||
This is a hard fork of [Mjolnir](https://github.com/matrix-org/mjolnir),
|
||||
This is a continuation and fork of [Mjolnir](https://github.com/matrix-org/mjolnir),
|
||||
with an entirely new framework for interacting with Matrix written to
|
||||
overcome some of the burdens there were holding development of mjolnir.
|
||||
|
||||
## Status
|
||||
|
||||
The command handler is currently being refactored and the syntax will become
|
||||
incompatible with legacy Mjolnir commands. The UX will be overhauled
|
||||
as such and we will consider the launch of Draupnir a 2.0.0 release.
|
||||
The command handler is currently being refactored and some command syntax will become
|
||||
incompatible with legacy Mjolnir commands.
|
||||
The UX is being overhauled and Draupnir is slowly moving towards a 2.0.0 release.
|
||||
|
||||
As Draupnir heads towards `v2.0.0` releases will appear [here](https://github.com/Gnuxie/Draupnir/releases).
|
||||
Until `v2.0.0` there will be frequent changes to commands.
|
||||
As Draupnir heads towards `v2.0.0`, releases will appear [here](https://github.com/Gnuxie/Draupnir/releases).
|
||||
Until `v2.0.0` there will be frequent changes to commands but all of these
|
||||
will be noted in the changes for that release.
|
||||
|
||||
Migrating from Mjolnir is straightforward, and Draupnir remains backwards compatible.
|
||||
So it is possible to try Draupnir and still have the option to switch back to Mjolnir.
|
||||
### Migration
|
||||
|
||||
Migrating from Mjolnir is straightforward and requires no manual steps,
|
||||
migration for your setup is likely as simple as changing your server config to
|
||||
pull the latest Draupnir docker image instead of a mjolnir one.
|
||||
Draupnir remains backwards compatible so that it is possible to try Draupnir
|
||||
and still have the option to switch back to Mjolnir.
|
||||
|
||||
Any problems with migration should be reported to our [support room](https://matrix.to/#/#draupnir:matrix.org).
|
||||
|
||||
## Features
|
||||
|
||||
As an all-in-one moderation tool, it can protect your server from malicious invites, spam
|
||||
messages, and whatever else you don't want. In addition to server-level protection, Draupnir
|
||||
is great for communities wanting to protect their rooms without having to use their personal
|
||||
accounts for moderation.
|
||||
As an all-in-one moderation tool, Draupnir can protect your community by
|
||||
applying policies from both your own and community curated policy lists.
|
||||
|
||||
The bot by default includes support for bans, redactions, anti-spam, server ACLs, room
|
||||
directory changes, room alias transfers, account deactivation, room shutdown, and more.
|
||||
directory changes and room alias transfers.
|
||||
|
||||
A Synapse module is also available to apply the same rulesets the bot uses across an entire
|
||||
homeserver.
|
||||
Support is also provided for some Synapse admin functions such as account
|
||||
deactivation and room shutdown.
|
||||
|
||||
A Synapse module is also available to apply the same rulesets the bot is watching
|
||||
across an entire homeserver.
|
||||
|
||||
### Differences from Mjolnir
|
||||
|
||||
The main difference from Mjolnir is that it is no longer necessary to use
|
||||
commands for some functions. Banning a user in a protected room from your
|
||||
Matrix client will cause Draupnir to show a prompt in the management room,
|
||||
which will offer to add the ban to a policy list.
|
||||
|
||||
If you do still wish to use the ban command, please note that users
|
||||
and other entities that are being banned are now the first argument
|
||||
to the ban command. It is now also possible to provide only the entity to
|
||||
Draupnir and have Draupnir prompt you for the policy list and the ban reason.
|
||||
|
||||
In general, any command that has been migrated to the new interface will
|
||||
feature better error messages for common problems and allow admins
|
||||
to trace the cause of unexpected errors much more easily.
|
||||
|
||||
## Setting up
|
||||
|
||||
@@ -88,7 +113,12 @@ server can only receive requests from your reverse proxy (e.g. `localhost`).
|
||||
|
||||
## Development
|
||||
|
||||
TODO. It's a TypeScript project with a linter.
|
||||
Draupnir is a TypeScript project that depends on the labour of a handful of
|
||||
developers, testers and users. The code base is in relatively good shape,
|
||||
and if you would like to contribute or gain an understanding of the workings
|
||||
of Draupnir, please read our [context document](./docs/context.md).
|
||||
|
||||
Once you have done that, go ahead and read our [contributing document](./CONTRIBUTING.md)
|
||||
|
||||
### Development and testing with mx-tester
|
||||
|
||||
|
||||
+7
-1
@@ -51,9 +51,11 @@ recordIgnoredInvites: false
|
||||
# (see verboseLogging to adjust this a bit.)
|
||||
managementRoom: "#moderators:example.org"
|
||||
|
||||
# Deprecated and will be removed in a future version.
|
||||
# Running with verboseLogging is unsupported.
|
||||
# Whether Draupnir should log a lot more messages in the room,
|
||||
# mainly involves "all-OK" messages, and debugging messages for when draupnir checks bans in a room.
|
||||
verboseLogging: true
|
||||
verboseLogging: false
|
||||
|
||||
# The log level of terminal (or container) output,
|
||||
# can be one of DEBUG, INFO, WARN and ERROR, in increasing order of importance and severity.
|
||||
@@ -73,6 +75,10 @@ verifyPermissionsOnStartup: true
|
||||
# turn on to trial some untrusted configuration or lists.
|
||||
noop: false
|
||||
|
||||
# Whether or not Draupnir should apply `m.room.server_acl` events.
|
||||
# DO NOT change this to `true` unless you are very confident that you know what you are doing.
|
||||
disableServerACL: false
|
||||
|
||||
# Whether Draupnir should check member lists quicker (by using a different endpoint),
|
||||
# keep in mind that enabling this will miss invited (but not joined) users.
|
||||
#
|
||||
|
||||
+6
-2
@@ -47,8 +47,12 @@ recordIgnoredInvites: false
|
||||
# Note: Mjolnir is fairly verbose - expect a lot of messages from it.
|
||||
managementRoom: "#moderators:localhost:9999"
|
||||
|
||||
# Set to false to make the management room a bit quieter.
|
||||
verboseLogging: true
|
||||
# Deprecated and will be removed in a future version.
|
||||
# Running with verboseLogging is unsupported.
|
||||
# Whether Draupnir should log a lot more messages in the room,
|
||||
# mainly involves "all-OK" messages, and debugging messages for when draupnir checks bans in a room.
|
||||
verboseLogging: false
|
||||
|
||||
|
||||
# The log level for the logs themselves. One of DEBUG, INFO, WARN, and ERROR.
|
||||
# This should be at INFO or DEBUG in order to get support for Mjolnir problems.
|
||||
|
||||
+31
-3
@@ -1,9 +1,9 @@
|
||||
## Context for developing Draupnir
|
||||
|
||||
#### And also context that is essential if you are developing anything
|
||||
that uses Policy Lists.
|
||||
alternatively context that is essential for developing
|
||||
anything that uses Policy Lists.
|
||||
|
||||
### Sync loop
|
||||
### The synchronisation loop
|
||||
|
||||
In order to understand how Draupnir works you have to first understand
|
||||
the sync loop of Matrix Clients. All Matrix clients have a sync loop.
|
||||
@@ -122,6 +122,34 @@ begin synchronising policies with with the protected rooms.
|
||||
Draupnir starts synchronising rooms by visiting the most recently
|
||||
active room first.
|
||||
|
||||
### A history of moderation projects
|
||||
|
||||
Mjolnir was originally created by
|
||||
[Travis Ralston](https://github.com/turt2live) as a good enough
|
||||
solution temprarily made permanent.
|
||||
The abstract architecture of Mjolnir remains today and we are
|
||||
thankful for good foundations, and significantly
|
||||
[policies](https://spec.matrix.org/latest/client-server-api/#moderation-policy-lists)
|
||||
that were
|
||||
[proposed](https://github.com/matrix-org/matrix-spec-proposals/pull/2313)
|
||||
by [Matthew Hodgson](https://github.com/ara4n).
|
||||
|
||||
There were several other similar solutions known to us that were
|
||||
developed and deployed at the same time as Mjolnir in the earlier days
|
||||
and either directly or indirectly had influence on things to come.
|
||||
Notably [Fly Swatter](https://github.com/serra-allgood/matrix-fly-swatter)
|
||||
and [Luna](https://gitlab.com/Gnuxie/luna).
|
||||
|
||||
After a period of maintenance, Mjolnir was then developed by other
|
||||
contributors from Element who restructured the project, tackled
|
||||
usability concerns and would go on to produce a multi-tenancy
|
||||
appservice mode of deployment called "Mjolnir for all".
|
||||
With the eventual aim of integrating the functions of Mjolnir
|
||||
transparently with both homeservers and clients.
|
||||
|
||||
This effort is now continued by the Matrix community in the form
|
||||
of Draupnir and [MTRNord](https://github.com/MTRNord)'s
|
||||
[Draupnir4all deployment](https://docs.draupnir.midnightthoughts.space/).
|
||||
|
||||
[^full-state]: matrix-bot-sdk could be modified to sync with
|
||||
`full_state` set to true. This has been
|
||||
|
||||
+15
-15
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "draupnir",
|
||||
"version": "1.84.0",
|
||||
"version": "1.85.1",
|
||||
"description": "A moderation tool for Matrix",
|
||||
"main": "lib/index.js",
|
||||
"repository": "https://github.com/MTRNord/Draupnir.git",
|
||||
@@ -15,34 +15,34 @@
|
||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||
"start:dev": "yarn build && node --async-stack-traces lib/index.js",
|
||||
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts",
|
||||
"test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
|
||||
"test:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --forbid-only --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
|
||||
"test:integration:single": "NODE_ENV=harness npx ts-mocha --require test/integration/fixtures.ts --timeout 300000 --project ./tsconfig.json",
|
||||
"test:appservice:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --timeout 300000 --project ./tsconfig.json \"test/appservice/integration/**/*Test.ts\"",
|
||||
"test:appservice:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --forbid-only --timeout 300000 --project ./tsconfig.json \"test/appservice/integration/**/*Test.ts\"",
|
||||
"test:appservice:integration:single": "NODE_ENV=harness npx ts-mocha --timeout 300000 --project ./tsconfig.json",
|
||||
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts",
|
||||
"version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][^\"]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/config": "^3.3.0",
|
||||
"@types/crypto-js": "^4.0.2",
|
||||
"@types/config": "^3.3.1",
|
||||
"@types/crypto-js": "^4.1.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/html-to-text": "^8.0.1",
|
||||
"@types/humanize-duration": "^3.27.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsdom": "^16.2.11",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/nedb": "^1.8.12",
|
||||
"@types/node": "^18.16.7",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/nedb": "^1.8.13",
|
||||
"@types/node": "^20.6.0",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/request": "^2.48.8",
|
||||
"@types/shell-quote": "1.7.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"eslint": "^7.32",
|
||||
"expect": "^27.0.6",
|
||||
"mocha": "^9.0.1",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"eslint": "^8.49",
|
||||
"expect": "^29.6.4",
|
||||
"mocha": "^10.2.0",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript-formatter": "^7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -57,7 +57,7 @@
|
||||
"@sentry/tracing": "^7.17.2",
|
||||
"await-lock": "^2.2.2",
|
||||
"body-parser": "^1.20.1",
|
||||
"config": "^3.3.8",
|
||||
"config": "^3.3.9",
|
||||
"express": "^4.17",
|
||||
"html-to-text": "^8.0.0",
|
||||
"humanize-duration": "^3.27.1",
|
||||
@@ -69,7 +69,7 @@
|
||||
"pg": "^8.8.0",
|
||||
"shell-quote": "^1.7.3",
|
||||
"ulidx": "^0.3.0",
|
||||
"yaml": "^2.2.2"
|
||||
"yaml": "^2.3.2"
|
||||
},
|
||||
"overrides": {
|
||||
"matrix-appservice-bridge": {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
* This modified file incorperates work from mjolnir
|
||||
* https://github.com/matrix-org/mjolnir
|
||||
* which included the following license notice:
|
||||
|
||||
Copyright 2019 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.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
import { trace } from "./utils";
|
||||
|
||||
export const ERROR_KIND_PERMISSION = "permission";
|
||||
export const ERROR_KIND_FATAL = "fatal";
|
||||
|
||||
const TRIGGER_INTERVALS: { [key: string]: number } = {
|
||||
[ERROR_KIND_PERMISSION]: 3 * 60 * 60 * 1000, // 3 hours
|
||||
[ERROR_KIND_FATAL]: 15 * 60 * 1000, // 15 minutes
|
||||
};
|
||||
|
||||
/**
|
||||
* The ErrorCache is used to suppress the same error messages for the same error state.
|
||||
* An example would be when mjolnir has been told to protect a room but is missing some permission such as the ability to send `m.room.server_acl`.
|
||||
* Each time `Mjolnir` synchronizes policies to protected rooms Mjolnir will try to log to the management room that Mjolnir doesn't have permission to send `m.room.server_acl`.
|
||||
* The ErrorCache is an attempt to make sure the error is reported only once.
|
||||
*/
|
||||
export default class ErrorCache {
|
||||
private roomsToErrors: Map<string/*room id*/, Map<string /*error kind*/, number>> = new Map();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the error cache for a room/kind in the situation where circumstances have changed e.g. if Mjolnir has been informed via sync of a `m.room.power_levels` event in the room, we would want to clear `ERROR_KIND_PERMISSION`
|
||||
* so that a user can see if their changes worked.
|
||||
* @param roomId The room to reset the error cache for.
|
||||
* @param kind The kind of error we are resetting.
|
||||
*/
|
||||
@trace
|
||||
public resetError(roomId: string, kind: string) {
|
||||
if (!this.roomsToErrors.has(roomId)) {
|
||||
this.roomsToErrors.set(roomId, new Map());
|
||||
}
|
||||
this.roomsToErrors.get(roomId)?.set(kind, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the error with the cache.
|
||||
* @param roomId The room where the error is occuring or related to.
|
||||
* @param kind What kind of error, either `ERROR_KIND_PERMISSION` or `ERROR_KIND_FATAL`.
|
||||
* @returns True if the error kind has been triggered in that room,
|
||||
* meaning it has been longer than the time specified in `TRIGGER_INTERVALS` since the last trigger (or the first trigger). Otherwise false.
|
||||
*/
|
||||
@trace
|
||||
public triggerError(roomId: string, kind: string): boolean {
|
||||
if (!this.roomsToErrors.get(roomId)) {
|
||||
this.roomsToErrors.set(roomId, new Map());
|
||||
}
|
||||
|
||||
const triggers = this.roomsToErrors.get(roomId)!;
|
||||
if (!triggers.has(kind)) {
|
||||
triggers?.set(kind, 0);
|
||||
}
|
||||
|
||||
const lastTriggerTime = triggers.get(kind)!;
|
||||
const now = new Date().getTime();
|
||||
const interval = TRIGGER_INTERVALS[kind];
|
||||
|
||||
if ((now - lastTriggerTime) >= interval) {
|
||||
triggers.set(kind, now);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,6 +339,9 @@ export class Mjolnir {
|
||||
|
||||
this.currentState = STATE_RUNNING;
|
||||
await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms.");
|
||||
if (this.config.verboseLogging) {
|
||||
await this.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir@startup", "The use of verbose logging is deprecated and will be removed in a future version, check your config.");
|
||||
}
|
||||
} catch (err) {
|
||||
try {
|
||||
LogService.error("Mjolnir", "Error during startup:", err);
|
||||
|
||||
+58
-94
@@ -25,16 +25,15 @@ limitations under the License.
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
import { LogLevel, LogService, MatrixGlob, UserID } from "matrix-bot-sdk";
|
||||
import { Permalinks } from "./commands/interface-manager/Permalinks";
|
||||
import { LogLevel, MatrixGlob, UserID } from "matrix-bot-sdk";
|
||||
import { CommandExceptionKind } from "./commands/interface-manager/CommandException";
|
||||
import { IConfig } from "./config";
|
||||
import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache";
|
||||
import ManagementRoomOutput from "./ManagementRoomOutput";
|
||||
import { MatrixSendClient } from "./MatrixEmitter";
|
||||
import AccessControlUnit, { Access } from "./models/AccessControlUnit";
|
||||
import { RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/ListRule";
|
||||
import PolicyList, { ListRuleChange, Revision } from "./models/PolicyList";
|
||||
import { RoomUpdateError } from "./models/RoomUpdateError";
|
||||
import { printActionResult, IRoomUpdateError, RoomUpdateException } from "./models/RoomUpdateError";
|
||||
import { ProtectionManager } from "./protections/ProtectionManager";
|
||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||
import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker";
|
||||
@@ -75,8 +74,6 @@ export class ProtectedRoomsSet {
|
||||
*/
|
||||
private readonly eventRedactionQueue = new EventRedactionQueue();
|
||||
|
||||
private readonly errorCache = new ErrorCache();
|
||||
|
||||
/**
|
||||
* These are globs sourced from `config.automaticallyRedactForReasons` that are matched against the reason of an
|
||||
* `m.ban` recommendation against a user.
|
||||
@@ -191,7 +188,7 @@ export class ProtectedRoomsSet {
|
||||
* @returns The list of errors encountered, for reporting to the management room.
|
||||
*/
|
||||
@trace
|
||||
public async processRedactionQueue(roomId?: string): Promise<RoomUpdateError[]> {
|
||||
public async processRedactionQueue(roomId?: string): Promise<IRoomUpdateError[]> {
|
||||
return await this.eventRedactionQueue.process(this.client, this.managementRoomOutput, roomId);
|
||||
}
|
||||
|
||||
@@ -213,24 +210,22 @@ export class ProtectedRoomsSet {
|
||||
}
|
||||
this.protectedRoomActivityTracker.handleEvent(roomId, event);
|
||||
if (event['type'] === 'm.room.power_levels' && event['state_key'] === '') {
|
||||
// power levels were updated - recheck permissions
|
||||
this.errorCache.resetError(roomId, ERROR_KIND_PERMISSION);
|
||||
await this.managementRoomOutput.logMessage(LogLevel.DEBUG, "Mjolnir", `Power levels changed in ${roomId} - checking permissions...`, roomId);
|
||||
const errors = await this.protectionManager.verifyPermissionsIn(roomId);
|
||||
const hadErrors = await this.printActionResult(errors);
|
||||
if (!hadErrors) {
|
||||
await this.managementRoomOutput.logMessage(LogLevel.DEBUG, "Mjolnir", `All permissions look OK.`);
|
||||
}
|
||||
await this.printActionResult(errors, { title: "There were errors verifying permissions.", noErrorsText: "All permissions look OK." });
|
||||
return;
|
||||
} else if (event['type'] === "m.room.member") {
|
||||
// The reason we have to apply bans on each member change is because
|
||||
// we cannot eagerly ban users (that is to ban them when they have never been a member)
|
||||
// as they can be force joined to a room they might not have known existed.
|
||||
// Only apply bans and then redactions in the room we are currently looking at.
|
||||
const banErrors = await this.applyUserBans([roomId]);
|
||||
const redactionErrors = await this.processRedactionQueue(roomId);
|
||||
await this.printActionResult(banErrors);
|
||||
await this.printActionResult(redactionErrors);
|
||||
const errors = [
|
||||
await this.applyUserBans([roomId]),
|
||||
await this.processRedactionQueue(roomId),
|
||||
].flat();
|
||||
if (errors.length > 0) {
|
||||
await this.printActionResult(errors, { title: 'There were errors updating member bans.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,26 +234,12 @@ export class ProtectedRoomsSet {
|
||||
*/
|
||||
@trace
|
||||
private async syncRoomsWithPolicies() {
|
||||
let hadErrors = false;
|
||||
const [aclErrors, banErrors] = await Promise.all([
|
||||
const errors = (await Promise.all([
|
||||
this.applyServerAcls(this.policyLists, this.protectedRoomsByActivity()),
|
||||
this.applyUserBans(this.protectedRoomsByActivity())
|
||||
]);
|
||||
const redactionErrors = await this.processRedactionQueue();
|
||||
hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:");
|
||||
hadErrors = hadErrors || await this.printActionResult(banErrors, "Errors updating member bans:");
|
||||
hadErrors = hadErrors || await this.printActionResult(redactionErrors, "Error updating redactions:");
|
||||
|
||||
if (!hadErrors) {
|
||||
const html = `<font color="#00cc00">Done updating rooms - no errors</font>`;
|
||||
const text = "Done updating rooms - no errors";
|
||||
await this.client.sendMessage(this.managementRoomId, {
|
||||
msgtype: "m.notice",
|
||||
body: text,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: html,
|
||||
});
|
||||
}
|
||||
this.applyUserBans(this.protectedRoomsByActivity()),
|
||||
this.processRedactionQueue()
|
||||
])).flat();
|
||||
await this.printActionResult(errors, { title: "There were errors synchronising the protected rooms." });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,9 +305,12 @@ export class ProtectedRoomsSet {
|
||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with.
|
||||
*/
|
||||
@trace
|
||||
private async applyServerAcls(lists: PolicyList[], roomIds: string[]): Promise<RoomUpdateError[]> {
|
||||
private async applyServerAcls(lists: PolicyList[], roomIds: string[]): Promise<IRoomUpdateError[]> {
|
||||
// we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event
|
||||
// finish out of order and therefore leave the room out of sync with the policy lists.
|
||||
if (this.config.disableServerACL) {
|
||||
return [];
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this.aclChain = this.aclChain
|
||||
.then(() => this._applyServerAcls(lists, roomIds))
|
||||
@@ -334,7 +318,7 @@ export class ProtectedRoomsSet {
|
||||
});
|
||||
}
|
||||
|
||||
private async _applyServerAcls(lists: PolicyList[], roomIds: string[]): Promise<RoomUpdateError[]> {
|
||||
private async _applyServerAcls(lists: PolicyList[], roomIds: string[]): Promise<IRoomUpdateError[]> {
|
||||
const serverName: string = new UserID(await this.client.getUserId()).domain;
|
||||
|
||||
// Construct a server ACL first
|
||||
@@ -346,7 +330,7 @@ export class ProtectedRoomsSet {
|
||||
await this.client.sendNotice(this.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`);
|
||||
}
|
||||
|
||||
const errors: RoomUpdateError[] = [];
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
for (const roomId of roomIds) {
|
||||
try {
|
||||
await this.managementRoomOutput.logMessage(LogLevel.DEBUG, "ApplyAcl", `Checking ACLs for ${roomId}`, roomId);
|
||||
@@ -371,8 +355,8 @@ export class ProtectedRoomsSet {
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
const kind = message && message.includes("You don't have permission to post that to the room") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL;
|
||||
errors.push({ roomId, errorMessage: message, errorKind: kind });
|
||||
const kind = message && message.includes("You don't have permission to post that to the room") ? CommandExceptionKind.Known : CommandExceptionKind.Unknown;
|
||||
errors.push(new RoomUpdateException(roomId, kind, e, message))
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
@@ -385,17 +369,18 @@ export class ProtectedRoomsSet {
|
||||
* @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with.
|
||||
*/
|
||||
@trace
|
||||
private async applyUserBans(roomIds: string[]): Promise<RoomUpdateError[]> {
|
||||
private async applyUserBans(roomIds: string[]): Promise<IRoomUpdateError[]> {
|
||||
// We can only ban people who are not already banned, and who match the rules.
|
||||
const errors: RoomUpdateError[] = [];
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
|
||||
const addErrorToReport = (roomId: string, e: any) => {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
errors.push({
|
||||
errors.push(new RoomUpdateException(
|
||||
roomId,
|
||||
errorMessage: message,
|
||||
errorKind: message && message.includes("You don't have permission to ban") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL,
|
||||
});
|
||||
message && message.includes("You don't have permission to ban") ? CommandExceptionKind.Known : CommandExceptionKind.Unknown,
|
||||
e,
|
||||
message
|
||||
));
|
||||
};
|
||||
|
||||
for (const roomId of roomIds) {
|
||||
@@ -493,42 +478,29 @@ export class ProtectedRoomsSet {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async printActionResult(errors: RoomUpdateError[], title: string | null = null, logAnyways = false) {
|
||||
if (errors.length <= 0) return false;
|
||||
private async printActionResult(
|
||||
errors: IRoomUpdateError[],
|
||||
renderOptions: { title?: string, noErrorsText?: string }
|
||||
): Promise<void> {
|
||||
await printActionResult(this.client, this.managementRoomId, errors, renderOptions);
|
||||
}
|
||||
|
||||
if (!logAnyways) {
|
||||
errors = errors.filter(e => this.errorCache.triggerError(e.roomId, e.errorKind));
|
||||
if (errors.length <= 0) {
|
||||
LogService.warn("Mjolnir", "Multiple errors are happening, however they are muted. Please check the management room.");
|
||||
return true;
|
||||
public async unbanUser(user: string): Promise<IRoomUpdateError[]> {
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
for (const room of this.protectedRoomActivityTracker.protectedRoomsByActivity()) {
|
||||
try {
|
||||
await this.client.unbanUser(user, room);
|
||||
} catch (e) {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
errors.push(new RoomUpdateException(
|
||||
room,
|
||||
message && message.includes("You don't have permission to ban") ? CommandExceptionKind.Known : CommandExceptionKind.Unknown,
|
||||
e,
|
||||
message
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let html = "";
|
||||
let text = "";
|
||||
|
||||
const htmlTitle = title ? `${title}<br />` : '';
|
||||
const textTitle = title ? `${title}\n` : '';
|
||||
|
||||
html += `<font color="#ff0000"><b>${htmlTitle}${errors.length} errors updating protected rooms!</b></font><br /><ul>`;
|
||||
text += `${textTitle}${errors.length} errors updating protected rooms!\n`;
|
||||
const viaServers = [(new UserID(await this.client.getUserId())).domain];
|
||||
for (const error of errors) {
|
||||
const alias = (await this.client.getPublishedAlias(error.roomId)) || error.roomId;
|
||||
const url = Permalinks.forRoom(alias, viaServers);
|
||||
html += `<li><a href="${url}">${alias}</a> - ${error.errorMessage}</li>`;
|
||||
text += `${url} - ${error.errorMessage}\n`;
|
||||
}
|
||||
html += "</ul>";
|
||||
|
||||
const message = {
|
||||
msgtype: "m.notice",
|
||||
body: text,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: html,
|
||||
};
|
||||
await this.client.sendMessage(this.managementRoomId, message);
|
||||
return true;
|
||||
return errors;
|
||||
}
|
||||
|
||||
public requiredProtectionPermissions() {
|
||||
@@ -536,22 +508,14 @@ export class ProtectedRoomsSet {
|
||||
}
|
||||
|
||||
@trace
|
||||
public async verifyPermissions(verbose = true, printRegardless = false) {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
public async verifyPermissions() {
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
for (const roomId of this.protectedRooms) {
|
||||
errors.push(...(await this.protectionManager.verifyPermissionsIn(roomId)));
|
||||
}
|
||||
|
||||
const hadErrors = await this.printActionResult(errors, "Permission errors in protected rooms:", printRegardless);
|
||||
if (!hadErrors && verbose) {
|
||||
const html = `<font color="#00cc00">All permissions look OK.</font>`;
|
||||
const text = "All permissions look OK.";
|
||||
await this.client.sendMessage(this.managementRoomId, {
|
||||
msgtype: "m.notice",
|
||||
body: text,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: html,
|
||||
});
|
||||
}
|
||||
await this.printActionResult(errors, {
|
||||
title: "There are permission errors in protected rooms.",
|
||||
noErrorsText: "All permissions look OK."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ import { BaseFunction, CommandTable, defineCommandTable, findCommandTable, findT
|
||||
import { findMatrixInterfaceAdaptor, MatrixContext } from "./interface-manager/MatrixInterfaceAdaptor";
|
||||
import { ArgumentStream } from "./interface-manager/ParameterParsing";
|
||||
import { CommandResult } from "./interface-manager/Validation";
|
||||
import { CommandException } from "./interface-manager/CommandException";
|
||||
import { CommandException, CommandExceptionKind } from "./interface-manager/CommandException";
|
||||
import { tickCrossRenderer } from "./interface-manager/MatrixHelpRenderer";
|
||||
import "./interface-manager/MatrixPresentations";
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function handleCommand(roomId: string, event: { content: { body: st
|
||||
try {
|
||||
return await adaptor.invoke(mjolnirContext, mjolnirContext, ...stream.rest());
|
||||
} catch (e) {
|
||||
const commandError = new CommandException(e, 'Unknown Unexpected Error');
|
||||
const commandError = new CommandException(CommandExceptionKind.Unknown, e, 'Unknown Unexpected Error');
|
||||
await tickCrossRenderer.call(mjolnirContext, mjolnir.client, roomId, event, CommandResult.Err(commandError));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ import { Mjolnir } from "../Mjolnir";
|
||||
|
||||
// !mjolnir verify
|
||||
export async function execPermissionCheckCommand(roomId: string, event: any, mjolnir: Mjolnir) {
|
||||
return mjolnir.protectedRoomsTracker.verifyPermissions(true, true);
|
||||
return mjolnir.protectedRoomsTracker.verifyPermissions();
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { findPresentationType, parameters } from "./interface-manager/ParameterP
|
||||
import { MjolnirContext } from "./CommandHandler";
|
||||
import { MatrixRoomID, MatrixRoomReference } from "./interface-manager/MatrixRoomReference";
|
||||
import { CommandResult } from "./interface-manager/Validation";
|
||||
import { CommandException } from "./interface-manager/CommandException";
|
||||
import { CommandException, CommandExceptionKind } from "./interface-manager/CommandException";
|
||||
import { defineMatrixInterfaceAdaptor } from "./interface-manager/MatrixInterfaceAdaptor";
|
||||
import { tickCrossRenderer } from "./interface-manager/MatrixHelpRenderer";
|
||||
import { DocumentNode } from "./interface-manager/DeadDocument";
|
||||
@@ -92,7 +92,7 @@ defineInterfaceCommand({
|
||||
return CommandException.Result<MatrixRoomID>(
|
||||
`The homeserver that Draupnir is hosted on cannot join this room using the room reference provided.\
|
||||
Try an alias or the "share room" button in your client to obtain a valid reference to the room.`,
|
||||
{ exception: e }
|
||||
{ exception: e, exceptionKind: CommandExceptionKind.Unknown }
|
||||
);
|
||||
}
|
||||
})();
|
||||
@@ -121,7 +121,10 @@ defineInterfaceCommand({
|
||||
try {
|
||||
await this.mjolnir.client.leaveRoom(roomID.toRoomIdOrAlias());
|
||||
} catch (exception) {
|
||||
return CommandException.Result(`Failed to leave ${roomRef.toPermalink()} - the room is no longer being protected, but the bot could not leave.`, { exception });
|
||||
return CommandException.Result(
|
||||
`Failed to leave ${roomRef.toPermalink()} - the room is no longer being protected, but the bot could not leave.`,
|
||||
{ exceptionKind: CommandExceptionKind.Unknown, exception }
|
||||
);
|
||||
}
|
||||
return CommandResult.Ok(undefined);
|
||||
},
|
||||
|
||||
@@ -60,7 +60,7 @@ async function renderListMatches(
|
||||
)
|
||||
}
|
||||
|
||||
function renderListRules(list: ListMatches) {
|
||||
export function renderListRules(list: ListMatches) {
|
||||
const renderRuleSummary = (rule: ListRule, entityDescription: string) => {
|
||||
return <li>
|
||||
{entityDescription} (<code>{rule.recommendation}</code>): <code>{rule.entity}</code> ({rule.reason})
|
||||
|
||||
@@ -6,6 +6,22 @@ import { randomUUID } from "crypto";
|
||||
import * as api from '@opentelemetry/api';
|
||||
import { CommandError, CommandResult } from "./Validation";
|
||||
import { trace } from "../../utils";
|
||||
import { LogService } from "matrix-bot-sdk";
|
||||
|
||||
export enum CommandExceptionKind {
|
||||
/**
|
||||
* This class is for exceptions that need to be reported to the user,
|
||||
* but are mostly irrelevant to the developers because the behaviour is well
|
||||
* understood and expected. These exceptions will never be logged to the error
|
||||
* level.
|
||||
*/
|
||||
Known = 'Known',
|
||||
/**
|
||||
* This class is to be used for reporting unexpected or unknown exceptions
|
||||
* that the developers need to know about.
|
||||
*/
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
// FIXME: I wonder if we could allow message to be JSX?
|
||||
// Then room references could be put into the DM and actually mean something.
|
||||
@@ -13,13 +29,25 @@ export class CommandException extends CommandError {
|
||||
public readonly uuid: string = api.trace.getSpan(api.context.active())?.spanContext().traceId ?? randomUUID();
|
||||
|
||||
constructor(
|
||||
public readonly exceptionKind: CommandExceptionKind,
|
||||
public readonly exception: Error | unknown,
|
||||
message: string) {
|
||||
super(message)
|
||||
this.log();
|
||||
}
|
||||
|
||||
@trace
|
||||
public static Result<Ok>(message: string, options: { exception: Error }): CommandResult<Ok, CommandException> {
|
||||
return CommandResult.Err(new CommandException(options.exception, message));
|
||||
public static Result<Ok>(message: string, options: { exception: Error, exceptionKind: CommandExceptionKind }): CommandResult<Ok, CommandException> {
|
||||
return CommandResult.Err(new CommandException(options.exceptionKind, options.exception, message));
|
||||
}
|
||||
|
||||
@trace
|
||||
protected log(): void {
|
||||
const logArguments: Parameters<typeof LogService['info']> = ["CommandException", this.exceptionKind, this.uuid, this.message, this.exception];
|
||||
if (this.exceptionKind === CommandExceptionKind.Known) {
|
||||
LogService.info(...logArguments);
|
||||
} else {
|
||||
LogService.error(...logArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export enum NodeTag {
|
||||
Fragment = 'fragment',
|
||||
Details = 'details',
|
||||
Summary = 'summary',
|
||||
Font = 'font',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,9 +4,26 @@
|
||||
*/
|
||||
|
||||
import { htmlEscape } from "../../utils";
|
||||
import { FringeLeafRenderFunction, FringeType, LeafNode, NodeTag, SimpleFringeRenderer } from "./DeadDocument";
|
||||
import { DocumentNode, FringeLeafRenderFunction, FringeType, LeafNode, NodeTag, SimpleFringeRenderer, TagDynamicEnvironment } from "./DeadDocument";
|
||||
import { blank, staticString, TransactionalOutputContext } from "./DeadDocumentMarkdown";
|
||||
|
||||
function writeAttributableNode(tagName: string, _fringe: FringeType, node: DocumentNode, context: TransactionalOutputContext, _environment: TagDynamicEnvironment) {
|
||||
context.output.writeString(`<${tagName}`);
|
||||
if (node.attributeMap.size > 0) {
|
||||
for (const [key, value] of node.attributeMap.entries()) {
|
||||
context.output.writeString(` ${htmlEscape(key)}="${htmlEscape(value)}"`);
|
||||
}
|
||||
}
|
||||
context.output.writeString('>')
|
||||
}
|
||||
|
||||
function attributableNode(tagName: string) {
|
||||
return function(fringe: FringeType, node: DocumentNode, context: TransactionalOutputContext, environment: TagDynamicEnvironment) {
|
||||
writeAttributableNode(tagName, fringe, node, context, environment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const HTML_RENDERER = new SimpleFringeRenderer<TransactionalOutputContext>();
|
||||
|
||||
HTML_RENDERER.registerRenderer<FringeLeafRenderFunction<TransactionalOutputContext>>(
|
||||
@@ -52,16 +69,11 @@ HTML_RENDERER.registerRenderer<FringeLeafRenderFunction<TransactionalOutputConte
|
||||
staticString('<i>'),
|
||||
staticString('</i>')
|
||||
).registerInnerNode(NodeTag.Anchor,
|
||||
function(_fringe, node, context, _environment) {
|
||||
context.output.writeString('<a');
|
||||
if (node.attributeMap.size > 0) {
|
||||
for (const [key, value] of node.attributeMap.entries()) {
|
||||
context.output.writeString(` ${htmlEscape(key)}="${htmlEscape(value)}"`);
|
||||
}
|
||||
}
|
||||
context.output.writeString('>')
|
||||
},
|
||||
attributableNode('a'),
|
||||
staticString('</a>')
|
||||
).registerInnerNode(NodeTag.Font,
|
||||
attributableNode('font'),
|
||||
staticString('</font>')
|
||||
).registerInnerNode(NodeTag.Root,
|
||||
blank,
|
||||
blank
|
||||
|
||||
@@ -142,4 +142,7 @@ MARKDOWN_RENDERER.registerRenderer<FringeLeafRenderFunction<TransactionalOutputC
|
||||
).registerInnerNode(NodeTag.Summary,
|
||||
staticString('<summary>'),
|
||||
staticString('</summary>')
|
||||
).registerInnerNode(NodeTag.Font,
|
||||
blank,
|
||||
blank
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function renderMatrix(node: DocumentNode, cb: SendMatrixEventCB): P
|
||||
context.output.commit(commitNode);
|
||||
};
|
||||
if (node.tag !== NodeTag.Root) {
|
||||
// rendering has to start (and end) with a committable node.
|
||||
throw new TypeError("Tried to render a node without a root, this will not be committable");
|
||||
}
|
||||
const markdownOutput = new PagedDuplexStream();
|
||||
|
||||
@@ -95,7 +95,7 @@ export class CommandError {
|
||||
public constructor(
|
||||
public readonly message: string,
|
||||
) {
|
||||
|
||||
// nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface IConfig {
|
||||
logMutedModules: string[],
|
||||
syncOnStartup: boolean;
|
||||
verifyPermissionsOnStartup: boolean;
|
||||
disableServerACL: boolean;
|
||||
noop: boolean;
|
||||
protectedRooms: string[]; // matrix.to urls
|
||||
fasterMembershipChecks: boolean;
|
||||
@@ -158,6 +159,7 @@ const defaultConfig: IConfig = {
|
||||
syncOnStartup: true,
|
||||
verifyPermissionsOnStartup: true,
|
||||
noop: false,
|
||||
disableServerACL: false,
|
||||
protectedRooms: [],
|
||||
fasterMembershipChecks: false,
|
||||
automaticallyRedactForReasons: ["spam", "advertising"],
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
* This modified file incorperates work from mjolnir
|
||||
* https://github.com/matrix-org/mjolnir
|
||||
* which included the following license notice:
|
||||
|
||||
Copyright 2019 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.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
export interface RoomUpdateError {
|
||||
roomId: string;
|
||||
errorMessage: string;
|
||||
errorKind: string;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
* This modified file incorperates work from mjolnir
|
||||
* https://github.com/matrix-org/mjolnir
|
||||
* which included the following license notice:
|
||||
|
||||
Copyright 2019 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.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
import { UserID } from "matrix-bot-sdk";
|
||||
import { CommandException, CommandExceptionKind } from "../commands/interface-manager/CommandException";
|
||||
import { DocumentNode } from "../commands/interface-manager/DeadDocument";
|
||||
import { renderMatrixAndSend } from "../commands/interface-manager/DeadDocumentMatrix";
|
||||
import { JSXFactory } from "../commands/interface-manager/JSXFactory";
|
||||
import { Permalinks } from "../commands/interface-manager/Permalinks";
|
||||
import { CommandError, CommandResult } from "../commands/interface-manager/Validation";
|
||||
import { MatrixSendClient } from "../MatrixEmitter";
|
||||
|
||||
export interface IRoomUpdateError extends CommandError {
|
||||
readonly roomId: string,
|
||||
}
|
||||
|
||||
export class PermissionError extends CommandError implements IRoomUpdateError {
|
||||
constructor(
|
||||
public readonly roomId: string,
|
||||
message: string
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomUpdateException extends CommandException implements IRoomUpdateError {
|
||||
constructor(public readonly roomId: string, ...args: ConstructorParameters<typeof CommandException>) {
|
||||
super(...args);
|
||||
}
|
||||
|
||||
public static Result<Ok>(
|
||||
message: string,
|
||||
options: {
|
||||
exception: Error,
|
||||
exceptionKind: CommandExceptionKind,
|
||||
roomId: string
|
||||
}): CommandResult<Ok, RoomUpdateException> {
|
||||
return CommandResult.Err(new RoomUpdateException(options.roomId, options.exceptionKind, options.exception, message));
|
||||
}
|
||||
}
|
||||
|
||||
function renderErrorItem(error: IRoomUpdateError, viaServers: string[]): DocumentNode {
|
||||
return <li>
|
||||
<a href={Permalinks.forRoom(error.roomId, viaServers)}>{error.roomId}</a> - {error.message}
|
||||
</li>
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a message to show to the user after taking an action in a room or a set of rooms.
|
||||
* @param client A matrix client.
|
||||
* @param errors Any errors associated with the action.
|
||||
* @param options.title To give context about what the action was, shown when there are errors.
|
||||
* @param options.noErrorsText To show when there are no errors.
|
||||
* @param options.skipNoErrors is ineffective and does nothing, it is an option for the accompnying `printActionResult`.
|
||||
* @returns A `DocumentNode` fragment that can be sent to Matrix or incorperated into another message.
|
||||
*/
|
||||
export async function renderActionResult(
|
||||
client: MatrixSendClient,
|
||||
errors: IRoomUpdateError[],
|
||||
{ title = 'There were errors updating protected rooms.', noErrorsText = 'Done updating rooms - no errors.'}: { title?: string, noErrorsText?: string } = {}
|
||||
): Promise<DocumentNode> {
|
||||
if (errors.length === 0) {
|
||||
return <fragment><font color="#00cc00">{noErrorsText}</font></fragment>
|
||||
}
|
||||
// This is a little unfortunate because for some reason we don't have a way to keep
|
||||
// room references around that have vias and are meaningful
|
||||
// there isn't really any easy way to do this :(
|
||||
const viaServers = [(new UserID(await client.getUserId())).domain];
|
||||
return <fragment>
|
||||
<font color="#ff0000">
|
||||
{title}<br/>
|
||||
</font>
|
||||
<details>
|
||||
<summary>
|
||||
<font color="#ff0000">
|
||||
<b>{errors.length} errors updating protected rooms!</b><br/>
|
||||
</font>
|
||||
</summary>
|
||||
<ul>
|
||||
{errors.map(error => renderErrorItem(error, viaServers))}
|
||||
</ul>
|
||||
</details>
|
||||
</fragment>
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a message to represent the outcome of an action in an update.
|
||||
* @param client A matrix client to send a notice with.
|
||||
* @param roomId The room to send the notice to.
|
||||
* @param errors Any errors that are a result of the action.
|
||||
* @param options.title To give context about what the action was, shown when there are errors.
|
||||
* @param options.noErrorsText To show when there are no errors.
|
||||
* @returns
|
||||
*/
|
||||
export async function printActionResult(
|
||||
client: MatrixSendClient,
|
||||
roomId: string,
|
||||
errors: IRoomUpdateError[],
|
||||
renderOptions: { title?: string, noErrorsText?: string } = {}
|
||||
): Promise<void> {
|
||||
await renderMatrixAndSend(
|
||||
<root>{await renderActionResult(client, errors, renderOptions)}</root>,
|
||||
roomId,
|
||||
undefined,
|
||||
client,
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,9 @@ limitations under the License.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*
|
||||
* In addition to the above, I want to add that this protection was inspired by
|
||||
* a Mjolnir PR that was originally created by Gergő Fándly https://github.com/matrix-org/mjolnir/pull/223
|
||||
*/
|
||||
|
||||
import { Protection } from "./Protection";
|
||||
@@ -31,18 +34,21 @@ import { LogService } from "matrix-bot-sdk";
|
||||
import { JSXFactory } from "../commands/interface-manager/JSXFactory";
|
||||
import { renderMatrixAndSend } from "../commands/interface-manager/DeadDocumentMatrix";
|
||||
import { renderMentionPill } from "../commands/interface-manager/MatrixHelpRenderer";
|
||||
import { RULE_USER } from "../models/ListRule";
|
||||
import { RULE_USER, ListRule } from "../models/ListRule";
|
||||
import { UserID } from "matrix-bot-sdk";
|
||||
import { ReactionListener } from "../commands/interface-manager/MatrixReactionHandler";
|
||||
import { PolicyListManager } from "../models/PolicyListManager";
|
||||
import { MatrixRoomReference } from "../commands/interface-manager/MatrixRoomReference";
|
||||
import { findPolicyListFromRoomReference } from "../commands/Ban";
|
||||
import { trace } from '../utils';
|
||||
import PolicyList from "../models/PolicyList";
|
||||
import { renderListRules } from "../commands/Rules";
|
||||
import { printActionResult, IRoomUpdateError, RoomUpdateException } from "../models/RoomUpdateError";
|
||||
import { CommandExceptionKind } from "../commands/interface-manager/CommandException";
|
||||
|
||||
const PROPAGATION_PROMPT_LISTENER = 'ge.applied-langua.ge.draupnir.ban_propagation';
|
||||
const BAN_PROPAGATION_PROMPT_LISTENER = 'ge.applied-langua.ge.draupnir.ban_propagation';
|
||||
const UNBAN_PROPAGATION_PROMPT_LISTENER = 'ge.applied-langua.ge.draupnir.unban_propagation';
|
||||
|
||||
function makePolicyListShortcodeReferenceMap(lists: PolicyListManager): Map<string, string> {
|
||||
return lists.lists.reduce((map, list, index) => (map.set(`${index + 1}.`, list.roomRef), map), new Map())
|
||||
function makePolicyListShortcodeReferenceMap(lists: PolicyList[]): Map<string, string> {
|
||||
return lists.reduce((map, list, index) => (map.set(`${index + 1}.`, list.roomRef), map), new Map())
|
||||
}
|
||||
|
||||
// would be nice to be able to use presentation types here idk.
|
||||
@@ -63,7 +69,7 @@ async function promptBanPropagation(
|
||||
event: any,
|
||||
roomId: string
|
||||
): Promise</*event id*/string> {
|
||||
const reactionMap = makePolicyListShortcodeReferenceMap(mjolnir.policyListManager);
|
||||
const reactionMap = makePolicyListShortcodeReferenceMap(mjolnir.policyListManager.lists);
|
||||
const promptEventId = (await renderMatrixAndSend(
|
||||
<root>The user {renderMentionPill(event["state_key"], event["content"]?.["displayname"] ?? event["state_key"])} was banned
|
||||
in <a href={`https://matrix.to/#/${roomId}`}>{roomId}</a> by {new UserID(event["sender"])} for <code>{event["content"]?.["reason"] ?? '<no reason supplied>'}</code>.<br />
|
||||
@@ -76,7 +82,7 @@ async function promptBanPropagation(
|
||||
undefined,
|
||||
mjolnir.client,
|
||||
mjolnir.reactionHandler.createAnnotation(
|
||||
PROPAGATION_PROMPT_LISTENER,
|
||||
BAN_PROPAGATION_PROMPT_LISTENER,
|
||||
reactionMap,
|
||||
{
|
||||
target: event["state_key"],
|
||||
@@ -88,6 +94,119 @@ async function promptBanPropagation(
|
||||
return promptEventId;
|
||||
}
|
||||
|
||||
async function promptUnbanPropagation(
|
||||
mjolnir: Mjolnir,
|
||||
event: any,
|
||||
roomId: string,
|
||||
rulesMatchingUser: Map<PolicyList, ListRule[]>
|
||||
): Promise</*event id*/string> {
|
||||
const reactionMap = new Map<string, string>(Object.entries({ 'unban from all': 'unban from all'}));
|
||||
// shouldn't we warn them that the unban will be futile?
|
||||
const promptEventId = (await renderMatrixAndSend(
|
||||
<root>
|
||||
The user {renderMentionPill(event["state_key"], event["content"]?.["displayname"] ?? event["state_key"])} was unbanned
|
||||
from the room <a href={`https://matrix.to/#/${roomId}`}>{roomId}</a> by {new UserID(event["sender"])} for <code>{event["content"]?.["reason"] ?? '<no reason supplied>'}</code>.<br/>
|
||||
However there are rules in Draupnir's watched lists matching this user:
|
||||
<ul>
|
||||
{
|
||||
[...rulesMatchingUser.entries()]
|
||||
.map(([list, rules]) => <li>{renderListRules({
|
||||
shortcode: list.listShortcode,
|
||||
roomRef: list.roomRef,
|
||||
roomId: list.roomId,
|
||||
matches: rules
|
||||
})}</li>)
|
||||
}
|
||||
</ul>
|
||||
Would you like to remove these rules and unban the user from all protected rooms?
|
||||
</root>,
|
||||
mjolnir.managementRoomId,
|
||||
undefined,
|
||||
mjolnir.client,
|
||||
mjolnir.reactionHandler.createAnnotation(
|
||||
UNBAN_PROPAGATION_PROMPT_LISTENER,
|
||||
reactionMap,
|
||||
{
|
||||
target: event["state_key"],
|
||||
reason: event["content"]?.["reason"],
|
||||
}
|
||||
)
|
||||
)).at(0) as string;
|
||||
await mjolnir.reactionHandler.addReactionsToEvent(mjolnir.client, mjolnir.managementRoomId, promptEventId, reactionMap);
|
||||
return promptEventId;
|
||||
}
|
||||
|
||||
interface ListenerContext {
|
||||
mjolnir: Mjolnir,
|
||||
}
|
||||
|
||||
async function banReactionListener(this: ListenerContext, key: string, item: unknown, context: BanPropagationMessageContext) {
|
||||
try {
|
||||
if (typeof item === 'string') {
|
||||
const listRef = MatrixRoomReference.fromPermalink(item);
|
||||
const listResult = await findPolicyListFromRoomReference(this.mjolnir, listRef);
|
||||
if (listResult.isOk()) {
|
||||
return await listResult.ok.banEntity(RULE_USER, context.target, context.reason);
|
||||
} else {
|
||||
LogService.warn("BanPropagation", "Timed out waiting for a response to a room level ban", listResult.err);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw new TypeError("The ban prompt event's reaction map is malformed.")
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error('BanPropagation', "Encountered an error while prompting the user for instructions to propagate a room level ban", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function unbanFromAllLists(mjolnir: Mjolnir, user: string): Promise<IRoomUpdateError[]> {
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
for (const list of mjolnir.policyListManager.lists) {
|
||||
try {
|
||||
await list.unbanEntity(RULE_USER, user);
|
||||
} catch (e) {
|
||||
LogService.info('BanPropagation', `Could not unban ${user} from ${list.roomRef}`, e);
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
errors.push(new RoomUpdateException(
|
||||
list.roomId,
|
||||
message.includes("You don't have permission") ? CommandExceptionKind.Known : CommandExceptionKind.Unknown,
|
||||
e,
|
||||
message
|
||||
));
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function unbanUserReactionListener(this: ListenerContext, _key: string, item: unknown, context: BanPropagationMessageContext): Promise<void> {
|
||||
try {
|
||||
if (item === 'unban from all') {
|
||||
const listErrors = await unbanFromAllLists(this.mjolnir, context.target);
|
||||
if (listErrors.length > 0) {
|
||||
await printActionResult(
|
||||
this.mjolnir.client,
|
||||
this.mjolnir.managementRoomId,
|
||||
listErrors,
|
||||
{ title: `There were errors unbanning ${context.target} from all lists.`}
|
||||
);
|
||||
} else {
|
||||
const unbanErrors = await this.mjolnir.protectedRoomsTracker.unbanUser(context.target);
|
||||
await printActionResult(
|
||||
this.mjolnir.client,
|
||||
this.mjolnir.managementRoomId,
|
||||
unbanErrors,
|
||||
{
|
||||
title: `There were errors unbanning ${context.target} from protected rooms.`,
|
||||
noErrorsText: `Done unbanning ${context.target} from protected rooms - no errors.`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error(`BanPropagationProtection`, "Unexpected error unbanning a user", e);
|
||||
}
|
||||
}
|
||||
|
||||
export class BanPropagation extends Protection {
|
||||
|
||||
settings = {};
|
||||
@@ -103,40 +222,36 @@ export class BanPropagation extends Protection {
|
||||
|
||||
@trace
|
||||
public async registerProtection(mjolnir: Mjolnir): Promise<void> {
|
||||
const listener: ReactionListener = async (key, item, context: BanPropagationMessageContext) => {
|
||||
try {
|
||||
if (typeof item === 'string') {
|
||||
const listRef = MatrixRoomReference.fromPermalink(item);
|
||||
const listResult = await findPolicyListFromRoomReference(mjolnir, listRef);
|
||||
if (listResult.isOk()) {
|
||||
return listResult.ok.banEntity(RULE_USER, context.target, context.reason);
|
||||
} else {
|
||||
LogService.warn("BanPropagation", "Timed out waiting for a response to a room level ban", listResult.err);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw new TypeError("The ban prompt event's reaction map is malformed.")
|
||||
}
|
||||
} catch (e) {
|
||||
LogService.error('BanPropagation', "Encountered an error while prompting the user for instructions to propagate a room level ban", e);
|
||||
}
|
||||
};
|
||||
mjolnir.reactionHandler.on(PROPAGATION_PROMPT_LISTENER, listener);
|
||||
mjolnir.reactionHandler.on(BAN_PROPAGATION_PROMPT_LISTENER, banReactionListener.bind({ mjolnir }));
|
||||
mjolnir.reactionHandler.on(UNBAN_PROPAGATION_PROMPT_LISTENER, unbanUserReactionListener.bind({ mjolnir }));
|
||||
}
|
||||
|
||||
@trace
|
||||
public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise<any> {
|
||||
if (event['type'] !== 'm.room.member' || event['content']?.['membership'] !== 'ban') {
|
||||
if (event['type'] !== 'm.room.member'
|
||||
|| !(event['content']?.['membership'] === 'ban' || event['content']?.['membership'] === 'leave')) {
|
||||
return;
|
||||
}
|
||||
if (mjolnir.policyListManager.lists.map(
|
||||
list => list.rulesMatchingEntity(event['state_key'], RULE_USER)
|
||||
).some(rules => rules.length > 0)
|
||||
) {
|
||||
return; // The user is already banned.
|
||||
const rulesMatchingUser = mjolnir.policyListManager.lists.reduce(
|
||||
(listMap, list) => {
|
||||
const rules = list.rulesMatchingEntity(event['state_key'], RULE_USER);
|
||||
if (rules.length > 0) {
|
||||
listMap.set(list, rules)
|
||||
};
|
||||
return listMap
|
||||
}, new Map<PolicyList, ListRule[]>()
|
||||
);
|
||||
const userMembership = event['content']?.['membership'];
|
||||
if (userMembership === 'ban') {
|
||||
if (rulesMatchingUser.size > 0) {
|
||||
return; // The user is already banned.
|
||||
}
|
||||
// do not await, we don't want to block the protection manager
|
||||
promptBanPropagation(mjolnir, event, roomId)
|
||||
} else if (userMembership === 'leave' && rulesMatchingUser.size > 0) {
|
||||
// Then this is a banned user being unbanned.
|
||||
// do not await, we don't want to block the protection manager
|
||||
promptUnbanPropagation(mjolnir, event, roomId, rulesMatchingUser);
|
||||
}
|
||||
// do not await, we don't want to block the protection manager
|
||||
promptBanPropagation(mjolnir, event, roomId)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ import { LogLevel, LogService } from "matrix-bot-sdk";
|
||||
import { ProtectionSettingValidationError } from "./ProtectionSettings";
|
||||
import { Consequence } from "./consequence";
|
||||
import { htmlEscape, trace } from "../utils";
|
||||
import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
import { IRoomUpdateError, PermissionError, RoomUpdateException } from "../models/RoomUpdateError";
|
||||
import { BanPropagation } from "./BanPropagation";
|
||||
import { MatrixDataManager, RawSchemedData, SchemaMigration, SCHEMA_VERSION_KEY } from "../models/MatrixDataManager";
|
||||
import { Permalinks } from "../commands/interface-manager/Permalinks";
|
||||
import { CommandExceptionKind } from "../commands/interface-manager/CommandException";
|
||||
|
||||
const PROTECTIONS: Protection[] = [
|
||||
new FirstMessageIsImage(),
|
||||
@@ -403,8 +403,8 @@ export class ProtectionManager {
|
||||
}
|
||||
|
||||
@trace
|
||||
public async verifyPermissionsIn(roomId: string): Promise<RoomUpdateError[]> {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
public async verifyPermissionsIn(roomId: string): Promise<IRoomUpdateError[]> {
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
const additionalPermissions = this.requiredProtectionPermissions();
|
||||
|
||||
try {
|
||||
@@ -432,35 +432,21 @@ export class ProtectionManager {
|
||||
const userLevel = plDefault(users[ownUserId], usersDefault);
|
||||
const aclLevel = plDefault(events["m.room.server_acl"], stateDefault);
|
||||
|
||||
// Wants: ban, kick, redact, m.room.server_acl
|
||||
const addErrorToReport = (message: string) => {
|
||||
errors.push(new PermissionError(roomId, message))
|
||||
}
|
||||
|
||||
if (userLevel < ban) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for bans: ${userLevel} < ${ban}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
addErrorToReport(`Missing power level for bans: ${userLevel} < ${ban}`);
|
||||
}
|
||||
if (userLevel < kick) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for kicks: ${userLevel} < ${kick}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
addErrorToReport(`Missing power level for kicks: ${userLevel} < ${kick}`);
|
||||
}
|
||||
if (userLevel < redact) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for redactions: ${userLevel} < ${redact}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
addErrorToReport(`Missing power level for redactions: ${userLevel} < ${redact}`);
|
||||
}
|
||||
if (userLevel < aclLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for server ACLs: ${userLevel} < ${aclLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
if (!this.mjolnir.config.disableServerACL && userLevel < aclLevel) {
|
||||
addErrorToReport(`Missing power level for server ACLs: ${userLevel} < ${aclLevel}`);
|
||||
}
|
||||
|
||||
// Wants: Additional permissions
|
||||
@@ -469,24 +455,15 @@ export class ProtectionManager {
|
||||
const permLevel = plDefault(events[additionalPermission], stateDefault);
|
||||
|
||||
if (userLevel < permLevel) {
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: `Missing power level for "${additionalPermission}" state events: ${userLevel} < ${permLevel}`,
|
||||
errorKind: ERROR_KIND_PERMISSION,
|
||||
});
|
||||
addErrorToReport(`Missing power level for "${additionalPermission}" state events: ${userLevel} < ${permLevel}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise OK
|
||||
} catch (e) {
|
||||
LogService.error("Mjolnir", e);
|
||||
errors.push({
|
||||
roomId,
|
||||
errorMessage: e.message || (e.body ? e.body.error : '<no message>'),
|
||||
errorKind: ERROR_KIND_FATAL,
|
||||
});
|
||||
const message = `Unexpected error when attempting to verify the permissions in ${roomId}`;
|
||||
errors.push(new RoomUpdateException(roomId, CommandExceptionKind.Unknown, e, message));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ limitations under the License.
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
import { LogLevel, MatrixClient } from "matrix-bot-sdk"
|
||||
import { ERROR_KIND_FATAL } from "../ErrorCache";
|
||||
import { RoomUpdateError } from "../models/RoomUpdateError";
|
||||
import { IRoomUpdateError, RoomUpdateException } from "../models/RoomUpdateError";
|
||||
import { redactUserMessagesIn, trace } from "../utils";
|
||||
import ManagementRoomOutput from "../ManagementRoomOutput";
|
||||
import { MatrixSendClient } from "../MatrixEmitter";
|
||||
import { CommandExceptionKind } from "../commands/interface-manager/CommandException";
|
||||
|
||||
export interface QueuedRedaction {
|
||||
/** The room which the redaction will take place in. */
|
||||
@@ -124,25 +124,21 @@ export class EventRedactionQueue {
|
||||
* @returns A description of any errors encountered by each QueuedRedaction that was processed.
|
||||
*/
|
||||
@trace
|
||||
public async process(client: MatrixSendClient, managementRoom: ManagementRoomOutput, limitToRoomId?: string): Promise<RoomUpdateError[]> {
|
||||
const errors: RoomUpdateError[] = [];
|
||||
public async process(client: MatrixSendClient, managementRoom: ManagementRoomOutput, limitToRoomId?: string): Promise<IRoomUpdateError[]> {
|
||||
const errors: IRoomUpdateError[] = [];
|
||||
const redact = async (currentBatch: QueuedRedaction[]) => {
|
||||
for (const redaction of currentBatch) {
|
||||
try {
|
||||
await redaction.redact(client, managementRoom);
|
||||
} catch (e) {
|
||||
let roomError: RoomUpdateError;
|
||||
if (e.roomId && e.errorMessage && e.errorKind) {
|
||||
roomError = e;
|
||||
} else {
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
roomError = {
|
||||
roomId: redaction.roomId,
|
||||
errorMessage: message,
|
||||
errorKind: ERROR_KIND_FATAL,
|
||||
};
|
||||
}
|
||||
errors.push(roomError);
|
||||
const message = e.message || (e.body ? e.body.error : '<no message>');
|
||||
const error = new RoomUpdateException(
|
||||
redaction.roomId,
|
||||
CommandExceptionKind.Unknown,
|
||||
e,
|
||||
message
|
||||
);
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="mjolnir",
|
||||
version="1.84.0", # version automated in package.json - Do not edit this line, use `yarn version`.
|
||||
version="1.85.1", # version automated in package.json - Do not edit this line, use `yarn version`.
|
||||
packages=find_packages(),
|
||||
description="Mjolnir Antispam",
|
||||
include_package_data=True,
|
||||
|
||||
@@ -597,7 +597,7 @@ describe('Test: Creating policy lists.', function() {
|
||||
})
|
||||
})
|
||||
|
||||
describe.only('Test: Continue to ban other marked members when one member cannot be banned', function() {
|
||||
describe('Test: Continue to ban other marked members when one member cannot be banned', function() {
|
||||
it('Failing to ban a moderator should not stop other members being banned.', async function(this: MjolnirTestContext) {
|
||||
if (this.mjolnir === undefined) {
|
||||
throw new TypeError("Mjolnir was never created.")
|
||||
|
||||
@@ -54,5 +54,26 @@ describe("Ban propagation test", function() {
|
||||
expect(rules.length).toBe(1);
|
||||
expect(rules[0].entity).toBe('@test:example.com');
|
||||
expect(rules[0].reason).toBe('spam');
|
||||
|
||||
// now unban them >:3
|
||||
const unbanPrompt = await getFirstEventMatching({
|
||||
matrix: mjolnir.matrixEmitter,
|
||||
targetRoom: mjolnir.managementRoomId,
|
||||
lookAfterEvent: async function () {
|
||||
// ban a user in one of our protected rooms using the moderator
|
||||
await moderator.unbanUser('@test:example.com', protectedRooms[0]);
|
||||
return undefined;
|
||||
},
|
||||
predicate: function (event: any): boolean {
|
||||
return (event['content']?.['body'] ?? '').startsWith('The user')
|
||||
}
|
||||
});
|
||||
|
||||
await moderator.unstableApis.addReactionToEvent(
|
||||
mjolnir.managementRoomId, unbanPrompt['event_id'], 'unban from all'
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
const rulesAfterUnban = policyList.rulesMatchingEntity('@test:example.com', RULE_USER);
|
||||
expect(rulesAfterUnban.length).toBe(0);
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user