mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-01 18:05:44 +00:00
Compare commits
42 Commits
jade/loggi
...
ginger/upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b11af3e8 | ||
|
|
71a26e433f | ||
|
|
d353446488 | ||
|
|
77e8fd1744 | ||
|
|
7fa7b129c0 | ||
|
|
247bc15659 | ||
|
|
88a35e139d | ||
|
|
37574ef5cc | ||
|
|
1c816850ed | ||
|
|
3483059e1c | ||
|
|
d865dd4454 | ||
|
|
adc7c5ac49 | ||
|
|
112403e470 | ||
|
|
ea0a124981 | ||
|
|
bf205fb13c | ||
|
|
9a6408f98f | ||
|
|
ca77970ff3 | ||
|
|
42f4ec34cd | ||
|
|
ecf74bb31f | ||
|
|
8c716befdc | ||
|
|
a8209d1dd9 | ||
|
|
9552dd7485 | ||
|
|
88c84f221f | ||
|
|
a10bd71945 | ||
|
|
2f11bf4d74 | ||
|
|
1e8748d1a0 | ||
|
|
70ef6e4211 | ||
|
|
212c1bc14d | ||
|
|
ce46b6869f | ||
|
|
a18b8254d0 | ||
|
|
279f7cbfe4 | ||
|
|
006c57face | ||
|
|
d52e0dc014 | ||
|
|
4b873a1b95 | ||
|
|
76865e6f91 | ||
|
|
99f16c2dfc | ||
|
|
5ac82f36f3 | ||
|
|
c249dd992e | ||
|
|
0956779802 | ||
|
|
a83c1f1513 | ||
|
|
8b5e4d8fe1 | ||
|
|
7502a944d7 |
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
|
||||
image: ghcr.io/renovatebot/renovate:42.70.2@sha256:3c2ac1b94fa92ef2fa4d1a0493f2c3ba564454720a32fdbcac2db2846ff1ee47
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
persist-credentials: true
|
||||
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- uses: https://github.com/cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0
|
||||
- uses: https://github.com/cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
|
||||
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -1632,6 +1632,16 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "draupnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.0"
|
||||
@@ -2982,6 +2992,16 @@ version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "meowlnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -4065,11 +4085,13 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"draupnir-antispam",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"meowlnir-antispam",
|
||||
"ruma-appservice-api",
|
||||
"ruma-client-api",
|
||||
"ruma-common",
|
||||
@@ -4085,7 +4107,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.10.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4097,7 +4119,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.18.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4120,7 +4142,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
@@ -4152,7 +4174,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.28.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
@@ -4177,7 +4199,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4199,7 +4221,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4208,7 +4230,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4218,7 +4240,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
@@ -4233,7 +4255,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4245,7 +4267,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
|
||||
139
Cargo.toml
139
Cargo.toml
@@ -1,26 +1,17 @@
|
||||
#cargo-features = ["profile-rustflags"]
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["src/*", "xtask/*"]
|
||||
default-members = ["src/*"]
|
||||
|
||||
[workspace.package]
|
||||
authors = [
|
||||
"June Clementine Strawberry <june@girlboss.ceo>",
|
||||
"strawberry <strawberry@puppygock.gay>", # woof
|
||||
"Jason Volk <jason@zemos.net>",
|
||||
]
|
||||
categories = ["network-programming"]
|
||||
description = "a very cool Matrix chat homeserver written in Rust"
|
||||
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
|
||||
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
|
||||
edition = "2024"
|
||||
homepage = "https://continuwuity.org/"
|
||||
keywords = ["chat", "matrix", "networking", "server", "uwu"]
|
||||
license = "Apache-2.0"
|
||||
# See also `rust-toolchain.toml`
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
rust-version = "1.86.0"
|
||||
version = "0.5.1"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
@@ -33,11 +24,11 @@ features = ["serde"]
|
||||
[workspace.dependencies.smallvec]
|
||||
version = "1.14.0"
|
||||
features = [
|
||||
"const_generics",
|
||||
"const_new",
|
||||
"serde",
|
||||
"union",
|
||||
"write",
|
||||
"const_generics",
|
||||
"const_new",
|
||||
"serde",
|
||||
"union",
|
||||
"write",
|
||||
]
|
||||
|
||||
[workspace.dependencies.smallstr]
|
||||
@@ -96,13 +87,13 @@ version = "1.11.1"
|
||||
version = "0.7.9"
|
||||
default-features = false
|
||||
features = [
|
||||
"form",
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"matched-path",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"form",
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"matched-path",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
@@ -149,10 +140,10 @@ features = ["aws_lc_rs"]
|
||||
version = "0.12.15"
|
||||
default-features = false
|
||||
features = [
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"hickory-dns",
|
||||
"http2",
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"hickory-dns",
|
||||
"http2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
@@ -188,18 +179,18 @@ default-features = false
|
||||
version = "0.25.5"
|
||||
default-features = false
|
||||
features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
]
|
||||
|
||||
[workspace.dependencies.blurhash]
|
||||
version = "0.2.3"
|
||||
default-features = false
|
||||
features = [
|
||||
"fast-linear-to-srgb",
|
||||
"image",
|
||||
"fast-linear-to-srgb",
|
||||
"image",
|
||||
]
|
||||
|
||||
# logging
|
||||
@@ -229,13 +220,13 @@ default-features = false
|
||||
version = "4.5.35"
|
||||
default-features = false
|
||||
features = [
|
||||
"derive",
|
||||
"env",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"string",
|
||||
"usage",
|
||||
"derive",
|
||||
"env",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"string",
|
||||
"usage",
|
||||
]
|
||||
|
||||
[workspace.dependencies.futures]
|
||||
@@ -247,15 +238,15 @@ features = ["std", "async-await"]
|
||||
version = "1.44.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"fs",
|
||||
"net",
|
||||
"macros",
|
||||
"sync",
|
||||
"signal",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"tracing",
|
||||
"fs",
|
||||
"net",
|
||||
"macros",
|
||||
"sync",
|
||||
"signal",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[workspace.dependencies.tokio-metrics]
|
||||
@@ -280,18 +271,18 @@ default-features = false
|
||||
version = "1.6.0"
|
||||
default-features = false
|
||||
features = [
|
||||
"server",
|
||||
"http1",
|
||||
"http2",
|
||||
"server",
|
||||
"http1",
|
||||
"http2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.hyper-util]
|
||||
version = "=0.1.17"
|
||||
default-features = false
|
||||
features = [
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
"tokio",
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
# to support multiple variations of setting a config option
|
||||
@@ -310,9 +301,9 @@ features = ["env", "toml"]
|
||||
version = "0.25.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"serde",
|
||||
"system-config",
|
||||
"tokio",
|
||||
"serde",
|
||||
"system-config",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
# Used for conduwuit::Error type
|
||||
@@ -351,7 +342,7 @@ version = "0.1.2"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
rev = "79abd5d331bca596b7f37e367a9f2cebccd9f64d"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -381,13 +372,13 @@ features = [
|
||||
"unstable-msc4095",
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4186",
|
||||
"unstable-msc4203", # sending to-device events to appservices
|
||||
"unstable-msc4210", # remove legacy mentions
|
||||
"unstable-extensible-events",
|
||||
"unstable-pdu",
|
||||
"unstable-msc4155"
|
||||
"unstable-msc4155"
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
@@ -395,11 +386,11 @@ git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
|
||||
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
default-features = false
|
||||
features = [
|
||||
"multi-threaded-cf",
|
||||
"mt_static",
|
||||
"lz4",
|
||||
"zstd",
|
||||
"bzip2",
|
||||
"multi-threaded-cf",
|
||||
"mt_static",
|
||||
"lz4",
|
||||
"zstd",
|
||||
"bzip2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.sha2]
|
||||
@@ -458,16 +449,16 @@ git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
]
|
||||
[workspace.dependencies.tikv-jemallocator]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
]
|
||||
[workspace.dependencies.tikv-jemalloc-ctl]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
@@ -491,9 +482,9 @@ default-features = false
|
||||
version = "0.1.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"static",
|
||||
"gcc",
|
||||
"light",
|
||||
"static",
|
||||
"gcc",
|
||||
"light",
|
||||
]
|
||||
|
||||
[workspace.dependencies.rustyline-async]
|
||||
@@ -848,6 +839,8 @@ unknown_lints = "allow"
|
||||
|
||||
###################
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
# Nobody except for us should be consuming these crates, they don't need metadata
|
||||
cargo_common_metadata = { level = "allow"}
|
||||
|
||||
## some sadness
|
||||
multiple_crate_versions = { level = "allow", priority = 1 }
|
||||
|
||||
2
changelog.d/1263.feature
Normal file
2
changelog.d/1263.feature
Normal file
@@ -0,0 +1,2 @@
|
||||
Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
|
||||
Contributed by @nex.
|
||||
1
changelog.d/1266.feature
Normal file
1
changelog.d/1266.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implemented account locking functionality, to complement user suspension. Contributed by @nex.
|
||||
1
changelog.d/1271.feature
Normal file
1
changelog.d/1271.feature
Normal file
@@ -0,0 +1 @@
|
||||
Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex.
|
||||
1
changelog.d/1272.feature
Normal file
1
changelog.d/1272.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.
|
||||
1
changelog.d/783.feature.md
Normal file
1
changelog.d/783.feature.md
Normal file
@@ -0,0 +1 @@
|
||||
Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**.
|
||||
@@ -421,7 +421,7 @@
|
||||
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
#
|
||||
# If you would like registration only via token reg, please configure
|
||||
# `registration_token` or `registration_token_file`.
|
||||
# `registration_token`.
|
||||
#
|
||||
#allow_registration = false
|
||||
|
||||
@@ -452,22 +452,13 @@
|
||||
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
# to true to allow open registration without any conditions.
|
||||
#
|
||||
# YOU NEED TO EDIT THIS OR USE registration_token_file.
|
||||
# If you do not want to set a static token, the `!admin token` commands
|
||||
# may also be used to manage registration tokens.
|
||||
#
|
||||
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||
#
|
||||
#registration_token =
|
||||
|
||||
# Path to a file on the system that gets read for additional registration
|
||||
# tokens. Multiple tokens can be added if you separate them with
|
||||
# whitespace
|
||||
#
|
||||
# continuwuity must be able to access the file, and it must not be empty
|
||||
#
|
||||
# example: "/etc/continuwuity/.reg_token"
|
||||
#
|
||||
#registration_token_file =
|
||||
|
||||
# The public site key for reCaptcha. If this is provided, reCaptcha
|
||||
# becomes required during registration. If both captcha *and*
|
||||
# registration token are enabled, both will be required during
|
||||
@@ -1647,7 +1638,7 @@
|
||||
|
||||
# Enable the tokio-console. This option is only relevant to developers.
|
||||
#
|
||||
# For more information, see:
|
||||
# For more information, see:
|
||||
# https://continuwuity.org/development.html#debugging-with-tokio-console
|
||||
#
|
||||
#tokio_console = false
|
||||
@@ -1923,3 +1914,41 @@
|
||||
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
[global.antispam.meowlnir]
|
||||
|
||||
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||
#
|
||||
# Example: "http://127.0.0.1:29339"
|
||||
#
|
||||
#base_url =
|
||||
|
||||
# The authentication secret defined in antispam->secret. Required for
|
||||
# continuwuity to talk to Meowlnir.
|
||||
#
|
||||
#secret =
|
||||
|
||||
# The management room for which to send requests
|
||||
#
|
||||
#management_room =
|
||||
|
||||
# If enabled run all federated join attempts (both federated and local)
|
||||
# through the Meowlnir anti-spam checks.
|
||||
#
|
||||
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
|
||||
# restricted join rule are checked.
|
||||
#
|
||||
#check_all_joins = false
|
||||
|
||||
[global.antispam.draupnir]
|
||||
|
||||
# The base URL on which to contact Draupnir (before /api/).
|
||||
#
|
||||
# Example: "http://127.0.0.1:29339"
|
||||
#
|
||||
#base_url =
|
||||
|
||||
# The authentication secret defined in
|
||||
# web->synapseHTTPAntispam->authorization
|
||||
#
|
||||
#secret =
|
||||
|
||||
@@ -52,7 +52,7 @@ ENV BINSTALL_VERSION=1.16.6
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
ENV LDDTREE_VERSION=0.3.7
|
||||
ENV LDDTREE_VERSION=0.4.0
|
||||
# renovate: datasource=crate depName=timelord-cli
|
||||
ENV TIMELORD_VERSION=3.0.1
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ ENV BINSTALL_VERSION=1.16.6
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
ENV LDDTREE_VERSION=0.3.7
|
||||
ENV LDDTREE_VERSION=0.4.0
|
||||
|
||||
# Install unpackaged tools
|
||||
RUN <<EOF
|
||||
|
||||
@@ -11,10 +11,10 @@ ### Use a registry
|
||||
|
||||
| Registry | Image | Notes |
|
||||
| --------------- | --------------------------------------------------------------- | -----------------------|
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest][fj] | Latest tagged image. |
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main][fj] | Main branch image. |
|
||||
|
||||
[fj]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | Performance optimised version. |
|
||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | Performance optimised version. |
|
||||
|
||||
Use
|
||||
|
||||
@@ -24,6 +24,15 @@ ### Use a registry
|
||||
|
||||
to pull it to your machine.
|
||||
|
||||
#### Mirrors
|
||||
|
||||
Images are mirrored to multiple locations automatically, on a schedule:
|
||||
|
||||
- `ghcr.io/continuwuity/continuwuity`
|
||||
- `docker.io/jadedblueeyes/continuwuity`
|
||||
- `registry.gitlab.com/continuwuity/continuwuity`
|
||||
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
|
||||
|
||||
### Run
|
||||
|
||||
When you have the image, you can simply run it with
|
||||
@@ -49,7 +58,7 @@ ### Run
|
||||
flag, which cleans up everything related to your container after you stop
|
||||
it.
|
||||
|
||||
### Docker-compose
|
||||
### Docker Compose
|
||||
|
||||
If the `docker run` command is not suitable for you or your setup, you can also use one
|
||||
of the provided `docker-compose` files.
|
||||
@@ -158,8 +167,19 @@ # Build for the current platform and load into the local Docker daemon
|
||||
# Example: Build for specific platforms and push to a registry.
|
||||
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
|
||||
|
||||
# Example: Build binary optimized for the current CPU
|
||||
# docker buildx build --load --tag continuwuity:latest --build-arg TARGET_CPU=native -f docker/Dockerfile .
|
||||
# Example: Build binary optimised for the current CPU (standard release profile)
|
||||
# docker buildx build --load \
|
||||
# --tag continuwuity:latest \
|
||||
# --build-arg TARGET_CPU=native \
|
||||
# -f docker/Dockerfile .
|
||||
|
||||
# Example: Build maxperf variant (release-max-perf profile with LTO)
|
||||
# Optimised for runtime performance and smaller binary size, but requires longer build time
|
||||
# docker buildx build --load \
|
||||
# --tag continuwuity:latest-maxperf \
|
||||
# --build-arg TARGET_CPU=native \
|
||||
# --build-arg RUST_PROFILE=release-max-perf \
|
||||
# -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
Refer to the Docker Buildx documentation for more advanced build options.
|
||||
@@ -198,5 +218,3 @@ ### Use Traefik as Proxy
|
||||
## Voice communication
|
||||
|
||||
See the [TURN](../turn.md) page.
|
||||
|
||||
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage
|
||||
|
||||
@@ -8,29 +8,39 @@ # Generic deployment documentation
|
||||
|
||||
## Installing Continuwuity
|
||||
|
||||
### Static prebuilt binary
|
||||
### Prebuilt binary
|
||||
|
||||
You may simply download the binary that fits your machine architecture (x86_64
|
||||
or aarch64). Run `uname -m` to see what you need.
|
||||
Download the binary for your architecture (x86_64 or aarch64) -
|
||||
run the `uname -m` to check which you need.
|
||||
|
||||
You can download prebuilt fully static musl binaries from the latest tagged
|
||||
release [here](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest) or
|
||||
from the `main` CI branch workflow artifact output. These also include Debian/Ubuntu
|
||||
packages.
|
||||
Prebuilt binaries are available from:
|
||||
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
|
||||
- **Development builds**: CI artifacts from the `main` branch
|
||||
(includes Debian/Ubuntu packages)
|
||||
|
||||
You can download these directly using curl. The `ci-bins` are CI workflow binaries organized by commit
|
||||
hash/revision, and `releases` are tagged releases. Sort by descending last
|
||||
modified date to find the latest.
|
||||
When browsing CI artifacts, `ci-bins` contains binaries organised
|
||||
by commit hash, while `releases` contains tagged versions. Sort
|
||||
by last modified date to find the most recent builds.
|
||||
|
||||
These binaries have jemalloc and io_uring statically linked and included with
|
||||
them, so no additional dynamic dependencies need to be installed.
|
||||
The binaries require jemalloc and io_uring on the host system. Currently
|
||||
we can't cross-build static binaries - contributions are welcome here.
|
||||
|
||||
For the **best** performance: if you are using an `x86_64` CPU made in the last ~15 years,
|
||||
we recommend using the `-haswell-` optimized binaries. These set
|
||||
`-march=haswell`, which provides the most compatible and highest performance with
|
||||
optimized binaries. The database backend, RocksDB, benefits most from this as it
|
||||
uses hardware-accelerated CRC32 hashing/checksumming, which is critical
|
||||
for performance.
|
||||
#### Performance-optimised builds
|
||||
|
||||
For x86_64 systems with CPUs from the last ~15 years, use the
|
||||
`-haswell-` optimised binaries for best performance. These
|
||||
binaries enable hardware-accelerated CRC32 checksumming in
|
||||
RocksDB, which significantly improves database performance.
|
||||
The haswell instruction set provides an excellent balance of
|
||||
compatibility and speed.
|
||||
|
||||
If you're using Docker instead, equivalent performance-optimised
|
||||
images are available with the `-maxperf` suffix (e.g.
|
||||
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
|
||||
These images use the `release-max-perf`
|
||||
build profile with
|
||||
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
|
||||
and, for amd64, target the haswell CPU architecture.
|
||||
|
||||
### Compiling
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ # Command-Line Help for `continuwuity`
|
||||
|
||||
## `continuwuity`
|
||||
|
||||
a very cool Matrix chat homeserver written in Rust
|
||||
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
|
||||
|
||||
**Usage:** `continuwuity [OPTIONS]`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Name: continuwuity
|
||||
Version: {{{ git_repo_version }}}
|
||||
Release: 1%{?dist}
|
||||
Summary: Very cool Matrix chat homeserver written in Rust
|
||||
Summary: A Matrix homeserver written in Rust.
|
||||
|
||||
License: Apache-2.0 AND MIT
|
||||
|
||||
@@ -23,7 +23,7 @@ Requires: glibc
|
||||
Requires: libstdc++
|
||||
|
||||
%global _description %{expand:
|
||||
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
|
||||
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
|
||||
|
||||
%description %{_description}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_admin"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::{
|
||||
appservice, appservice::AppserviceCommand, check, check::CheckCommand, context::Context,
|
||||
debug, debug::DebugCommand, federation, federation::FederationCommand, media,
|
||||
media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
|
||||
server::ServerCommand, user, user::UserCommand,
|
||||
appservice::{self, AppserviceCommand},
|
||||
check::{self, CheckCommand},
|
||||
context::Context,
|
||||
debug::{self, DebugCommand},
|
||||
federation::{self, FederationCommand},
|
||||
media::{self, MediaCommand},
|
||||
query::{self, QueryCommand},
|
||||
room::{self, RoomCommand},
|
||||
server::{self, ServerCommand},
|
||||
token::{self, TokenCommand},
|
||||
user::{self, UserCommand},
|
||||
};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -19,6 +26,10 @@ pub enum AdminCommand {
|
||||
/// - Commands for managing local users
|
||||
Users(UserCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing registration tokens
|
||||
Token(TokenCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing rooms
|
||||
Rooms(RoomCommand),
|
||||
@@ -64,6 +75,11 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
||||
context.bail_restricted()?;
|
||||
user::process(command, context).await
|
||||
},
|
||||
| Token(command) => {
|
||||
// token commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
token::process(command, context).await
|
||||
},
|
||||
| Rooms(command) => room::process(command, context).await,
|
||||
| Federation(command) => federation::process(command, context).await,
|
||||
| Server(command) => server::process(command, context).await,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod token;
|
||||
pub(crate) mod user;
|
||||
|
||||
extern crate conduwuit_api as api;
|
||||
|
||||
76
src/admin/token/commands.rs
Normal file
76
src/admin/token/commands.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use conduwuit_macros::admin_command;
|
||||
use futures::StreamExt;
|
||||
use service::registration_tokens::TokenExpires;
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
|
||||
let expires = {
|
||||
if expires.immortal {
|
||||
None
|
||||
} else if let Some(max_uses) = expires.max_uses {
|
||||
Some(TokenExpires::AfterUses(max_uses))
|
||||
} else if expires.once {
|
||||
Some(TokenExpires::AfterUses(1))
|
||||
} else if let Some(max_age) = expires
|
||||
.max_age
|
||||
.as_deref()
|
||||
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
|
||||
.transpose()?
|
||||
{
|
||||
Some(TokenExpires::AfterTime(max_age))
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let (token, info) = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.issue_token(self.sender_or_service_user().into(), expires);
|
||||
|
||||
self.write_str(&format!(
|
||||
"New registration token issued: `{token}`. {}.",
|
||||
if let Some(expires) = info.expires {
|
||||
format!("{expires}")
|
||||
} else {
|
||||
"Never expires".to_owned()
|
||||
}
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn revoke_token(&self, token: String) -> Result {
|
||||
let Some(token) = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.validate_token(token)
|
||||
.await
|
||||
else {
|
||||
return Err!("This token does not exist or has already expired.");
|
||||
};
|
||||
|
||||
self.services.registration_tokens.revoke_token(token)?;
|
||||
|
||||
self.write_str("Token revoked successfully.").await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_tokens(&self) -> Result {
|
||||
let tokens: Vec<_> = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
|
||||
.await?;
|
||||
|
||||
for token in tokens {
|
||||
self.write_str(&format!("- {token}\n")).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
51
src/admin/token/mod.rs
Normal file
51
src/admin/token/mod.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
mod commands;
|
||||
|
||||
use clap::{Args, Subcommand};
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::admin_command_dispatch;
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum TokenCommand {
|
||||
/// - Issue a new registration token
|
||||
#[clap(name = "issue")]
|
||||
IssueToken {
|
||||
/// When this token will expire.
|
||||
#[command(flatten)]
|
||||
expires: TokenExpires,
|
||||
},
|
||||
|
||||
/// - Revoke a registration token
|
||||
#[clap(name = "revoke")]
|
||||
RevokeToken {
|
||||
/// The token to revoke.
|
||||
token: String,
|
||||
},
|
||||
|
||||
/// - List all registration tokens
|
||||
#[clap(name = "list")]
|
||||
ListTokens,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct TokenExpires {
|
||||
/// The maximum number of times this token is allowed to be used before it
|
||||
/// expires.
|
||||
#[arg(long)]
|
||||
max_uses: Option<u64>,
|
||||
|
||||
/// The maximum age of this token (e.g. 30s, 5m, 7d). It will expire after
|
||||
/// this much time has passed.
|
||||
#[arg(long)]
|
||||
max_age: Option<String>,
|
||||
|
||||
/// This token will never expire.
|
||||
#[arg(long)]
|
||||
immortal: bool,
|
||||
|
||||
/// A shortcut for `--max-uses 1`.
|
||||
#[arg(long)]
|
||||
once: bool,
|
||||
}
|
||||
@@ -238,6 +238,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
@@ -262,6 +263,7 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
@@ -278,7 +280,12 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
|
||||
pub(super) async fn reset_password(
|
||||
&self,
|
||||
logout: bool,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
@@ -301,7 +308,18 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
|
||||
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
|
||||
},
|
||||
}
|
||||
.await
|
||||
.await?;
|
||||
|
||||
if logout {
|
||||
self.services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
|
||||
.await;
|
||||
write!(self, "\nAll existing sessions have been logged out.").await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
@@ -974,3 +992,113 @@ pub(super) async fn force_leave_remote_room(
|
||||
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to lock the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("Admin users cannot be locked.");
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.lock_account(&user_id, self.sender_or_service_user())
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been locked."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
self.services.users.unlock_account(&user_id).await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been unlocked."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to log out the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("You cannot forcefully log out admin users.");
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
|
||||
.await;
|
||||
self.write_str(&format!("User {user_id} has been logged out from all devices."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to disable login for the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("Admin users cannot have their login disallowed.");
|
||||
}
|
||||
self.services.users.disable_login(&user_id);
|
||||
|
||||
self.write_str(&format!(
|
||||
"{user_id} can no longer log in. Their existing sessions remain unaffected."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
self.services.users.enable_login(&user_id);
|
||||
|
||||
self.write_str(&format!("{user_id} can now log in.")).await
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ pub enum UserCommand {
|
||||
|
||||
/// - Reset user password
|
||||
ResetPassword {
|
||||
/// Log out existing sessions
|
||||
#[arg(short, long)]
|
||||
logout: bool,
|
||||
/// Username of the user for whom the password should be reset
|
||||
username: String,
|
||||
/// New password for the user, if unspecified one is generated
|
||||
@@ -59,6 +62,18 @@ pub enum UserCommand {
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// - Forcefully log a user out of all of their devices.
|
||||
///
|
||||
/// This will invalidate all access tokens for the specified user,
|
||||
/// effectively logging them out from all sessions.
|
||||
/// Note that this is destructive and may result in data loss for the user,
|
||||
/// such as encryption keys. Use with caution. Can only be used in the admin
|
||||
/// room.
|
||||
Logout {
|
||||
/// Username of the user to log out
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - Suspend a user
|
||||
///
|
||||
/// Suspended users are able to log in, sync, and read messages, but are not
|
||||
@@ -81,6 +96,42 @@ pub enum UserCommand {
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - Lock a user
|
||||
///
|
||||
/// Locked users are unable to use their accounts beyond logging out. This
|
||||
/// is akin to a temporary deactivation that does not change the user's
|
||||
/// password. This can be used to quickly prevent a user from accessing
|
||||
/// their account.
|
||||
Lock {
|
||||
/// Username of the user to lock
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - Unlock a user
|
||||
///
|
||||
/// Reverses the effects of the `lock` command, allowing the user to use
|
||||
/// their account again.
|
||||
Unlock {
|
||||
/// Username of the user to unlock
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - Enable login for a user
|
||||
EnableLogin {
|
||||
/// Username of the user to enable login for
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - Disable login for a user
|
||||
///
|
||||
/// Disables login for the specified user without deactivating or locking
|
||||
/// their account. This prevents the user from obtaining new access tokens,
|
||||
/// but does not invalidate existing sessions.
|
||||
DisableLogin {
|
||||
/// Username of the user to disable login for
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// - List local users in the database
|
||||
#[clap(alias = "list")]
|
||||
ListUsers,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_api"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -179,13 +179,18 @@ pub(crate) async fn register_route(
|
||||
},
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden("Registration has been disabled.")));
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest
|
||||
&& (!services.config.allow_guest_registration
|
||||
|| (services.config.allow_registration
|
||||
&& services.globals.registration_token.is_some()))
|
||||
&& services
|
||||
.registration_tokens
|
||||
.get_config_file_token()
|
||||
.is_some()))
|
||||
{
|
||||
info!(
|
||||
"Guest registration disabled / registration enabled with token configured, \
|
||||
@@ -203,7 +208,9 @@ pub(crate) async fn register_route(
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
let user_id = match (body.username.as_ref(), is_guest) {
|
||||
@@ -301,7 +308,13 @@ pub(crate) async fn register_route(
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
// Populate required UIAA flows
|
||||
if services.globals.registration_token.is_some() {
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Registration token required
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
@@ -323,7 +336,19 @@ pub(crate) async fn register_route(
|
||||
}
|
||||
|
||||
if uiaainfo.flows.is_empty() && !skip_auth {
|
||||
// No registration token necessary, but clients must still go through the flow
|
||||
// Registration isn't _disabled_, but there's no captcha configured and no
|
||||
// registration tokens currently set. Bail out by default unless open
|
||||
// registration was explicitly enabled.
|
||||
if !services
|
||||
.config
|
||||
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// We have open registration enabled (😧), provide a dummy stage
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
||||
completed: Vec::new(),
|
||||
@@ -846,19 +871,20 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking
|
||||
///
|
||||
/// Currently does not have any ratelimiting, and this isn't very practical as
|
||||
/// there is only one registration token allowed.
|
||||
/// Checks if the provided registration token is valid at the time of checking.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
let Some(reg_token) = services.globals.registration_token.clone() else {
|
||||
return Err!(Request(Forbidden("Server does not allow token registration")));
|
||||
};
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid })
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use conduwuit::{
|
||||
Err, Result, debug_error, err, info,
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||
warn,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
@@ -124,6 +125,18 @@ pub(crate) async fn invite_helper(
|
||||
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
.antispam
|
||||
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
|
||||
sender_user, recipient_user, room_id
|
||||
);
|
||||
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
|
||||
}
|
||||
|
||||
if !services.globals.user_is_local(recipient_user) {
|
||||
let (pdu, pdu_json, invite_room_state) = {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
|
||||
join_rules::{AllowRule, JoinRule},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
},
|
||||
@@ -288,6 +288,23 @@ pub async fn join_room_by_id_helper(
|
||||
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
.antispam
|
||||
.user_may_join_room(
|
||||
sender_user.to_owned(),
|
||||
room_id.to_owned(),
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_invited(sender_user, room_id)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
|
||||
return Err!(Request(Forbidden("You are not allowed to join this room.")));
|
||||
}
|
||||
|
||||
let server_in_room = services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -321,6 +338,17 @@ pub async fn join_room_by_id_helper(
|
||||
)));
|
||||
}
|
||||
|
||||
if services.antispam.check_all_joins() {
|
||||
if let Err(e) = services
|
||||
.antispam
|
||||
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
|
||||
.await
|
||||
{
|
||||
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
|
||||
return Err!(Request(Forbidden("Antispam rejected join request.")));
|
||||
}
|
||||
}
|
||||
|
||||
if server_in_room {
|
||||
join_room_by_id_helper_local(
|
||||
services,
|
||||
@@ -347,7 +375,6 @@ pub async fn join_room_by_id_helper(
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
|
||||
}
|
||||
|
||||
@@ -720,45 +747,51 @@ async fn join_room_by_id_helper_local(
|
||||
state_lock: RoomMutexGuard,
|
||||
) -> Result {
|
||||
debug_info!("We can join locally");
|
||||
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
|
||||
|
||||
let join_rules_event_content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomJoinRulesEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomJoinRules,
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
let restriction_rooms = match join_rules_event_content {
|
||||
| Ok(RoomJoinRulesEventContent {
|
||||
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
|
||||
}) => restricted
|
||||
.allow
|
||||
.into_iter()
|
||||
.filter_map(|a| match a {
|
||||
| AllowRule::RoomMembership(r) => Some(r.room_id),
|
||||
| _ => None,
|
||||
})
|
||||
.collect(),
|
||||
| _ => Vec::new(),
|
||||
};
|
||||
|
||||
let join_authorized_via_users_server: Option<OwnedUserId> = {
|
||||
if restriction_rooms
|
||||
.iter()
|
||||
.stream()
|
||||
.any(|restriction_room_id| {
|
||||
trace!("Checking if {sender_user} is joined to {restriction_room_id}");
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, restriction_room_id)
|
||||
})
|
||||
.await
|
||||
{
|
||||
services
|
||||
let mut restricted_join_authorized = None;
|
||||
match join_rules {
|
||||
| JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
|
||||
for restriction in restricted.allow {
|
||||
match restriction {
|
||||
| AllowRule::RoomMembership(membership) => {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &membership.room_id)
|
||||
.await
|
||||
{
|
||||
restricted_join_authorized = Some(true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
| AllowRule::UnstableSpamChecker => {
|
||||
match services
|
||||
.antispam
|
||||
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
|
||||
.await
|
||||
{
|
||||
| Ok(()) => {
|
||||
restricted_join_authorized = Some(true);
|
||||
break;
|
||||
},
|
||||
| Err(_) =>
|
||||
return Err!(Request(Forbidden(
|
||||
"Antispam rejected join request."
|
||||
))),
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
|
||||
None
|
||||
} else {
|
||||
match restricted_join_authorized.unwrap() {
|
||||
| true => services
|
||||
.rooms
|
||||
.state_cache
|
||||
.local_users_in_room(room_id)
|
||||
@@ -774,10 +807,14 @@ async fn join_room_by_id_helper_local(
|
||||
.boxed()
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned)
|
||||
} else {
|
||||
trace!("No restriction rooms are joined by {sender_user}");
|
||||
None
|
||||
.map(ToOwned::to_owned),
|
||||
| false => {
|
||||
warn!(
|
||||
"Join authorization failed for restricted join in room {room_id} for user \
|
||||
{sender_user}"
|
||||
);
|
||||
return Err!(Request(Forbidden("You are not authorized to join this room.")));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -805,16 +842,14 @@ async fn join_room_by_id_helper_local(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if restriction_rooms.is_empty()
|
||||
&& (servers.is_empty()
|
||||
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
|
||||
{
|
||||
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
warn!(
|
||||
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
|
||||
join requirements"
|
||||
?error,
|
||||
servers = %servers.len(),
|
||||
"Could not join restricted room locally, attempting remote join",
|
||||
);
|
||||
let Ok((make_join_response, remote_server)) =
|
||||
make_join_request(services, sender_user, room_id, servers).await
|
||||
|
||||
@@ -178,7 +178,20 @@ pub async fn leave_room(
|
||||
.rooms
|
||||
.state_cache
|
||||
.left_state(user_id, room_id)
|
||||
.await?
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
// `left_state` may return an Err if the user _is_ in the room they're
|
||||
// trying to leave, but the membership cache is incorrect and
|
||||
// they're cached as being joined. In this situation
|
||||
// we save a `None` to the `roomuserid_leftcount` table, which generates
|
||||
// and sends a dummy leave to the client.
|
||||
warn!(
|
||||
?err,
|
||||
"Trying to leave room not cached as leave, sending dummy leave \
|
||||
event to client"
|
||||
);
|
||||
})
|
||||
.unwrap_or_default()
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, err, info,
|
||||
utils::{self, ReadyExt, hash},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_core::{debug_error, debug_warn};
|
||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||
@@ -12,6 +13,7 @@
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
session::{
|
||||
get_login_token,
|
||||
get_login_types::{
|
||||
@@ -184,6 +186,15 @@ pub(crate) async fn handle_login(
|
||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&user_id).await? {
|
||||
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
|
||||
}
|
||||
|
||||
if services.users.is_login_disabled(&user_id).await {
|
||||
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
||||
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
||||
}
|
||||
|
||||
if cfg!(feature = "ldap") && services.config.ldap.enable {
|
||||
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
|
||||
| Ok(user_id) => Ok(user_id),
|
||||
|
||||
@@ -137,12 +137,30 @@ pub(super) async fn auth(
|
||||
| (
|
||||
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
|
||||
Token::User((user_id, device_id)),
|
||||
) => Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: Some(user_id),
|
||||
sender_device: Some(device_id),
|
||||
appservice_info: None,
|
||||
}),
|
||||
) => {
|
||||
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
|
||||
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
|
||||
})?;
|
||||
if is_locked {
|
||||
// Only /logout and /logout/all are allowed for locked users
|
||||
if !matches!(
|
||||
metadata,
|
||||
&ruma::api::client::session::logout::v3::Request::METADATA
|
||||
| &ruma::api::client::session::logout_all::v3::Request::METADATA
|
||||
) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UserLocked,
|
||||
"This account has been locked.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Auth {
|
||||
origin: None,
|
||||
sender_user: Some(user_id),
|
||||
sender_device: Some(device_id),
|
||||
appservice_info: None,
|
||||
})
|
||||
},
|
||||
| (AuthScheme::ServerSignatures, Token::None) =>
|
||||
Ok(auth_server(services, request, json_body).await?),
|
||||
| (
|
||||
|
||||
@@ -148,6 +148,15 @@ pub(crate) async fn create_invite_route(
|
||||
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
.antispam
|
||||
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
|
||||
.await
|
||||
{
|
||||
warn!("Antispam rejected invite: {e:?}");
|
||||
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
|
||||
}
|
||||
|
||||
let mut invite_state = body.invite_room_state.clone();
|
||||
|
||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
|
||||
};
|
||||
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
@@ -122,6 +122,16 @@ pub(crate) async fn create_join_event_template_route(
|
||||
None
|
||||
}
|
||||
};
|
||||
if services.antispam.check_all_joins() && join_authorized_via_users_server.is_none() {
|
||||
if services
|
||||
.antispam
|
||||
.meowlnir_accept_make_join(body.room_id.clone(), body.user_id.clone())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden("Antispam rejected join request.")));
|
||||
}
|
||||
}
|
||||
|
||||
let (_pdu, mut pdu_json) = services
|
||||
.rooms
|
||||
@@ -136,7 +146,6 @@ pub(crate) async fn create_join_event_template_route(
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
// room v3 and above removed the "event_id" field from remote PDU format
|
||||
@@ -192,25 +201,44 @@ pub(crate) async fn user_can_perform_restricted_join(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if r.allow
|
||||
.iter()
|
||||
.filter_map(|rule| {
|
||||
if let AllowRule::RoomMembership(membership) = rule {
|
||||
Some(membership)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.stream()
|
||||
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id))
|
||||
.await
|
||||
{
|
||||
Ok(true)
|
||||
} else {
|
||||
Err!(Request(UnableToAuthorizeJoin(
|
||||
"Joining user is not known to be in any required room."
|
||||
)))
|
||||
for allow_rule in &r.allow {
|
||||
match allow_rule {
|
||||
| AllowRule::RoomMembership(membership) => {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(user_id, &membership.room_id)
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
"User {} is allowed to join room {} via membership in room {}",
|
||||
user_id, room_id, membership.room_id
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
},
|
||||
| AllowRule::UnstableSpamChecker =>
|
||||
return match services
|
||||
.antispam
|
||||
.meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned())
|
||||
.await
|
||||
{
|
||||
| Ok(()) => Ok(true),
|
||||
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
|
||||
},
|
||||
| _ => {
|
||||
debug_info!(
|
||||
"Unsupported allow rule in restricted join for room {}: {:?}",
|
||||
room_id,
|
||||
allow_rule
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err!(Request(UnableToAuthorizeJoin(
|
||||
"Joining user is not known to be in any required room."
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_strip_event_id(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_build_metadata"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_core"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -146,22 +146,6 @@ pub fn check(config: &Config) -> Result {
|
||||
));
|
||||
}
|
||||
|
||||
// check if we can read the token file path, and check if the file is empty
|
||||
if config.registration_token_file.as_ref().is_some_and(|path| {
|
||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
||||
error!("Failed to read the registration token file: {e}");
|
||||
}) else {
|
||||
return true;
|
||||
};
|
||||
|
||||
token == String::new()
|
||||
}) {
|
||||
return Err!(Config(
|
||||
"registration_token_file",
|
||||
"Registration token file was specified but is empty or failed to be read"
|
||||
));
|
||||
}
|
||||
|
||||
if config.max_request_size < 10_000_000 {
|
||||
return Err!(Config(
|
||||
"max_request_size",
|
||||
@@ -187,29 +171,9 @@ pub fn check(config: &Config) -> Result {
|
||||
));
|
||||
}
|
||||
|
||||
if config.allow_registration
|
||||
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
&& config.registration_token.is_none()
|
||||
&& config.registration_token_file.is_none()
|
||||
&& config.recaptcha_site_key.is_none()
|
||||
{
|
||||
return Err!(Config(
|
||||
"registration_token",
|
||||
"!! You have `allow_registration` enabled without a token or captcha configured \
|
||||
which means you are allowing ANYONE to register on your continuwuity instance \
|
||||
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
|
||||
abused by malicious actors. If this is not the intended behaviour, please set a \
|
||||
registration token. For security and safety reasons, continuwuity will shut down. \
|
||||
If you are extra sure this is the desired behaviour you want, please set the \
|
||||
following config option to true:
|
||||
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
|
||||
));
|
||||
}
|
||||
|
||||
if config.allow_registration
|
||||
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
&& config.registration_token.is_none()
|
||||
&& config.registration_token_file.is_none()
|
||||
{
|
||||
warn!(
|
||||
"Open registration is enabled via setting \
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
pub use figment::{Figment, value::Value as FigmentValue};
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
api::client::discovery::discover_support::ContactRole,
|
||||
};
|
||||
use serde::{Deserialize, de::IgnoredAny};
|
||||
@@ -53,7 +53,8 @@
|
||||
### For more information, see:
|
||||
### https://continuwuity.org/configuration.html
|
||||
"#,
|
||||
ignore = "config_paths catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
|
||||
ignore = "config_paths catchall well_known tls blurhashing \
|
||||
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
|
||||
)]
|
||||
pub struct Config {
|
||||
// Paths to config file(s). Not supposed to be set manually in the config file,
|
||||
@@ -544,7 +545,7 @@ pub struct Config {
|
||||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
///
|
||||
/// If you would like registration only via token reg, please configure
|
||||
/// `registration_token` or `registration_token_file`.
|
||||
/// `registration_token`.
|
||||
#[serde(default)]
|
||||
pub allow_registration: bool,
|
||||
|
||||
@@ -575,22 +576,14 @@ pub struct Config {
|
||||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
||||
/// to true to allow open registration without any conditions.
|
||||
///
|
||||
/// YOU NEED TO EDIT THIS OR USE registration_token_file.
|
||||
/// If you do not want to set a static token, the `!admin token` commands
|
||||
/// may also be used to manage registration tokens.
|
||||
///
|
||||
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||
///
|
||||
/// display: sensitive
|
||||
pub registration_token: Option<String>,
|
||||
|
||||
/// Path to a file on the system that gets read for additional registration
|
||||
/// tokens. Multiple tokens can be added if you separate them with
|
||||
/// whitespace
|
||||
///
|
||||
/// continuwuity must be able to access the file, and it must not be empty
|
||||
///
|
||||
/// example: "/etc/continuwuity/.reg_token"
|
||||
pub registration_token_file: Option<PathBuf>,
|
||||
|
||||
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
||||
/// becomes required during registration. If both captcha *and*
|
||||
/// registration token are enabled, both will be required during
|
||||
@@ -1887,7 +1880,7 @@ pub struct Config {
|
||||
|
||||
/// Enable the tokio-console. This option is only relevant to developers.
|
||||
///
|
||||
/// For more information, see:
|
||||
/// For more information, see:
|
||||
/// https://continuwuity.org/development.html#debugging-with-tokio-console
|
||||
#[serde(default)]
|
||||
pub tokio_console: bool,
|
||||
@@ -2024,6 +2017,10 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub ldap: LdapConfig,
|
||||
|
||||
/// Configuration for antispam support
|
||||
#[serde(default)]
|
||||
pub antispam: Option<Antispam>,
|
||||
|
||||
// external structure; separate section
|
||||
#[serde(default)]
|
||||
pub blurhashing: BlurhashConfig,
|
||||
@@ -2240,7 +2237,58 @@ struct ListeningAddr {
|
||||
addrs: Either<IpAddr, Vec<IpAddr>>,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str; 9] = &[
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Antispam {
|
||||
pub meowlnir: Option<MeowlnirConfig>,
|
||||
pub draupnir: Option<DraupnirConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam.meowlnir"
|
||||
)]
|
||||
pub struct MeowlnirConfig {
|
||||
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||
///
|
||||
/// Example: "http://127.0.0.1:29339"
|
||||
pub base_url: Url,
|
||||
|
||||
/// The authentication secret defined in antispam->secret. Required for
|
||||
/// continuwuity to talk to Meowlnir.
|
||||
pub secret: String,
|
||||
|
||||
/// The management room for which to send requests
|
||||
pub management_room: OwnedRoomId,
|
||||
|
||||
/// If enabled run all federated join attempts (both federated and local)
|
||||
/// through the Meowlnir anti-spam checks.
|
||||
///
|
||||
/// By default, only join attempts for rooms with the `fi.mau.spam_checker`
|
||||
/// restricted join rule are checked.
|
||||
#[serde(default)]
|
||||
pub check_all_joins: bool,
|
||||
}
|
||||
|
||||
// TODO: the DraupnirConfig and MeowlnirConfig are basically identical.
|
||||
// Maybe management_room could just become an Option<> and these structs merged?
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam.draupnir"
|
||||
)]
|
||||
pub struct DraupnirConfig {
|
||||
/// The base URL on which to contact Draupnir (before /api/).
|
||||
///
|
||||
/// Example: "http://127.0.0.1:29339"
|
||||
pub base_url: Url,
|
||||
|
||||
/// The authentication secret defined in
|
||||
/// web->synapseHTTPAntispam->authorization
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
"max_concurrent_requests",
|
||||
@@ -2250,6 +2298,7 @@ struct ListeningAddr {
|
||||
"well_known_support_role",
|
||||
"well_known_support_email",
|
||||
"well_known_support_mxid",
|
||||
"registration_token_file",
|
||||
];
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -75,10 +75,12 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
| ThreepidDenied
|
||||
| InviteBlocked
|
||||
| WrongRoomKeysVersion { .. }
|
||||
| UserSuspended
|
||||
| Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||
|
||||
// 401
|
||||
| UnknownToken { .. } | MissingToken | Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
|
||||
StatusCode::UNAUTHORIZED,
|
||||
|
||||
// 400
|
||||
| _ => StatusCode::BAD_REQUEST,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_database"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -141,6 +141,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "referencedevents",
|
||||
..descriptor::RANDOM
|
||||
},
|
||||
Descriptor {
|
||||
name: "registrationtoken_info",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomid_invitedcount",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -386,6 +390,14 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "userid_suspension",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_lock",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_logindisabled",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_presenceid",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_macros"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
name = "conduwuit"
|
||||
default-run = "conduwuit"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
metadata.crane.workspace = true
|
||||
|
||||
@@ -23,14 +20,13 @@ crate-type = [
|
||||
|
||||
[package.metadata.deb]
|
||||
name = "continuwuity"
|
||||
maintainer = "continuwuity developers <contact@continuwuity.org>"
|
||||
copyright = "2024, continuwuity developers"
|
||||
maintainer = "Continuwuity Team and contributors <team@continuwuity.org>"
|
||||
license-file = ["../../LICENSE", "3"]
|
||||
depends = "$auto, ca-certificates"
|
||||
breaks = ["conduwuit (<<0.5.0)"]
|
||||
replaces = ["conduwuit (<<0.5.0)"]
|
||||
extended-description = """\
|
||||
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
|
||||
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."""
|
||||
section = "net"
|
||||
priority = "optional"
|
||||
conf-files = ["/etc/conduwuit/conduwuit.toml"]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_router"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_service"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
182
src/service/antispam/mod.rs
Normal file
182
src/service/antispam/mod.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use conduwuit::{Result, config::Antispam, debug};
|
||||
use ruma::{OwnedRoomId, OwnedUserId, draupnir_antispam, meowlnir_antispam};
|
||||
|
||||
use crate::{client, config, sending, service::Dep};
|
||||
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
client: Dep<client::Service>,
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
services: Services,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
client: args.depend::<client::Service>("client"),
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
async fn send_antispam_request<T>(
|
||||
&self,
|
||||
base_url: &str,
|
||||
secret: &str,
|
||||
request: T,
|
||||
) -> Result<T::IncomingResponse>
|
||||
where
|
||||
T: ruma::api::OutgoingRequest + Debug + Send,
|
||||
{
|
||||
sending::antispam::send_antispam_request(
|
||||
&self.services.client.appservice,
|
||||
base_url,
|
||||
secret,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Checks with the antispam service whether `inviter` may invite `invitee`
|
||||
/// to `room_id`.
|
||||
///
|
||||
/// If no antispam service is configured, this always returns `Ok(())`.
|
||||
/// If an error is returned, the invite should be blocked - the antispam
|
||||
/// service was unreachable, or refused the invite.
|
||||
pub async fn user_may_invite(
|
||||
&self,
|
||||
inviter: OwnedUserId,
|
||||
invitee: OwnedUserId,
|
||||
room_id: OwnedRoomId,
|
||||
) -> Result<()> {
|
||||
if let Some(config) = &self.services.config.antispam {
|
||||
let result = if let Some(meowlnir) = &config.meowlnir {
|
||||
debug!(?room_id, ?inviter, ?invitee, "Asking meowlnir for user_may_invite");
|
||||
self.send_antispam_request(
|
||||
meowlnir.base_url.as_str(),
|
||||
&meowlnir.secret,
|
||||
meowlnir_antispam::user_may_invite::v1::Request::new(
|
||||
meowlnir.management_room.clone(),
|
||||
inviter,
|
||||
invitee,
|
||||
room_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.inspect(|_| debug!("meowlnir allowed the invite"))
|
||||
.inspect_err(|e| debug!("meowlnir denied the invite: {e:?}"))
|
||||
.map(|_| ())
|
||||
} else if let Some(draupnir) = &config.draupnir {
|
||||
debug!(?room_id, ?inviter, ?invitee, "Asking draupnir for user_may_invite");
|
||||
self.send_antispam_request(
|
||||
draupnir.base_url.as_str(),
|
||||
&draupnir.secret,
|
||||
draupnir_antispam::user_may_invite::v1::Request::new(
|
||||
room_id, inviter, invitee,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.inspect(|_| debug!("draupnir allowed the invite"))
|
||||
.inspect_err(|e| debug!("draupnir denied the invite: {e:?}"))
|
||||
.map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
return result;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks with the antispam service whether `user_id` may join `room_id`.
|
||||
pub async fn user_may_join_room(
|
||||
&self,
|
||||
user_id: OwnedUserId,
|
||||
room_id: OwnedRoomId,
|
||||
is_invited: bool,
|
||||
) -> Result<()> {
|
||||
if let Some(config) = &self.services.config.antispam {
|
||||
let result = if let Some(meowlnir) = &config.meowlnir {
|
||||
debug!(?room_id, ?user_id, ?is_invited, "Asking meowlnir for user_may_join_room");
|
||||
self.send_antispam_request(
|
||||
meowlnir.base_url.as_str(),
|
||||
&meowlnir.secret,
|
||||
meowlnir_antispam::user_may_join_room::v1::Request::new(
|
||||
meowlnir.management_room.clone(),
|
||||
user_id,
|
||||
room_id,
|
||||
is_invited,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.inspect(|_| debug!("meowlnir allowed the join"))
|
||||
.inspect_err(|e| debug!("meowlnir denied the join: {e:?}"))
|
||||
.map(|_| ())
|
||||
} else if let Some(draupnir) = &config.draupnir {
|
||||
debug!(?room_id, ?user_id, ?is_invited, "Asking draupnir for user_may_join_room");
|
||||
self.send_antispam_request(
|
||||
draupnir.base_url.as_str(),
|
||||
&draupnir.secret,
|
||||
draupnir_antispam::user_may_join_room::v1::Request::new(
|
||||
user_id, room_id, is_invited,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.inspect(|_| debug!("draupnir allowed the join"))
|
||||
.inspect_err(|e| debug!("draupnir denied the join: {e:?}"))
|
||||
.map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
return result;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks with Meowlnir whether the incoming federated `make_join` request
|
||||
/// should be allowed. Applies the `fi.mau.spam_checker` join rule.
|
||||
pub async fn meowlnir_accept_make_join(
|
||||
&self,
|
||||
room_id: OwnedRoomId,
|
||||
user_id: OwnedUserId,
|
||||
) -> Result<()> {
|
||||
if let Some(Antispam { meowlnir: Some(meowlnir), .. }) = &self.services.config.antispam {
|
||||
debug!(?room_id, ?user_id, "Asking meowlnir for accept_make_join");
|
||||
self.send_antispam_request(
|
||||
meowlnir.base_url.as_str(),
|
||||
&meowlnir.secret,
|
||||
meowlnir_antispam::accept_make_join::v1::Request::new(
|
||||
meowlnir.management_room.clone(),
|
||||
user_id,
|
||||
room_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.inspect(|_| debug!("meowlnir allowed the make_join"))
|
||||
.inspect_err(|e| debug!("meowlnir denied the make_join: {e:?}"))
|
||||
.map(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether all joins should be checked with Meowlnir.
|
||||
/// Is always false if Meowlnir is not configured.
|
||||
pub fn check_all_joins(&self) -> bool {
|
||||
if let Some(Antispam { meowlnir: Some(cfg), .. }) = &self.services.config.antispam {
|
||||
cfg.check_all_joins
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ pub struct Service {
|
||||
pub server_user: OwnedUserId,
|
||||
pub admin_alias: OwnedRoomAliasId,
|
||||
pub turn_secret: String,
|
||||
pub registration_token: Option<String>,
|
||||
}
|
||||
|
||||
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
|
||||
@@ -41,19 +40,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
},
|
||||
);
|
||||
|
||||
let registration_token = config.registration_token_file.as_ref().map_or_else(
|
||||
|| config.registration_token.clone(),
|
||||
|path| {
|
||||
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
|
||||
error!("Failed to read the registration token file: {e}");
|
||||
}) else {
|
||||
return config.registration_token.clone();
|
||||
};
|
||||
|
||||
Some(token.trim().to_owned())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
db,
|
||||
server: args.server.clone(),
|
||||
@@ -66,7 +52,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
)
|
||||
.expect("@conduit:server_name is valid"),
|
||||
turn_secret,
|
||||
registration_token,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#![type_length_limit = "8192"]
|
||||
#![allow(refining_impl_trait)]
|
||||
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
extern crate conduwuit_database as database;
|
||||
mod manager;
|
||||
mod migrations;
|
||||
mod service;
|
||||
@@ -10,6 +12,7 @@
|
||||
pub mod account_data;
|
||||
pub mod admin;
|
||||
pub mod announcements;
|
||||
pub mod antispam;
|
||||
pub mod appservice;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
@@ -21,6 +24,7 @@
|
||||
pub mod moderation;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
pub mod registration_tokens;
|
||||
pub mod resolver;
|
||||
pub mod rooms;
|
||||
pub mod sending;
|
||||
@@ -30,9 +34,6 @@
|
||||
pub mod uiaa;
|
||||
pub mod users;
|
||||
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
extern crate conduwuit_database as database;
|
||||
|
||||
use ctor::{ctor, dtor};
|
||||
pub(crate) use service::{Args, Dep, Service};
|
||||
|
||||
|
||||
129
src/service/registration_tokens/data.rs
Normal file
129
src/service/registration_tokens/data.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use conduwuit::utils::{
|
||||
self,
|
||||
stream::{ReadyExt, TryIgnore},
|
||||
};
|
||||
use database::{Database, Deserialized, Json, Map};
|
||||
use futures::Stream;
|
||||
use ruma::OwnedUserId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) struct Data {
|
||||
registrationtoken_info: Arc<Map>,
|
||||
}
|
||||
|
||||
/// Metadata of a registration token.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DatabaseTokenInfo {
|
||||
/// The admin user who created this token.
|
||||
pub creator: OwnedUserId,
|
||||
/// The number of times this token has been used to create an account.
|
||||
pub uses: u64,
|
||||
/// When this token will expire, if it expires.
|
||||
pub expires: Option<TokenExpires>,
|
||||
}
|
||||
|
||||
impl DatabaseTokenInfo {
|
||||
pub(super) fn new(creator: OwnedUserId, expires: Option<TokenExpires>) -> Self {
|
||||
Self { creator, uses: 0, expires }
|
||||
}
|
||||
|
||||
/// Determine whether this token info represents a valid token, i.e. one
|
||||
/// that has not expired according to its [`Self::expires`] property. If
|
||||
/// [`Self::expires`] is [`None`], this function will always return `true`.
|
||||
#[must_use]
|
||||
pub fn is_valid(&self) -> bool {
|
||||
match self.expires {
|
||||
| Some(TokenExpires::AfterUses(max_uses)) => self.uses < max_uses,
|
||||
| Some(TokenExpires::AfterTime(expiry_time)) => {
|
||||
let now = SystemTime::now();
|
||||
|
||||
expiry_time >= now
|
||||
},
|
||||
| None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DatabaseTokenInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Token created by {} and used {} times. ", &self.creator, self.uses)?;
|
||||
if let Some(expires) = &self.expires {
|
||||
write!(f, "{expires}.")?;
|
||||
} else {
|
||||
write!(f, "Never expires.")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TokenExpires {
|
||||
AfterUses(u64),
|
||||
AfterTime(SystemTime),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenExpires {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
| Self::AfterUses(max_uses) => write!(f, "Expires after {max_uses} uses"),
|
||||
| Self::AfterTime(max_age) => {
|
||||
let now = SystemTime::now();
|
||||
let formatted_expiry = utils::time::format(*max_age, "%+");
|
||||
|
||||
match max_age.duration_since(now) {
|
||||
| Ok(duration) => write!(
|
||||
f,
|
||||
"Expires in {} ({formatted_expiry})",
|
||||
utils::time::pretty(duration)
|
||||
),
|
||||
| Err(_) => write!(f, "Expired at {formatted_expiry}"),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||
Self {
|
||||
registrationtoken_info: db["registrationtoken_info"].clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a registration token with its metadata in the database.
|
||||
pub(super) fn save_token(&self, token: &str, info: &DatabaseTokenInfo) {
|
||||
self.registrationtoken_info.raw_put(token, Json(info));
|
||||
}
|
||||
|
||||
/// Delete a registration token.
|
||||
pub(super) fn revoke_token(&self, token: &str) { self.registrationtoken_info.remove(token); }
|
||||
|
||||
/// Look up a registration token's metadata.
|
||||
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<DatabaseTokenInfo> {
|
||||
self.registrationtoken_info
|
||||
.get(token)
|
||||
.await
|
||||
.deserialized()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Iterate over all valid tokens and delete expired ones.
|
||||
pub(super) fn iterate_and_clean_tokens(
|
||||
&self,
|
||||
) -> impl Stream<Item = (&str, DatabaseTokenInfo)> + Send + '_ {
|
||||
self.registrationtoken_info
|
||||
.stream()
|
||||
.ignore_err()
|
||||
.ready_filter_map(|item: (&str, DatabaseTokenInfo)| {
|
||||
if item.1.is_valid() {
|
||||
Some(item)
|
||||
} else {
|
||||
self.registrationtoken_info.remove(item.0);
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
172
src/service/registration_tokens/mod.rs
Normal file
172
src/service/registration_tokens/mod.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
mod data;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use data::Data;
|
||||
pub use data::{DatabaseTokenInfo, TokenExpires};
|
||||
use futures::{Stream, StreamExt, stream};
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use crate::{Dep, config};
|
||||
|
||||
const RANDOM_TOKEN_LENGTH: usize = 16;
|
||||
|
||||
pub struct Service {
|
||||
db: Data,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
}
|
||||
|
||||
/// A validated registration token which may be used to create an account.
|
||||
#[derive(Debug)]
|
||||
pub struct ValidToken {
|
||||
pub token: String,
|
||||
pub source: ValidTokenSource,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValidToken {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "`{}` --- {}", self.token, &self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ValidToken {
|
||||
fn eq(&self, other: &str) -> bool { self.token == other }
|
||||
}
|
||||
|
||||
/// The source of a valid database token.
|
||||
#[derive(Debug)]
|
||||
pub enum ValidTokenSource {
|
||||
/// The static token set in the homeserver's config file, which is
|
||||
/// always valid.
|
||||
ConfigFile,
|
||||
/// A database token which has been checked to be valid.
|
||||
Database(DatabaseTokenInfo),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValidTokenSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
| Self::ConfigFile => write!(f, "Token defined in config."),
|
||||
| Self::Database(info) => info.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
db: Data::new(args.db),
|
||||
services: Services {
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Issue a new registration token and save it in the database.
|
||||
pub fn issue_token(
|
||||
&self,
|
||||
creator: OwnedUserId,
|
||||
expires: Option<TokenExpires>,
|
||||
) -> (String, DatabaseTokenInfo) {
|
||||
let token = utils::random_string(RANDOM_TOKEN_LENGTH);
|
||||
let info = DatabaseTokenInfo::new(creator, expires);
|
||||
|
||||
self.db.save_token(&token, &info);
|
||||
(token, info)
|
||||
}
|
||||
|
||||
/// Get the registration token set in the config file, if it exists.
|
||||
pub fn get_config_file_token(&self) -> Option<ValidToken> {
|
||||
self.services
|
||||
.config
|
||||
.registration_token
|
||||
.clone()
|
||||
.map(|token| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a registration token.
|
||||
pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
|
||||
// Check the registration token in the config first
|
||||
if self
|
||||
.get_config_file_token()
|
||||
.is_some_and(|valid_token| valid_token == *token)
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
});
|
||||
}
|
||||
|
||||
// Now check the database
|
||||
if let Some(token_info) = self.db.lookup_token_info(&token).await
|
||||
&& token_info.is_valid()
|
||||
{
|
||||
return Some(ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::Database(token_info),
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise it's not valid
|
||||
None
|
||||
}
|
||||
|
||||
/// Mark a valid token as having been used to create a new account.
|
||||
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// we don't track uses of the config file token, do nothing
|
||||
},
|
||||
| ValidTokenSource::Database(mut info) => {
|
||||
info.uses = info.uses.saturating_add(1);
|
||||
|
||||
self.db.save_token(&token, &info);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to revoke a valid token.
|
||||
///
|
||||
/// Note that some tokens (like the one set in the config file) cannot be
|
||||
/// revoked.
|
||||
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// the config file token cannot be revoked
|
||||
Err!(
|
||||
"The token set in the config file cannot be revoked. Edit the config file \
|
||||
to change it."
|
||||
)
|
||||
},
|
||||
| ValidTokenSource::Database(_) => {
|
||||
self.db.revoke_token(&token);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all valid registration tokens.
|
||||
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
|
||||
let db_tokens = self
|
||||
.db
|
||||
.iterate_and_clean_tokens()
|
||||
.map(|(token, info)| ValidToken {
|
||||
token: token.to_owned(),
|
||||
source: ValidTokenSource::Database(info),
|
||||
});
|
||||
|
||||
stream::iter(self.get_config_file_token()).chain(db_tokens)
|
||||
}
|
||||
}
|
||||
65
src/service/sending/antispam.rs
Normal file
65
src/service/sending/antispam.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::{fmt::Debug, mem};
|
||||
|
||||
use bytes::BytesMut;
|
||||
use conduwuit::{Err, Result, debug_error, err, utils, warn};
|
||||
use reqwest::Client;
|
||||
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
|
||||
|
||||
/// Sends a request to an antispam service
|
||||
pub(crate) async fn send_antispam_request<T>(
|
||||
client: &Client,
|
||||
base_url: &str,
|
||||
secret: &str,
|
||||
request: T,
|
||||
) -> Result<T::IncomingResponse>
|
||||
where
|
||||
T: OutgoingRequest + Debug + Send,
|
||||
{
|
||||
const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_15];
|
||||
let http_request = request
|
||||
.try_into_http_request::<BytesMut>(base_url, SendAccessToken::Always(secret), &VERSIONS)?
|
||||
.map(BytesMut::freeze);
|
||||
let reqwest_request = reqwest::Request::try_from(http_request)?;
|
||||
|
||||
let mut response = client.execute(reqwest_request).await.map_err(|e| {
|
||||
warn!("Could not send request to antispam: {e:?}");
|
||||
e
|
||||
})?;
|
||||
|
||||
// reqwest::Response -> http::Response conversion
|
||||
let status = response.status();
|
||||
let mut http_response_builder = http::Response::builder()
|
||||
.status(status)
|
||||
.version(response.version());
|
||||
mem::swap(
|
||||
response.headers_mut(),
|
||||
http_response_builder
|
||||
.headers_mut()
|
||||
.expect("http::response::Builder is usable"),
|
||||
);
|
||||
|
||||
let body = response.bytes().await?; // TODO: handle timeout
|
||||
|
||||
if !status.is_success() {
|
||||
debug_error!("Antispam response bytes: {:?}", utils::string_from_bytes(&body));
|
||||
return match status {
|
||||
| http::StatusCode::FORBIDDEN =>
|
||||
Err!(Request(Forbidden("Request was rejected by antispam service.",))),
|
||||
| _ => Err!(BadServerResponse(warn!(
|
||||
"Antispam returned unsuccessful HTTP response {status}",
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
let response = T::IncomingResponse::try_from_http_response(
|
||||
http_response_builder
|
||||
.body(body)
|
||||
.expect("reqwest body is valid http body"),
|
||||
);
|
||||
|
||||
response.map_err(|e| {
|
||||
err!(BadServerResponse(warn!(
|
||||
"Antispam returned invalid/malformed response bytes: {e}",
|
||||
)))
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod antispam;
|
||||
mod appservice;
|
||||
mod data;
|
||||
mod dest;
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
account_data, admin, announcements, appservice, client, config, emergency, federation,
|
||||
globals, key_backups,
|
||||
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||
federation, globals, key_backups,
|
||||
manager::Manager,
|
||||
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
||||
service::{Args, Map, Service},
|
||||
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||
server_keys,
|
||||
service::{self, Args, Map, Service},
|
||||
sync, transaction_ids, uiaa, users,
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ pub struct Services {
|
||||
pub media: Arc<media::Service>,
|
||||
pub presence: Arc<presence::Service>,
|
||||
pub pusher: Arc<pusher::Service>,
|
||||
pub registration_tokens: Arc<registration_tokens::Service>,
|
||||
pub resolver: Arc<resolver::Service>,
|
||||
pub rooms: rooms::Service,
|
||||
pub federation: Arc<federation::Service>,
|
||||
@@ -39,6 +41,7 @@ pub struct Services {
|
||||
pub users: Arc<users::Service>,
|
||||
pub moderation: Arc<moderation::Service>,
|
||||
pub announcements: Arc<announcements::Service>,
|
||||
pub antispam: Arc<antispam::Service>,
|
||||
|
||||
manager: Mutex<Option<Arc<Manager>>>,
|
||||
pub(crate) service: Arc<Map>,
|
||||
@@ -76,6 +79,7 @@ macro_rules! build {
|
||||
media: build!(media::Service),
|
||||
presence: build!(presence::Service),
|
||||
pusher: build!(pusher::Service),
|
||||
registration_tokens: build!(registration_tokens::Service),
|
||||
rooms: rooms::Service {
|
||||
alias: build!(rooms::alias::Service),
|
||||
auth_chain: build!(rooms::auth_chain::Service),
|
||||
@@ -107,6 +111,7 @@ macro_rules! build {
|
||||
users: build!(users::Service),
|
||||
moderation: build!(moderation::Service),
|
||||
announcements: build!(announcements::Service),
|
||||
antispam: build!(antispam::Service),
|
||||
|
||||
manager: Mutex::new(None),
|
||||
service,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Error, Result, SyncRwLock, err, error, implement, utils,
|
||||
@@ -16,7 +13,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{Dep, config, globals, users};
|
||||
use crate::{Dep, config, globals, registration_tokens, users};
|
||||
|
||||
pub struct Service {
|
||||
userdevicesessionid_uiaarequest: SyncRwLock<RequestMap>,
|
||||
@@ -28,6 +25,7 @@ struct Services {
|
||||
globals: Dep<globals::Service>,
|
||||
users: Dep<users::Service>,
|
||||
config: Dep<config::Service>,
|
||||
registration_tokens: Dep<registration_tokens::Service>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -50,6 +48,8 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
config: args.depend::<config::Service>("config"),
|
||||
registration_tokens: args
|
||||
.depend::<registration_tokens::Service>("registration_tokens"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -57,26 +57,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn read_tokens(&self) -> Result<HashSet<String>> {
|
||||
let mut tokens = HashSet::new();
|
||||
if let Some(file) = &self.services.config.registration_token_file.as_ref() {
|
||||
match std::fs::read_to_string(file) {
|
||||
| Ok(text) => {
|
||||
text.split_ascii_whitespace().for_each(|token| {
|
||||
tokens.insert(token.to_owned());
|
||||
});
|
||||
},
|
||||
| Err(e) => error!("Failed to read the registration token file: {e}"),
|
||||
}
|
||||
}
|
||||
if let Some(token) = &self.services.config.registration_token {
|
||||
tokens.insert(token.to_owned());
|
||||
}
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
/// Creates a new Uiaa session. Make sure the session token is unique.
|
||||
#[implement(Service)]
|
||||
pub fn create(
|
||||
@@ -229,8 +209,18 @@ pub async fn try_auth(
|
||||
}
|
||||
},
|
||||
| AuthData::RegistrationToken(t) => {
|
||||
let tokens = self.read_tokens().await?;
|
||||
if tokens.contains(t.token.trim()) {
|
||||
let token = t.token.trim().to_owned();
|
||||
|
||||
if let Some(valid_token) = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.validate_token(token)
|
||||
.await
|
||||
{
|
||||
self.services
|
||||
.registration_tokens
|
||||
.mark_token_as_used(valid_token);
|
||||
|
||||
uiaainfo.completed.push(AuthType::RegistrationToken);
|
||||
} else {
|
||||
uiaainfo.auth_error = Some(StandardErrorBody {
|
||||
|
||||
@@ -77,6 +77,8 @@ struct Data {
|
||||
userid_origin: Arc<Map>,
|
||||
userid_password: Arc<Map>,
|
||||
userid_suspension: Arc<Map>,
|
||||
userid_lock: Arc<Map>,
|
||||
userid_logindisabled: Arc<Map>,
|
||||
userid_selfsigningkeyid: Arc<Map>,
|
||||
userid_usersigningkeyid: Arc<Map>,
|
||||
useridprofilekey_value: Arc<Map>,
|
||||
@@ -115,6 +117,8 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
userid_origin: args.db["userid_origin"].clone(),
|
||||
userid_password: args.db["userid_password"].clone(),
|
||||
userid_suspension: args.db["userid_suspension"].clone(),
|
||||
userid_lock: args.db["userid_lock"].clone(),
|
||||
userid_logindisabled: args.db["userid_logindisabled"].clone(),
|
||||
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
||||
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
|
||||
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
|
||||
@@ -220,6 +224,26 @@ pub async fn unsuspend_account(&self, user_id: &UserId) {
|
||||
self.db.userid_suspension.remove(user_id);
|
||||
}
|
||||
|
||||
pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
|
||||
// NOTE: Locking is basically just suspension with a more severe effect,
|
||||
// so we'll just re-use the suspension data structure to store the lock state.
|
||||
let suspension = self
|
||||
.db
|
||||
.userid_lock
|
||||
.get(user_id)
|
||||
.await
|
||||
.deserialized::<UserSuspension>()
|
||||
.unwrap_or_else(|_| UserSuspension {
|
||||
suspended: true,
|
||||
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
|
||||
suspended_by: locking_user.to_string(),
|
||||
});
|
||||
|
||||
self.db.userid_lock.raw_put(user_id, Json(suspension));
|
||||
}
|
||||
|
||||
pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); }
|
||||
|
||||
/// Check if a user has an account on this homeserver.
|
||||
#[inline]
|
||||
pub async fn exists(&self, user_id: &UserId) -> bool {
|
||||
@@ -255,6 +279,34 @@ pub async fn is_suspended(&self, user_id: &UserId) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_locked(&self, user_id: &UserId) -> Result<bool> {
|
||||
match self
|
||||
.db
|
||||
.userid_lock
|
||||
.get(user_id)
|
||||
.await
|
||||
.deserialized::<UserSuspension>()
|
||||
{
|
||||
| Ok(s) => Ok(s.suspended),
|
||||
| Err(e) =>
|
||||
if e.is_not_found() {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_login(&self, user_id: &UserId) {
|
||||
self.db.userid_logindisabled.insert(user_id, "");
|
||||
}
|
||||
|
||||
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
|
||||
|
||||
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
|
||||
self.db.userid_logindisabled.contains(user_id).await
|
||||
}
|
||||
|
||||
/// Check if account is active, infallible
|
||||
pub async fn is_active(&self, user_id: &UserId) -> bool {
|
||||
!self.is_deactivated(user_id).await.unwrap_or(true)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
[package]
|
||||
name = "conduwuit_web"
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
[package]
|
||||
name = "xtask-generate-commands"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
Reference in New Issue
Block a user