mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-08 02:01:38 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e713c7c9e1 | |||
| 5901efef71 | |||
| a5cb5d812b | |||
| fc93f79a0c | |||
| 8d1e6a167e | |||
| 3746f3c80b | |||
| d495ad2541 | |||
| be1c344787 | |||
| d1c7ee8769 | |||
| d6a2760903 | |||
| 8b762cf2e6 | |||
| 1ce9ae2cbf | |||
| 6a3370005e | |||
| 675cfb964a | |||
| 09312791a7 | |||
| 087d8b1016 | |||
| 6155dd2726 | |||
| 688cd8f46a | |||
| 3ab1f102dd | |||
| 480a32e4d4 | |||
| fadd559837 | |||
| 79c63c17fc | |||
| cdc772ba10 | |||
| 5f1b80a47c | |||
| 0f8b56f521 | |||
| 67d8d72506 | |||
| fcfa7b8bef | |||
| 0cc1e4685c | |||
| 3d2915093c | |||
| e1c54f4dec | |||
| 0c9fa3b7e5 | |||
| a95b488e6a | |||
| 4f8833e937 | |||
| f32599e030 | |||
| b6f0b41d3d | |||
| d5675b85cf | |||
| 951b5abe19 | |||
| a325ad16f1 | |||
| f93a1cc506 | |||
| 6e8dbcbfab | |||
| 97458207e5 | |||
| ab8929e2fa | |||
| 166d7d0f63 | |||
| 20a6f0c6fb | |||
| 3885e43b5d | |||
| ef7ad6082c | |||
| 717d319708 | |||
| 0b04757bef | |||
| f2b7dd6519 | |||
| 9d06208a7a | |||
| 955da3a74f | |||
| 7e79a544cf | |||
| f5db4d17d6 | |||
| 54fd1d313f | |||
| bb7fd9efc1 | |||
| aa79072411 | |||
| 8b72c5eb11 | |||
| e5cfc503d8 | |||
| 07d5081008 | |||
| dba7f47972 | |||
| 0a2d4e1cb2 | |||
| f45857acd4 |
@@ -24,7 +24,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.44.0
|
||||
rev: v1.45.0
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
|
||||
+1
-131
@@ -1,131 +1 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement over Matrix at [#continuwuity:continuwuity.org](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) or email at <tom@tcpip.uk>, <jade@continuwuity.org> and <nex@continuwuity.org> respectively.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
Contributors are expected to follow the [Continuwuity Community Guidelines](continuwuity.org/community/guidelines).
|
||||
|
||||
Generated
+284
-123
@@ -72,6 +72,12 @@ dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -113,6 +119,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "ar_archive_writer"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
|
||||
dependencies = [
|
||||
"object",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -820,6 +835,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chumsky"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"stacker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
@@ -949,6 +974,7 @@ dependencies = [
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
"conduwuit_core",
|
||||
@@ -957,6 +983,7 @@ dependencies = [
|
||||
"conduwuit_service",
|
||||
"const-str",
|
||||
"futures",
|
||||
"lettre",
|
||||
"log",
|
||||
"ruma",
|
||||
"serde-saphyr",
|
||||
@@ -970,6 +997,7 @@ dependencies = [
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
@@ -986,10 +1014,12 @@ dependencies = [
|
||||
"hyper",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"lettre",
|
||||
"log",
|
||||
"rand 0.10.0",
|
||||
"reqwest",
|
||||
"ruma",
|
||||
"ruminuwuity",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
@@ -1011,6 +1041,7 @@ version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
"assign",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bytes",
|
||||
@@ -1034,6 +1065,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"lettre",
|
||||
"libc",
|
||||
"libloading 0.9.0",
|
||||
"lock_api",
|
||||
@@ -1061,7 +1093,7 @@ dependencies = [
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tokio-metrics",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
@@ -1100,6 +1132,7 @@ dependencies = [
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
"axum-server",
|
||||
@@ -1135,6 +1168,7 @@ name = "conduwuit_service"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"assign",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"blurhash",
|
||||
@@ -1144,20 +1178,24 @@ dependencies = [
|
||||
"const-str",
|
||||
"either",
|
||||
"futures",
|
||||
"governor",
|
||||
"hickory-resolver",
|
||||
"http",
|
||||
"image",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"ldap3",
|
||||
"lettre",
|
||||
"log",
|
||||
"loole",
|
||||
"lru-cache",
|
||||
"nonzero_ext",
|
||||
"rand 0.10.0",
|
||||
"recaptcha-verify",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ruma",
|
||||
"ruminuwuity",
|
||||
"rustyline-async",
|
||||
"sd-notify",
|
||||
"serde",
|
||||
@@ -1177,6 +1215,7 @@ name = "conduwuit_web"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"assign",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -1257,16 +1296,6 @@ dependencies = [
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "continuwuity-admin-api"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@@ -1692,16 +1721,6 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "draupnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.1"
|
||||
@@ -1757,6 +1776,22 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-encoding"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -1958,6 +1993,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -2074,6 +2115,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
@@ -2164,6 +2211,25 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-sink",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"hashbrown 0.16.1",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"smallvec",
|
||||
"spinning_top",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -2228,13 +2294,23 @@ version = "0.1.2+12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2242,6 +2318,11 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
@@ -2796,30 +2877,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js_option"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
|
||||
checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.3.16"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9"
|
||||
checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"
|
||||
dependencies = [
|
||||
"const_panic",
|
||||
"konst_kernel",
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst_kernel"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c"
|
||||
dependencies = [
|
||||
"typewit",
|
||||
]
|
||||
|
||||
@@ -2899,6 +2970,37 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chumsky",
|
||||
"email-encoding",
|
||||
"email_address",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate",
|
||||
"idna",
|
||||
"mime",
|
||||
"nom 8.0.0",
|
||||
"percent-encoding",
|
||||
"quoted_printable",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"serde",
|
||||
"socket2 0.6.3",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -3114,16 +3216,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meowlnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -3281,6 +3373,12 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
@@ -4023,6 +4121,16 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
|
||||
dependencies = [
|
||||
"ar_archive_writer",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.1"
|
||||
@@ -4127,6 +4235,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
@@ -4145,6 +4259,8 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
@@ -4154,7 +4270,7 @@ version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
@@ -4169,6 +4285,16 @@ dependencies = [
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
@@ -4231,7 +4357,7 @@ dependencies = [
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.18",
|
||||
"v_frame",
|
||||
@@ -4398,31 +4524,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"continuwuity-admin-api",
|
||||
"draupnir-antispam",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"meowlnir-antispam",
|
||||
"ruma-appservice-api",
|
||||
"ruma-client-api",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-federation-api",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-identity-service-api",
|
||||
"ruma-push-gateway-api",
|
||||
"ruma-signatures",
|
||||
"ruma-state-res",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.10.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4433,13 +4555,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.18.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.22.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
"bytes",
|
||||
"date_header",
|
||||
"http",
|
||||
"js_int",
|
||||
"js_option",
|
||||
@@ -4456,12 +4577,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"date_header",
|
||||
"form_urlencoded",
|
||||
"getrandom 0.2.17",
|
||||
"http",
|
||||
@@ -4469,14 +4591,13 @@ dependencies = [
|
||||
"js_int",
|
||||
"konst",
|
||||
"percent-encoding",
|
||||
"rand 0.10.0",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-macros",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -4484,37 +4605,34 @@ dependencies = [
|
||||
"uuid",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.28.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.32.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"percent-encoding",
|
||||
"pulldown-cmark",
|
||||
"regex",
|
||||
"ruma-common",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4524,9 +4642,10 @@ dependencies = [
|
||||
"js_int",
|
||||
"memchr",
|
||||
"mime",
|
||||
"rand 0.10.0",
|
||||
"rand 0.8.5",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-signatures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
@@ -4535,28 +4654,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -4564,13 +4674,13 @@ dependencies = [
|
||||
"ruma-identifiers-validation",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml 0.8.23",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4581,21 +4691,46 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a97b91adcc012ef04991d823b8b5a79c6686ae48#a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"memchr",
|
||||
"pkcs8",
|
||||
"rand 0.10.0",
|
||||
"rand_core 0.6.4",
|
||||
"rand 0.8.5",
|
||||
"ruma-common",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"subslice",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-state-res"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-signatures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruminuwuity"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"ruma",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-librocksdb-sys"
|
||||
version = "0.42.0+10.10.1"
|
||||
@@ -5080,9 +5215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -5255,6 +5390,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -5271,6 +5415,19 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strict"
|
||||
version = "0.2.0"
|
||||
@@ -5308,15 +5465,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subslice"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a8e4809a3bb02de01f1f7faf1ba01a83af9e8eabcd4d31dd6e413d14d56aae"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -5638,13 +5786,26 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned 1.0.4",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
@@ -5665,9 +5826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -5693,16 +5854,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.0.1+spec-1.1.0",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.10+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
@@ -5943,15 +6104,6 @@ name = "typewit"
|
||||
version = "1.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
|
||||
dependencies = [
|
||||
"typewit_proc_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typewit_proc_macros"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
|
||||
|
||||
[[package]]
|
||||
name = "uname"
|
||||
@@ -6406,6 +6558,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
|
||||
+32
-21
@@ -47,9 +47,9 @@ default-features = false
|
||||
features = ["features"]
|
||||
|
||||
[workspace.dependencies.toml]
|
||||
version = "0.9.5"
|
||||
version = "1.0.0"
|
||||
default-features = false
|
||||
features = ["parse"]
|
||||
features = ["parse", "serde"]
|
||||
|
||||
[workspace.dependencies.sanitize-filename]
|
||||
version = "0.6.0"
|
||||
@@ -340,50 +340,45 @@ version = "0.1.88"
|
||||
[workspace.dependencies.lru-cache]
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies.assign]
|
||||
version = "1.1.1"
|
||||
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
"federation-api",
|
||||
"markdown",
|
||||
"push-gateway-api-c",
|
||||
"unstable-exhaustive-types",
|
||||
"state-res",
|
||||
"rand",
|
||||
"markdown",
|
||||
"ring-compat",
|
||||
"compat-upload-signatures",
|
||||
"identifiers-validation",
|
||||
"unstable-unspecified",
|
||||
"unstable-msc2448",
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
"unstable-msc3026",
|
||||
"unstable-msc3061",
|
||||
"unstable-msc3814",
|
||||
"unstable-msc3245",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc3381", # polls
|
||||
"unstable-msc3489", # beacon / live location
|
||||
"unstable-msc3575",
|
||||
"unstable-msc3930", # polls push rules
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4095",
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"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-msc4310",
|
||||
"unstable-msc4380",
|
||||
"unstable-msc4143", # livekit well_known response
|
||||
"unstable-msc4284"
|
||||
"unstable-msc4439", # pgp_key in .well_known/matrix/support
|
||||
"unstable-extensible-events",
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
@@ -556,6 +551,19 @@ version = "1.0.1"
|
||||
[workspace.dependencies.askama]
|
||||
version = "0.15.0"
|
||||
|
||||
[workspace.dependencies.lettre]
|
||||
version = "0.11.19"
|
||||
default-features = false
|
||||
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "rustls-native-certs", "tokio1", "ring", "tokio1-rustls", "tracing", "serde"]
|
||||
|
||||
[workspace.dependencies.governor]
|
||||
version = "0.10.4"
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
|
||||
[workspace.dependencies.nonzero_ext]
|
||||
version = "0.3.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
@@ -643,6 +651,10 @@ default-features = false
|
||||
package = "conduwuit"
|
||||
path = "src/main"
|
||||
|
||||
[workspace.dependencies.ruminuwuity]
|
||||
package = "ruminuwuity"
|
||||
path = "src/ruminuwuity"
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# Release profiles
|
||||
@@ -916,7 +928,6 @@ fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
get_unwrap = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
let_underscore_untyped = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_assert_message = "warn"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -187,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 June
|
||||
Copyright 2023 Continuwuity Team and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[book]
|
||||
title = "continuwuity"
|
||||
description = "continuwuity is a community continuation of the conduwuit Matrix homeserver, written in Rust."
|
||||
language = "en"
|
||||
authors = ["The continuwuity Community"]
|
||||
text-direction = "ltr"
|
||||
src = "docs"
|
||||
|
||||
[build]
|
||||
build-dir = "public"
|
||||
create-missing = true
|
||||
extra-watch-dirs = ["debian", "docs"]
|
||||
|
||||
[rust]
|
||||
edition = "2024"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/{path}"
|
||||
git-repository-url = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
git-repository-icon = "fa-git-alt"
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
@@ -0,0 +1 @@
|
||||
Added support for associating email addresses with accounts, requiring email addresses for registration, and resetting passwords via email. Contributed by @ginger
|
||||
@@ -0,0 +1 @@
|
||||
Fixed error 500 when joining non-existent rooms. Contributed by @ezera.
|
||||
@@ -0,0 +1,2 @@
|
||||
Add new config option for [MSC4439](https://github.com/matrix-org/matrix-spec-proposals/pull/4439)
|
||||
PGP key URIs. Contributed by LogN.
|
||||
@@ -0,0 +1 @@
|
||||
Added `!admin users reset-push-rules` command to reset the notification settings of users. Contributed by @nex.
|
||||
@@ -0,0 +1 @@
|
||||
Notification pushers are now automatically removed when their associated device is. Admin commands now exist for manual cleanup too. Contributed by @nex.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed resolving IP of servers that only use SRV delegation. Contributed by @tulir.
|
||||
@@ -1869,6 +1869,11 @@
|
||||
#
|
||||
#support_mxid =
|
||||
|
||||
# PGP key URI for server support contacts, to be served as part of the
|
||||
# MSC1929 server support endpoint.
|
||||
#
|
||||
#support_pgp_key =
|
||||
|
||||
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
|
||||
#
|
||||
# A list of MatrixRTC foci URLs which will be served as part of the
|
||||
@@ -2041,3 +2046,41 @@
|
||||
# web->synapseHTTPAntispam->authorization
|
||||
#
|
||||
#secret =
|
||||
|
||||
#[global.smtp]
|
||||
|
||||
# A `smtp://`` URI which will be used to connect to a mail server.
|
||||
# Uncommenting the [global.smtp] group and setting this option enables
|
||||
# features which depend on the ability to send email,
|
||||
# such as self-service password resets.
|
||||
#
|
||||
# For most modern mail servers, format the URI like this:
|
||||
# `smtps://username:password@hostname:port`
|
||||
# Note that you will need to URL-encode the username and password. If your
|
||||
# username _is_ your email address, you will need to replace the `@` with
|
||||
# `%40`.
|
||||
#
|
||||
# For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||
# https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||
#
|
||||
#connection_uri =
|
||||
|
||||
# The outgoing address which will be used for sending emails.
|
||||
#
|
||||
# For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
#
|
||||
# ...or if you don't want to read the RFC, for some reason:
|
||||
# - `Name <address@domain.org>` to specify a sender name
|
||||
# - `address@domain.org` to not use a name
|
||||
#
|
||||
#sender =
|
||||
|
||||
# Whether to require that users provide an email address when they
|
||||
# register.
|
||||
#
|
||||
#require_email_for_registration = false
|
||||
|
||||
# Whether to require that users who register with a registration token
|
||||
# provide an email address.
|
||||
#
|
||||
#require_email_for_token_registration = false
|
||||
|
||||
+2
-2
@@ -15,13 +15,13 @@ ARG LLVM_VERSION=21
|
||||
|
||||
# Install repo tools
|
||||
# Line one: compiler tools
|
||||
# Line two: curl, for downloading binaries
|
||||
# Line two: curl, for downloading binaries and wget because llvm.sh is broken with curl
|
||||
# Line three: for xx-verify
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && apt-get install -y \
|
||||
pkg-config make jq \
|
||||
curl git software-properties-common \
|
||||
wget curl git software-properties-common \
|
||||
file
|
||||
|
||||
# LLVM packages
|
||||
|
||||
@@ -5,7 +5,7 @@ # Matrix RTC/Element Call Setup
|
||||
:::
|
||||
|
||||
:::tip
|
||||
You can find help setting up Matrix RTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
|
||||
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
|
||||
:::
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
# Continuwuity Community Guidelines
|
||||
|
||||
Welcome to the Continuwuity commuwunity! We're excited to have you here. Continuwuity is a
|
||||
continuation of the conduwuit homeserver, which in turn is a hard-fork of the Conduit homeserver,
|
||||
aimed at making Matrix more accessible and inclusive for everyone.
|
||||
Welcome to the Continuwuity commuwunity! We're excited to have you here.
|
||||
|
||||
This space is dedicated to fostering a positive, supportive, and welcoming environment for everyone.
|
||||
These guidelines apply to all Continuwuity spaces, including our Matrix rooms and any other
|
||||
community channels that reference them. We've written these guidelines to help us all create an
|
||||
environment where everyone feels safe and respected.
|
||||
Our project aims to make Matrix more accessible and inclusive for everyone. To that end, we are dedicated to fostering a positive, supportive, safe and welcoming environment for our community.
|
||||
|
||||
For code and contribution guidelines, please refer to the
|
||||
[Contributor's Covenant](https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CODE_OF_CONDUCT.md).
|
||||
Below are additional guidelines specific to the Continuwuity community.
|
||||
These guidelines apply to all Continuwuity spaces, including our Matrix rooms and code forge.
|
||||
|
||||
Our community spaces are intended for individuals aged 16 or over, because we expect maturity and respect from our community members.
|
||||
|
||||
## Our Values and Expected Behaviors
|
||||
|
||||
@@ -29,17 +24,21 @@ ## Our Values and Expected Behaviors
|
||||
|
||||
3. **Communicate Clearly and Kindly**: Our community includes neurodivergent individuals and those
|
||||
who may not appreciate sarcasm or subtlety. Communicate clearly and kindly. Avoid ambiguity and
|
||||
ensure your messages can be easily understood by all. Avoid placing the burden of education on
|
||||
ensure your messages can be easily understood by all.
|
||||
|
||||
4. **Be Considerate and Proactive**: Not everyone has the same time, resource and experience to spare.
|
||||
Don't expect others to give up their time and labour for you; be thankful for what you have already been given.
|
||||
Avoid placing the burden of education on
|
||||
marginalized groups; please make an effort to look into your questions before asking others for
|
||||
detailed explanations.
|
||||
|
||||
4. **Be Open to Improving Inclusivity**: Actively participate in making our community more inclusive.
|
||||
5. **Be Engaged and Open-Minded**: Actively participate in making our community more inclusive.
|
||||
Report behaviour that contradicts these guidelines (see Reporting and Enforcement below) and be
|
||||
open to constructive feedback aimed at improving our community. Understand that discussing
|
||||
negative experiences can be emotionally taxing; focus on the message, not the tone.
|
||||
|
||||
5. **Commit to Our Values**: Building an inclusive community requires ongoing effort from everyone.
|
||||
Recognise that addressing bias and discrimination is a continuous process that needs commitment
|
||||
6. **Commit to Our Values**: Building an inclusive community requires ongoing effort from everyone.
|
||||
Recognise that creating a welcoming and open community is a continuous process that needs commitment
|
||||
and action from all members.
|
||||
|
||||
## Unacceptable Behaviors
|
||||
@@ -72,36 +71,6 @@ ## Unacceptable Behaviors
|
||||
This is not an exhaustive list. Any behaviour that makes others feel unsafe or unwelcome may be
|
||||
subject to enforcement action.
|
||||
|
||||
## Matrix Community
|
||||
|
||||
These Community Guidelines apply to the entire
|
||||
[Continuwuity Matrix Space](https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) and its rooms, including:
|
||||
|
||||
### [#continuwuity:continuwuity.org](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
This room is for support and discussions about Continuwuity. Ask questions, share insights, and help
|
||||
each other out while adhering to these guidelines.
|
||||
|
||||
We ask that this room remain focused on the Continuwuity software specifically: the team are
|
||||
typically happy to engage in conversations about related subjects in the off-topic room.
|
||||
|
||||
### [#offtopic:continuwuity.org](https://matrix.to/#/#offtopic:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
For off-topic community conversations about any subject. While this room allows for a wide range of
|
||||
topics, the same guidelines apply. Please keep discussions respectful and inclusive, and avoid
|
||||
divisive or stressful subjects like specific country/world politics unless handled with exceptional
|
||||
care and respect for diverse viewpoints.
|
||||
|
||||
General topics, such as world events, are welcome as long as they follow the guidelines. If a member
|
||||
of the team asks for the conversation to end, please respect their decision.
|
||||
|
||||
### [#dev:continuwuity.org](https://matrix.to/#/#dev:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
This room is dedicated to discussing active development of Continuwuity, including ongoing issues or
|
||||
code development. Collaboration here must follow these guidelines, and please consider raising
|
||||
[an issue](https://forgejo.ellis.link/continuwuation/continuwuity/issues) on the repository to help
|
||||
track progress.
|
||||
|
||||
## Reporting and Enforcement
|
||||
|
||||
We take these Community Guidelines seriously to protect our community members. If you witness or
|
||||
@@ -114,6 +83,7 @@ ## Reporting and Enforcement
|
||||
will immediately alert all available moderators.
|
||||
* **Direct Message:** If you're not comfortable raising the issue publicly, please send a direct
|
||||
message (DM) to one of the room moderators.
|
||||
* **Email**: Please email Jade and/or Nex at `jade@continuwuity.org` and `nex@continuwuity.org` respectively, or email `team@continuwuity.org`.
|
||||
|
||||
Reports will be handled with discretion. We will investigate promptly and thoroughly.
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ ## `!admin debug database-files`
|
||||
|
||||
List database files
|
||||
|
||||
## `!admin debug send-test-email`
|
||||
|
||||
Send a test email to the invoking admin's email address
|
||||
|
||||
## `!admin debug tester`
|
||||
|
||||
Developer test stubs
|
||||
|
||||
@@ -12,6 +12,24 @@ ## `!admin users reset-password`
|
||||
|
||||
Reset user password
|
||||
|
||||
## `!admin users issue-password-reset-link`
|
||||
|
||||
Issue a self-service password reset link for a user
|
||||
|
||||
## `!admin users get-email`
|
||||
|
||||
Get a user's associated email address
|
||||
|
||||
## `!admin users get-user-by-email`
|
||||
|
||||
Get the user with the given email address
|
||||
|
||||
## `!admin users change-email`
|
||||
|
||||
Update or remove a user's email address.
|
||||
|
||||
If `email` is not supplied, the user's existing address will be removed.
|
||||
|
||||
## `!admin users deactivate`
|
||||
|
||||
Deactivate a user
|
||||
|
||||
Generated
+80
-80
@@ -123,14 +123,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rsbuild/core": {
|
||||
"version": "2.0.0-beta.10",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.10.tgz",
|
||||
"integrity": "sha512-6xalOGzWjamJQvC+qnAipo6azfW3cn9JSRSkTMBz/hiXFzcfy54GX31gCDhRY0TooEisyJ2wbGWjGcT8zPwwxg==",
|
||||
"version": "2.0.0-beta.11",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.11.tgz",
|
||||
"integrity": "sha512-IBbQx7SrnSpD7j2p2qyq3qDxoqmG4E6lcflTpbBitX6iUrzpVRQbP4rktXZ2iuY7ph9+FtUK/SVAVA+Ocm3Nig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/core": "2.0.0-beta.8",
|
||||
"@swc/helpers": "^0.5.19"
|
||||
"@rspack/core": "2.0.0-beta.9",
|
||||
"@swc/helpers": "^0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"rsbuild": "bin/rsbuild.js"
|
||||
@@ -167,28 +167,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-6tG/yYhUIF1zcEF7qw9GPA1Bwj5gq+Hqy4OzVzIBUWOn/2bKsFTWuorEJh8Yx1LwOnjNO7O+NbsATvk5zEOGKQ==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-QgkOvzl6BJc4Vg5eaY9r7MkHNfXvVZPgTIeYkdBEOYPowdyCLhlG9vH7QltqLKP9KDNel70YIeMyUrpTqez01w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@rspack/binding-darwin-arm64": "2.0.0-beta.8",
|
||||
"@rspack/binding-darwin-x64": "2.0.0-beta.8",
|
||||
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.8",
|
||||
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.8",
|
||||
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.8",
|
||||
"@rspack/binding-linux-x64-musl": "2.0.0-beta.8",
|
||||
"@rspack/binding-wasm32-wasi": "2.0.0-beta.8",
|
||||
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.8",
|
||||
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.8",
|
||||
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.8"
|
||||
"@rspack/binding-darwin-arm64": "2.0.0-beta.9",
|
||||
"@rspack/binding-darwin-x64": "2.0.0-beta.9",
|
||||
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.9",
|
||||
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.9",
|
||||
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.9",
|
||||
"@rspack/binding-linux-x64-musl": "2.0.0-beta.9",
|
||||
"@rspack/binding-wasm32-wasi": "2.0.0-beta.9",
|
||||
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.9",
|
||||
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.9",
|
||||
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-arm64": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-h3x2GreEh8J36A3cWFeHZGTuz4vjUArk9dBDq8fZSyaUQQQox/lp8bUOGa/2YuYUOXk0gei2GN+/BVi2R5p39A==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-9Aao24b+lrVGG25itl2c7e6HK6eNH5J5ao1Uq5UoSwSJZOxRPuY+QlHIvE2tyt833Ly9qcT1J7os2AIUNlF6Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -200,9 +200,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-x64": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-+XTA37+FZjXgwxkNX94T/EqspFO8Q3Km4CklQ3nOQzieMi31w+TLBB0uTsnT1ugp0UTN5PHLd4DFK1SQB7Ckbg==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-sP6gusMsxm3W4aHpRsmVaBQU09n1p/1+XpLHT/gZy6nJ7Wy3nqfNKNoybNBORwCuFcGUon6cVRcieN9AEm6iJA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -214,9 +214,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-vD2+ztbMmeBR65jBlwUZCNIjUzO0exp/LaPSMIhLlqPlk670gMCQ7fmKo3tSgQ9tobfizEA/Atdy3/lW1Rl64A==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-k2DPN3B2qaz4L/h/R+l7rbDk/lLwbR/sayfsHZ8sLdZ3f6pvaSI9ejrsFv0nU4OmKCQsz4zYuoKTVFPtDfbGjA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -231,9 +231,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-musl": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-jJ1XB7Yz9YdPRA6MJ35S9/mb+3jeI4p9v78E3dexzCPA3G4X7WXbyOcRbUlYcyOlE5MtX5O19rDexqWlkD9tVw==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-7+XwAsqhfc2rIHMc9mY6RMBTP76RRqmUm1UjidqYdJl5hYBa5apffjeZfJYgAhVbSwKB/tUffzPpEffGUuc5kw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -248,9 +248,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-gnu": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-qy+fK/tiYw3KvGjTGGMu/mWOdvBYrMO8xva/ouiaRTrx64PPZ6vyqFXOUfHj9rhY5L6aU2NTObpV6HZHcBtmhQ==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-z/EOUKEq5rq4sYsVSFL9uzdPtTPVA82x3gsRJlDTfEcruZZI7Y6JKUkpDYkC0LivXqyOnoOz8slAFd2/dByRtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -265,9 +265,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-musl": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-eJF1IsayHhsURu5Dp6fzdr5jYGeJmoREOZAc9UV3aEqY6zNAcWgZT1RwKCCujJylmHgCTCOuxqdK/VdFJqWDyw==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-LVIXrqtAOy/DowIB04jyUyYy+5kHtZNJ0W5EJd39OwY/9gGvhgAEVvSWu7JrRAvKW1kQsV7GnRT5ninbDrRw1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -282,9 +282,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-HssdOQE8i+nUWoK+NDeD5OSyNxf80k3elKCl/due3WunoNn0h6tUTSZ8QB+bhcT4tjH9vTbibWZIT91avtvUNw==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-Vl7aDAt7DCqtZ/RJd8hLFjQqufX+efL/XZG3qADsagl/SspH1ItJ7N6X1S8o50eKoshy27Jr7mQYZEdufX9qhQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -296,9 +296,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-RuHbXuIMJr0ANMFoGXIb3sUZE5VwIsJw70u3TKPwfoaOFiJjgW7Pi2JTLPoTYfOlE+CNcu2ldX8VJRBbktR4NA==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-g4Fc3JjfibuHt5ltoV64eK0bs6NKlh8kgHA8Go3ETwEGO6OBck877e+5CqPtjTH8c1/KQPbnCoccGR1OScoZGg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -310,9 +310,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-ia32-msvc": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-ajzIOk30zjTKPiay+d6oV7lqzzqdgIXQhDD5YtcOqPn7NTh7949EB1NZX5l3Ueh1m8k4DSe7n07qFLjHDhZ8jw==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-Oii4HpCEH3CBDKSXcS6EVlV9nGYVKAV/uBLSsuZ0RNdEG0i+OHvEiicqHAwuIYZNlH4Ea/Vwc+Dl5PM2twCZ4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -324,9 +324,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-x64-msvc": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-MqPuHCbxyLSEjavbhYapHs7cvs2zSA9GKd8nJtDuSMmDTVHFzwHfUXTffUMFB4JTCAvdpMn8XtOG/UOW5QVRCA==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-7UFjyy7QMtWvf1CBEVQkHL6bJBKaVY9yq9+Qxb7ggtxvpBbkoYykdsrhMTvr/f5TBjBqHmyeb0/oYXqo5pWFBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -338,13 +338,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/core": {
|
||||
"version": "2.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-GHiMNhcxfzJV3DqxIYYjiBGzhFkwwt+jSJl8+aVFRbQM0AYRdZJSfQDH4G5rHD1gO2yc3ktOOMHYnZWNtXCwdA==",
|
||||
"version": "2.0.0-beta.9",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.9.tgz",
|
||||
"integrity": "sha512-4sN3f72l4cj8n/dSCdWn6FkSjfHiDxHWrO1Kmqd0Bk0MmgyW+ldHitsSWPETCAxjTJGXY34r5sou5sYzb0DRww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/binding": "2.0.0-beta.8"
|
||||
"@rspack/binding": "2.0.0-beta.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -383,17 +383,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/core": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.7.tgz",
|
||||
"integrity": "sha512-+HH6EVSs1SVvm+6l78lluK8u70ihKVX26VHEqYJzTBHLtipMXllmX2mfkjCoATEP2uqU+es4xSPurss+AztHWg==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.8.tgz",
|
||||
"integrity": "sha512-MDkpm6fO0+NoW+Lx0KVL/n9DSRGQcoggeXY+EtlC+ySqF9VxQk4hu87fQhD8q2ikMOd7lbVsWmKspd3rIFD88g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@rsbuild/core": "2.0.0-beta.10",
|
||||
"@rsbuild/core": "2.0.0-beta.11",
|
||||
"@rsbuild/plugin-react": "~1.4.6",
|
||||
"@rspress/shared": "2.0.7",
|
||||
"@rspress/shared": "2.0.8",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@unhead/react": "^2.1.12",
|
||||
@@ -417,7 +417,7 @@
|
||||
"react-lazy-with-preload": "^2.2.1",
|
||||
"react-reconciler": "0.33.0",
|
||||
"react-render-to-markdown": "19.0.1",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-router-dom": "^7.13.2",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-cjk-friendly": "^2.0.1",
|
||||
@@ -443,39 +443,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-client-redirects": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.7.tgz",
|
||||
"integrity": "sha512-fH8HMUktt5ar6alSpGSDE/+dgY+TU/h0bW+4ntysUApKodNF1vg4ta60qju+5guWeRXD/35nH6rMV2Z0nW9BfQ==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.8.tgz",
|
||||
"integrity": "sha512-6/+CYf4u2PGOmuQkqvzLeUKTdOlj+Fnt3D/6IgjZmbXcSDweLvHhC+dHgdZw7T4paiqIxeCqU0duYX8W5agAug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspress/core": "^2.0.7"
|
||||
"@rspress/core": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-sitemap": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.7.tgz",
|
||||
"integrity": "sha512-K4Y8yhpiQkF+cbpfgVhdj/sfwq0K/12AusT06gfVSdJV7R4GvJO28y1GxfrsHlki8nopWu8Zqfqb3dcrNlccfA==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.8.tgz",
|
||||
"integrity": "sha512-V3u+wRvzmJmC+GkigvQYDQfEo43xUlya9OVLqSyRcB/crQ0U99oto6v73isZB/qS/pTb2wbFY+CbzOqrD1uBsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspress/core": "^2.0.7"
|
||||
"@rspress/core": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/shared": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.7.tgz",
|
||||
"integrity": "sha512-x7OqCGP5Ir1/X+6fvhtApw/ObHfwVIdWne6LlX3GGUHyHF+01yci6vrUzEP2R6PainnqySzW2+345C6zZ3dZuA==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.8.tgz",
|
||||
"integrity": "sha512-kvfBUvMvWcn/7PJHqZxPeu1yblzvAuB1/gk/1orp5KsYu3wbZ7X3Hsm9smDJVs5Plw1iPt67t9fOYNSM0+VjUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rsbuild/core": "2.0.0-beta.10",
|
||||
"@rsbuild/core": "2.0.0-beta.11",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"lodash-es": "^4.17.23",
|
||||
@@ -609,9 +609,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
|
||||
"integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3151,9 +3151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
|
||||
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
|
||||
"version": "7.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz",
|
||||
"integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3174,13 +3174,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
|
||||
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
|
||||
"version": "7.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz",
|
||||
"integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.1"
|
||||
"react-router": "7.13.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -80,7 +80,9 @@ conduwuit-macros.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
futures.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde-saphyr.workspace = true
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use lettre::message::Mailbox;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
||||
@@ -876,3 +877,31 @@ pub(super) async fn trim_memory(&self) -> Result {
|
||||
|
||||
writeln!(self, "done").await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn send_test_email(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let mailer = self.services.mailer.expect_mailer()?;
|
||||
let Some(sender) = self.sender else {
|
||||
return Err!("No sender user provided in context");
|
||||
};
|
||||
|
||||
let Some(email) = self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender.localpart())
|
||||
.await
|
||||
else {
|
||||
return Err!("{} has no associated email address", sender);
|
||||
};
|
||||
|
||||
mailer
|
||||
.send(Mailbox::new(None, email.clone()), service::mailer::messages::Test)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Test email successfully sent to {email}"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -225,6 +225,9 @@ pub enum DebugCommand {
|
||||
level: Option<i32>,
|
||||
},
|
||||
|
||||
/// Send a test email to the invoking admin's email address
|
||||
SendTestEmail,
|
||||
|
||||
/// Developer test stubs
|
||||
#[command(subcommand)]
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use clap::Subcommand;
|
||||
use conduwuit::Result;
|
||||
use ruma::OwnedUserId;
|
||||
use conduwuit::{
|
||||
Result,
|
||||
utils::{IterStream, stream::BroadbandExt},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedDeviceId, OwnedUserId};
|
||||
|
||||
use crate::Context;
|
||||
|
||||
@@ -11,6 +15,23 @@ pub enum PusherCommand {
|
||||
/// Full user ID
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
/// Deletes a specific pusher by ID
|
||||
DeletePusher {
|
||||
user_id: OwnedUserId,
|
||||
pusher_id: String,
|
||||
},
|
||||
|
||||
/// Deletes all pushers for a user
|
||||
DeleteAllUser {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
/// Deletes all pushers associated with a device ID
|
||||
DeleteAllDevice {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) async fn process(subcommand: PusherCommand, context: &Context<'_>) -> Result {
|
||||
@@ -24,6 +45,51 @@ pub(super) async fn process(subcommand: PusherCommand, context: &Context<'_>) ->
|
||||
|
||||
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
|
||||
},
|
||||
| PusherCommand::DeletePusher { user_id, pusher_id } => {
|
||||
services.pusher.delete_pusher(&user_id, &pusher_id).await;
|
||||
write!(context, "Deleted pusher {pusher_id} for {user_id}.")
|
||||
},
|
||||
| PusherCommand::DeleteAllUser { user_id } => {
|
||||
let pushers = services
|
||||
.pusher
|
||||
.get_pushkeys(&user_id)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let pusher_count = pushers.len();
|
||||
pushers
|
||||
.stream()
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&user_id, pushkey).await;
|
||||
})
|
||||
.await;
|
||||
write!(context, "Deleted {pusher_count} pushers for {user_id}.")
|
||||
},
|
||||
| PusherCommand::DeleteAllDevice { user_id, device_id } => {
|
||||
let pushers = services
|
||||
.pusher
|
||||
.get_pushkeys(&user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.as_ref()
|
||||
.is_some_and(|pusher_device| pusher_device == &device_id)
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let pusher_count = pushers.len();
|
||||
pushers
|
||||
.stream()
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&user_id, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
write!(context, "Deleted {pusher_count} pushers for {device_id}.")
|
||||
},
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
+134
-23
@@ -3,14 +3,18 @@
|
||||
fmt::Write as _,
|
||||
};
|
||||
|
||||
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
|
||||
use api::client::{
|
||||
full_user_deactivate, join_room_by_id_helper, leave_room, recreate_push_rules_and_return,
|
||||
remote_leave_room,
|
||||
};
|
||||
use conduwuit::{
|
||||
Err, Result, debug_warn, error, info,
|
||||
matrix::{Event, pdu::PduBuilder},
|
||||
matrix::{Event, pdu::PartialPdu},
|
||||
utils::{self, ReadyExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
|
||||
events::{
|
||||
@@ -728,40 +732,33 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
|
||||
let mut room_power_levels = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.ok();
|
||||
.get_room_power_levels(&room_id)
|
||||
.await;
|
||||
|
||||
let user_can_demote_self = room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(&user_id, &user_id)
|
||||
}) || self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
let user_can_demote_self =
|
||||
room_power_levels.user_can_change_user_power_level(&user_id, &user_id);
|
||||
|
||||
if !user_can_demote_self {
|
||||
return Err!("User is not allowed to modify their own power levels in the room.",);
|
||||
}
|
||||
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(&user_id);
|
||||
room_power_levels.users.remove(&user_id);
|
||||
|
||||
let event_id = self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &power_levels_content),
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
room_power_levels
|
||||
.try_into()
|
||||
.expect("PLs should be valid for room version"),
|
||||
),
|
||||
&user_id,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -924,9 +921,9 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
redacts: Some(event.event_id().to_owned()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
..PartialPdu::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(event.event_id().to_owned()),
|
||||
reason: Some(reason),
|
||||
})
|
||||
@@ -1094,3 +1091,117 @@ pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||
|
||||
self.write_str(&format!("{user_id} can now log in.")).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_email(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
match self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(user_id.localpart())
|
||||
.await
|
||||
{
|
||||
| Some(email) =>
|
||||
self.write_str(&format!("{user_id} has the associated email address {email}."))
|
||||
.await,
|
||||
| None =>
|
||||
self.write_str(&format!("{user_id} has no associated email address."))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_user_by_email(&self, email: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let Ok(email) = Address::try_from(email) else {
|
||||
return Err!("Invalid email address.");
|
||||
};
|
||||
|
||||
match self.services.threepid.get_localpart_for_email(&email).await {
|
||||
| Some(localpart) => {
|
||||
let user_id = OwnedUserId::parse(format!(
|
||||
"@{localpart}:{}",
|
||||
self.services.globals.server_name()
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
self.write_str(&format!("{email} belongs to {user_id}."))
|
||||
.await
|
||||
},
|
||||
| None =>
|
||||
self.write_str(&format!("No user has {email} as their email address."))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn change_email(&self, user_id: String, email: Option<String>) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
let Ok(new_email) = email.map(Address::try_from).transpose() else {
|
||||
return Err!("Invalid email address.");
|
||||
};
|
||||
|
||||
if self.services.mailer.mailer().is_none() {
|
||||
warn!("SMTP has not been configured on this server, emails cannot be sent.");
|
||||
}
|
||||
|
||||
let current_email = self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(user_id.localpart())
|
||||
.await;
|
||||
|
||||
match (current_email, new_email) {
|
||||
| (None, None) =>
|
||||
self.write_str(&format!(
|
||||
"{user_id} already had no associated email. No changes have been made."
|
||||
))
|
||||
.await,
|
||||
| (current_email, Some(new_email)) => {
|
||||
self.services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &new_email)
|
||||
.await?;
|
||||
|
||||
if let Some(current_email) = current_email {
|
||||
self.write_str(&format!(
|
||||
"The associated email of {user_id} has been changed from {current_email} to \
|
||||
{new_email}."
|
||||
))
|
||||
.await
|
||||
} else {
|
||||
self.write_str(&format!(
|
||||
"{user_id} has been associated with the email {new_email}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
},
|
||||
| (Some(current_email), None) => {
|
||||
self.services
|
||||
.threepid
|
||||
.disassociate_localpart_email(user_id.localpart())
|
||||
.await;
|
||||
|
||||
self.write_str(&format!(
|
||||
"The associated email of {user_id} has been removed (it was {current_email})."
|
||||
))
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn reset_push_rules(&self, user_id: String) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
if !self.services.users.is_active(&user_id).await {
|
||||
return Err!("User is not active.");
|
||||
}
|
||||
recreate_push_rules_and_return(self.services, &user_id).await?;
|
||||
self.write_str("Reset user's push rules to the server default.")
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -35,6 +35,24 @@ pub enum UserCommand {
|
||||
username: String,
|
||||
},
|
||||
|
||||
/// Get a user's associated email address.
|
||||
GetEmail {
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// Get the user with the given email address.
|
||||
GetUserByEmail {
|
||||
email: String,
|
||||
},
|
||||
|
||||
/// Update or remove a user's email address.
|
||||
///
|
||||
/// If `email` is not supplied, the user's existing address will be removed.
|
||||
ChangeEmail {
|
||||
user_id: String,
|
||||
email: Option<String>,
|
||||
},
|
||||
|
||||
/// Deactivate a user
|
||||
///
|
||||
/// User will be removed from all rooms by default.
|
||||
@@ -239,4 +257,10 @@ pub enum UserCommand {
|
||||
#[arg(long)]
|
||||
yes_i_want_to_do_this: bool,
|
||||
},
|
||||
|
||||
/// Resets the push-rules (notification settings) of the target user to the
|
||||
/// server defaults.
|
||||
ResetPushRules {
|
||||
user_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,10 +85,13 @@ http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest.workspace = true
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
ruminuwuity.workspace = true
|
||||
serde_html_form.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
OwnedRoomAliasId, continuwuity_admin_api::rooms,
|
||||
events::room::message::RoomMessageEventContent,
|
||||
};
|
||||
use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent};
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
|
||||
@@ -36,7 +34,6 @@ pub(crate) async fn ban_room(
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.ready_filter(|user| services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
let mut evicted = Vec::new();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
|
||||
use ruma::OwnedRoomId;
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -22,7 +23,7 @@ pub(crate) async fn list_rooms(
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(room_id).await {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,980 +0,0 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, debug_info, err, error, info,
|
||||
matrix::pdu::PduBuilder,
|
||||
utils::{self, ReadyExt, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
OwnedRoomId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||
deactivate, get_3pids, get_username_availability,
|
||||
register::{self, LoginType},
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
whoami,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
events::{
|
||||
GlobalAccountDataEventType, StateEventType,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
push,
|
||||
};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/v3/register/available`
|
||||
///
|
||||
/// Checks if a username is valid and available on this server.
|
||||
///
|
||||
/// Conditions for returning true:
|
||||
/// - The user id is not historical
|
||||
/// - The server name of the user id matches this server
|
||||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become
|
||||
/// invalid when trying to register
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
||||
pub(crate) async fn get_register_available_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&body.username)
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
body.username.clone()
|
||||
} else {
|
||||
body.username.to_lowercase()
|
||||
};
|
||||
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// unless the username is from the broken matrix appservice IRC bridge, we
|
||||
// should follow synapse's behaviour on not allowing things like spaces
|
||||
// and UTF-8 characters in usernames
|
||||
if !is_matrix_appservice_irc {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} contains disallowed characters or spaces: \
|
||||
{e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||
}
|
||||
}
|
||||
|
||||
if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
Ok(get_username_availability::v3::Response { available: true })
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
///
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// html) to check if the user id is valid and available.
|
||||
///
|
||||
/// - Only works if registration is enabled
|
||||
/// - If type is guest: ignores all parameters except
|
||||
/// initial_device_display_name
|
||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||
/// check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||
/// access_token
|
||||
#[allow(clippy::doc_markdown)]
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||
pub(crate) async fn register_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<register::v3::Request>,
|
||||
) -> Result<register::v3::Response> {
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Allow registration if it's enabled in the config file or if this is the first
|
||||
// run (so the first user account can be created)
|
||||
let allow_registration =
|
||||
services.config.allow_registration || services.firstrun.is_first_run();
|
||||
|
||||
if !allow_registration && body.appservice_info.is_none() {
|
||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||
| (Some(username), Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (Some(username), _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (_, Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (None, _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
info!(
|
||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||
name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||
}
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.users.count().await < 2 {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, \
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
let user_id = match (body.username.as_ref(), is_guest) {
|
||||
| (Some(username), false) => {
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc =
|
||||
body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
if services.globals.forbidden_usernames().is_match(username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
username.clone()
|
||||
} else {
|
||||
username.to_lowercase()
|
||||
};
|
||||
|
||||
let proposed_user_id = match UserId::parse_with_server_name(
|
||||
&body_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// unless the username is from the broken matrix appservice IRC bridge, or
|
||||
// we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !is_matrix_appservice_irc && !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {body_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&proposed_user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
proposed_user_id
|
||||
},
|
||||
| _ => loop {
|
||||
let proposed_user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
if !services.users.exists(&proposed_user_id).await {
|
||||
break proposed_user_id;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
match body.appservice_info {
|
||||
| Some(ref info) =>
|
||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||
return Err!(Request(Exclusive(
|
||||
"Username is not in an appservice namespace."
|
||||
)));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||
},
|
||||
}
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: Vec::new(),
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
// Populate required UIAA flows
|
||||
|
||||
if services.firstrun.is_first_run() {
|
||||
// Registration token forced while in first-run mode
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
});
|
||||
} else {
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Registration token required
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
});
|
||||
}
|
||||
|
||||
if services.config.recaptcha_private_site_key.is_some() {
|
||||
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
||||
// ReCaptcha required
|
||||
uiaainfo
|
||||
.flows
|
||||
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
|
||||
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
|
||||
"m.login.recaptcha": {
|
||||
"public_key": pubkey,
|
||||
},
|
||||
}))
|
||||
.expect("Failed to serialize recaptcha params");
|
||||
}
|
||||
}
|
||||
|
||||
if uiaainfo.flows.is_empty() && !skip_auth {
|
||||
// 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(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if !skip_auth {
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
||||
.unwrap(),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
)
|
||||
.await?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services.uiaa.create(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
||||
.unwrap(),
|
||||
"".into(),
|
||||
&uiaainfo,
|
||||
json,
|
||||
);
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let password = if is_guest { None } else { body.password.as_deref() };
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password, None).await?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||
// content is set to the user's display name with a space before it
|
||||
if !services.globals.new_user_displayname_suffix().is_empty()
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
let no_device = body.inhibit_login
|
||||
|| body
|
||||
.appservice_info
|
||||
.as_ref()
|
||||
.is_some_and(|aps| aps.registration.device_management);
|
||||
let (token, device) = if !no_device {
|
||||
// Don't create a device for inhibited logins
|
||||
let device_id = if is_guest { None } else { body.device_id.clone() }
|
||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||
|
||||
// Generate new token for the device
|
||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Create device for this account
|
||||
services
|
||||
.users
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&new_token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.await?;
|
||||
debug_info!(%user_id, %device_id, "User account was created");
|
||||
(Some(new_token), Some(device_id))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(register::v3::Response {
|
||||
access_token: token,
|
||||
user_id,
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||
pub(crate) async fn change_password_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.ready_filter(|id| *id != body.sender_device())
|
||||
.for_each(|id| services.users.remove_device(sender_user, id))
|
||||
.await;
|
||||
|
||||
// Remove all pushers except the ones associated with this session
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||
.is_some()
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {sender_user} changed their password.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} changed their password."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||
})? && body.appservice_info.is_none();
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: body.sender_user().to_owned(),
|
||||
device_id: body.sender_device.clone(),
|
||||
is_guest,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||
pub(crate) async fn deactivate_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Remove profile pictures and display name
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} deactivated their account."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
///
|
||||
/// - Currently always returns empty list
|
||||
pub(crate) async fn third_party_route(
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an phone
|
||||
/// number to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// 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> {
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
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:
|
||||
///
|
||||
/// - Mark as deactivated
|
||||
/// - Removing display name
|
||||
/// - Removing avatar URL and blurhash
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.ready_for_each(|(profile_key, _)| {
|
||||
services.users.set_profile_key(user_id, &profile_key, None);
|
||||
})
|
||||
.await;
|
||||
|
||||
// TODO: Rescind all user invites
|
||||
|
||||
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(user_id, user_id)
|
||||
}) || services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: None,
|
||||
blurhash: None,
|
||||
membership: MembershipState::Leave,
|
||||
displayname: None,
|
||||
join_authorized_via_users_server: None,
|
||||
reason: None,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
redact_events: None,
|
||||
}),
|
||||
room_id,
|
||||
));
|
||||
|
||||
// TODO: Redact all messages sent by the user in the room
|
||||
}
|
||||
|
||||
super::update_all_rooms(services, pdu_queue, user_id).await;
|
||||
for room_id in all_joined_rooms {
|
||||
services.rooms.state_cache.forget(room_id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, err, info,
|
||||
pdu::PartialPdu,
|
||||
utils::{ReadyExt, stream::BroadbandExt},
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
OwnedRoomId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||
deactivate, get_username_availability, request_password_change_token_via_email,
|
||||
whoami,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
},
|
||||
assign,
|
||||
events::room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
};
|
||||
use service::{mailer::messages, uiaa::Identity};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
pub(crate) mod register;
|
||||
pub(crate) mod threepid;
|
||||
|
||||
/// # `GET /_matrix/client/v3/register/available`
|
||||
///
|
||||
/// Checks if a username is valid and available on this server.
|
||||
///
|
||||
/// Conditions for returning true:
|
||||
/// - The user id is not historical
|
||||
/// - The server name of the user id matches this server
|
||||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become
|
||||
/// invalid when trying to register
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
||||
pub(crate) async fn get_register_available_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} contains disallowed characters or spaces: {e}",
|
||||
body.username
|
||||
))));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} is not valid: {e}",
|
||||
body.username
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||
}
|
||||
}
|
||||
|
||||
if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
Ok(get_username_availability::v3::Response::new(true))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||
pub(crate) async fn change_password_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let identity = if let Some(ref user_id) = body.sender_user {
|
||||
// A signed-in user is trying to change their password, prompt them for their
|
||||
// existing one
|
||||
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(
|
||||
&body.auth,
|
||||
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||
Box::default(),
|
||||
Some(Identity::from_user_id(user_id)),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// A signed-out user is trying to reset their password, prompt them for email
|
||||
// confirmation. Note that we do not _send_ an email here, their client should
|
||||
// have already hit `/account/password/requestToken` to send the email. We
|
||||
// just validate it.
|
||||
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(
|
||||
&body.auth,
|
||||
vec![AuthFlow::new(vec![AuthType::EmailIdentity])],
|
||||
Box::default(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let sender_user = UserId::parse(format!(
|
||||
"@{}:{}",
|
||||
identity.localpart.expect("localpart should be known"),
|
||||
services.globals.server_name()
|
||||
))
|
||||
.expect("user ID should be valid");
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(&sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
services
|
||||
.users
|
||||
.all_device_ids(&sender_user)
|
||||
.ready_filter(|id| *id != body.sender_device())
|
||||
.for_each(async |id| services.users.remove_device(&sender_user, &id).await)
|
||||
.await;
|
||||
|
||||
// Remove all pushers except the ones associated with this session
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(&sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||
.is_some()
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {} changed their password.", &sender_user);
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {} changed their password.", &sender_user))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of resetting a user's password.
|
||||
pub(crate) async fn request_password_change_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
||||
) -> Result<request_password_change_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
||||
return Err!(Request(ThreepidNotFound(
|
||||
"No account is associated with this email address"
|
||||
)));
|
||||
};
|
||||
|
||||
let user_id =
|
||||
UserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(display_name.clone(), email),
|
||||
|verification_link| messages::PasswordReset {
|
||||
display_name: display_name.as_deref(),
|
||||
user_id: &user_id,
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_password_change_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||
})? && body.appservice_info.is_none();
|
||||
|
||||
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), is_guest), {
|
||||
device_id: body.sender_device.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||
pub(crate) async fn deactivate_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint is technically optional,
|
||||
// but we require the user to be logged in
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
// Remove profile pictures and display name
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} deactivated their account."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response::new(ThirdPartyIdRemovalStatus::Success))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// 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> {
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response::new(valid))
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
///
|
||||
/// - Mark as deactivated
|
||||
/// - Removing display name
|
||||
/// - Removing avatar URL and blurhash
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
if services.globals.user_is_local(user_id) {
|
||||
let _ = services
|
||||
.threepid
|
||||
.disassociate_localpart_email(user_id.localpart())
|
||||
.await;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.ready_for_each(|(profile_key, _)| {
|
||||
services.users.set_profile_key(user_id, &profile_key, None);
|
||||
})
|
||||
.await;
|
||||
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(user_id)
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(user_id, pushkey).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
// TODO: Rescind all user invites
|
||||
|
||||
let mut pdu_queue: Vec<(PartialPdu, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_power_levels(&room_id)
|
||||
.await;
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels.user_can_change_user_power_level(user_id, user_id);
|
||||
|
||||
if user_can_demote_self
|
||||
&& let Ok(mut power_levels_content) =
|
||||
RoomPowerLevelsEventContent::try_from(room_power_levels)
|
||||
{
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PartialPdu::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PartialPdu::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Leave),
|
||||
),
|
||||
room_id,
|
||||
));
|
||||
|
||||
// TODO: Redact all messages sent by the user in the room
|
||||
}
|
||||
|
||||
super::update_all_rooms(services, pdu_queue, user_id).await;
|
||||
for room_id in all_joined_rooms {
|
||||
services.rooms.state_cache.forget(room_id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
use std::{collections::HashMap, fmt::Write};
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug_info, error, info,
|
||||
utils::{self},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
register::{self, LoginType},
|
||||
request_registration_token_via_email,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
GlobalAccountDataEventType, push_rules::PushRulesEvent,
|
||||
room::message::RoomMessageEventContent,
|
||||
},
|
||||
push,
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use service::mailer::messages;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
///
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// html) to check if the user id is valid and available.
|
||||
///
|
||||
/// - Only works if registration is enabled
|
||||
/// - If type is guest: ignores all parameters except
|
||||
/// initial_device_display_name
|
||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||
/// check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||
/// access_token
|
||||
#[allow(clippy::doc_markdown)]
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||
pub(crate) async fn register_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<register::v3::Request>,
|
||||
) -> Result<register::v3::Response> {
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Allow registration if it's enabled in the config file or if this is the first
|
||||
// run (so the first user account can be created)
|
||||
let allow_registration =
|
||||
services.config.allow_registration || services.firstrun.is_first_run();
|
||||
|
||||
if !allow_registration && body.appservice_info.is_none() {
|
||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||
| (Some(username), Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (Some(username), _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (_, Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (None, _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
info!(
|
||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||
name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||
}
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.firstrun.is_first_run() {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, \
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// Appeservices and guests get to skip auth
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
let identity = if skip_auth {
|
||||
// Appservices and guests have no identity
|
||||
None
|
||||
} else {
|
||||
// Perform UIAA to determine the user's identity
|
||||
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
||||
|
||||
Some(
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(&body.auth, flows, params, None)
|
||||
.await?,
|
||||
)
|
||||
};
|
||||
|
||||
// If the user didn't supply a username but did supply an email, use
|
||||
// the email's user as their initial localpart to avoid falling back to
|
||||
// a randomly generated localpart
|
||||
let supplied_username = body.username.clone().or_else(|| {
|
||||
if let Some(identity) = &identity
|
||||
&& let Some(email) = &identity.email
|
||||
{
|
||||
Some(email.user().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let user_id = determine_registration_user_id(
|
||||
&services,
|
||||
supplied_username,
|
||||
is_guest,
|
||||
emergency_mode_enabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
// For appservice logins, make sure that the user ID is in the appservice's
|
||||
// namespace
|
||||
|
||||
match body.appservice_info {
|
||||
| Some(ref info) =>
|
||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||
return Err!(Request(Exclusive(
|
||||
"Username is not in an appservice namespace."
|
||||
)));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||
},
|
||||
}
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||
{
|
||||
// For non-appservice logins, ban user IDs which are in an appservice's
|
||||
// namespace (unless emergency mode is enabled)
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
let password = if is_guest { None } else { body.password.as_deref() };
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password, None).await?;
|
||||
|
||||
// Set an initial display name
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// Apply the new user displayname suffix, if it's set
|
||||
if !services.globals.new_user_displayname_suffix().is_empty()
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent::new(
|
||||
push::Ruleset::server_default(&user_id).into(),
|
||||
))
|
||||
.expect("should be able to serialize push rules"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
let (token, device) = if !body.inhibit_login {
|
||||
let device_id = if is_guest { None } else { body.device_id.clone() }
|
||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||
|
||||
// Generate new token for the device
|
||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Create device for this account
|
||||
services
|
||||
.users
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&new_token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.await?;
|
||||
(Some(new_token), Some(device_id))
|
||||
} else {
|
||||
// Don't create a device for inhibited logins
|
||||
(None, None)
|
||||
};
|
||||
|
||||
debug_info!(%user_id, ?device, "User account was created");
|
||||
|
||||
// If the user registered with an email, associate it with their account.
|
||||
if let Some(identity) = identity
|
||||
&& let Some(email) = identity.email
|
||||
{
|
||||
// This may fail if the email is already in use, but we already check for that
|
||||
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||
// that an email is sniped by another user between the `/requestToken` request
|
||||
// and the `/register` request.
|
||||
let _ = services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &email)
|
||||
.await;
|
||||
}
|
||||
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(assign!(register::v3::Response::new(user_id), {
|
||||
access_token: token,
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Determine which flows and parameters should be presented when
|
||||
/// registering a new account.
|
||||
async fn create_registration_uiaa_session(
|
||||
services: &Services,
|
||||
) -> Result<(Vec<AuthFlow>, Box<RawValue>)> {
|
||||
let mut params = HashMap::<String, serde_json::Value>::new();
|
||||
|
||||
let flows = if services.firstrun.is_first_run() {
|
||||
// Registration token forced while in first-run mode
|
||||
vec![AuthFlow::new(vec![AuthType::RegistrationToken])]
|
||||
} else {
|
||||
let mut flows = vec![];
|
||||
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Trusted registration flow with a token is available
|
||||
let mut token_flow = AuthFlow::new(vec![AuthType::RegistrationToken]);
|
||||
|
||||
if let Some(smtp) = &services.config.smtp
|
||||
&& smtp.require_email_for_token_registration
|
||||
{
|
||||
// Email is required for token registrations
|
||||
token_flow.stages.push(AuthType::EmailIdentity);
|
||||
}
|
||||
|
||||
flows.push(token_flow);
|
||||
}
|
||||
|
||||
let mut untrusted_flow = AuthFlow::default();
|
||||
|
||||
if services.config.recaptcha_private_site_key.is_some() {
|
||||
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
||||
// ReCaptcha is configured for untrusted registrations
|
||||
untrusted_flow.stages.push(AuthType::ReCaptcha);
|
||||
|
||||
params.insert(
|
||||
AuthType::ReCaptcha.as_str().to_owned(),
|
||||
serde_json::json!({
|
||||
"public_key": pubkey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(smtp) = &services.config.smtp
|
||||
&& smtp.require_email_for_registration
|
||||
{
|
||||
// Email is required for untrusted registrations
|
||||
untrusted_flow.stages.push(AuthType::EmailIdentity);
|
||||
}
|
||||
|
||||
if !untrusted_flow.stages.is_empty() {
|
||||
flows.push(untrusted_flow);
|
||||
}
|
||||
|
||||
if flows.is_empty() {
|
||||
// No flows are configured. 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 flow
|
||||
flows.push(AuthFlow::new(vec![AuthType::Dummy]));
|
||||
}
|
||||
|
||||
flows
|
||||
};
|
||||
|
||||
let params = serde_json::value::to_raw_value(¶ms).expect("params should be valid JSON");
|
||||
|
||||
Ok((flows, params))
|
||||
}
|
||||
|
||||
async fn determine_registration_user_id(
|
||||
services: &Services,
|
||||
supplied_username: Option<String>,
|
||||
is_guest: bool,
|
||||
emergency_mode_enabled: bool,
|
||||
) -> Result<OwnedUserId> {
|
||||
if let Some(supplied_username) = supplied_username
|
||||
&& !is_guest
|
||||
{
|
||||
// The user gets to pick their username. Do some validation to make sure it's
|
||||
// acceptable.
|
||||
|
||||
// Don't allow registration with forbidden usernames.
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&supplied_username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// Create and validate the user ID
|
||||
let user_id = match UserId::parse_with_server_name(
|
||||
&supplied_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {supplied_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
} else {
|
||||
// The user is a guest or didn't specify a username. Generate a username for
|
||||
// them.
|
||||
|
||||
loop {
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
break Ok(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of registering a new account.
|
||||
pub(crate) async fn request_registration_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_registration_token_via_email::v3::Request>,
|
||||
) -> Result<request_registration_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||
}
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(None, email),
|
||||
|verification_link| messages::NewAccount {
|
||||
server_name: services.config.server_name.as_ref(),
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_registration_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, err};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
api::client::account::{
|
||||
ThirdPartyIdRemovalStatus, add_3pid, delete_3pid, get_3pids,
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
},
|
||||
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||
};
|
||||
use service::{mailer::messages, uiaa::Identity};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
pub(crate) async fn third_party_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let mut threepids = vec![];
|
||||
|
||||
if let Some(email) = services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender_user.localpart())
|
||||
.await
|
||||
{
|
||||
threepids.push(
|
||||
ThirdPartyIdentifierInit {
|
||||
address: email.to_string(),
|
||||
medium: Medium::Email,
|
||||
// We don't currently track these, and they aren't used for much
|
||||
validated_at: MilliSecondsSinceUnixEpoch::now(),
|
||||
added_at: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::UNIX_EPOCH)
|
||||
.unwrap(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(get_3pids::v3::Response::new(threepids))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of changing an account's email.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||
}
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(None, email),
|
||||
|verification_link| messages::ChangeEmail {
|
||||
server_name: services.config.server_name.as_str(),
|
||||
user_id: body.sender_user.as_deref(),
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_3pid_management_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err!(Request(ThreepidMediumNotSupported(
|
||||
"MSISDN third-party identifiers are not supported."
|
||||
)))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/add`
|
||||
pub(crate) async fn add_3pid_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<add_3pid::v3::Request>,
|
||||
) -> Result<add_3pid::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
// Require password auth to add an email
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
let email = services
|
||||
.threepid
|
||||
.consume_valid_session(&body.sid, &body.client_secret)
|
||||
.await
|
||||
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||
|
||||
services
|
||||
.threepid
|
||||
.associate_localpart_email(sender_user.localpart(), &email)
|
||||
.await?;
|
||||
|
||||
Ok(add_3pid::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/delete`
|
||||
pub(crate) async fn delete_3pid_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_3pid::v3::Request>,
|
||||
) -> Result<delete_3pid::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if body.medium != Medium::Email {
|
||||
return Ok(delete_3pid::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
});
|
||||
}
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.disassociate_localpart_email(sender_user.localpart())
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
|
||||
}
|
||||
|
||||
Ok(delete_3pid::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||
})
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub(crate) async fn set_global_account_data_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_global_account_data::v3::Response {})
|
||||
Ok(set_global_account_data::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
@@ -65,7 +65,7 @@ pub(crate) async fn set_room_account_data_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_room_account_data::v3::Response {})
|
||||
Ok(set_room_account_data::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
@@ -119,7 +119,7 @@ async fn set_account_data(
|
||||
event_type_s: &str,
|
||||
data: &RawJsonValue,
|
||||
) -> Result {
|
||||
if event_type_s == RoomAccountDataEventType::FullyRead.to_cow_str() {
|
||||
if event_type_s == "m.fully_read" {
|
||||
return Err!(Request(BadJson(
|
||||
"This endpoint cannot be used for marking a room as fully read (setting \
|
||||
m.fully_read)"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruma::api::client::admin::{get_suspended, set_suspended};
|
||||
use ruminuwuity::admin::{get_suspended, set_suspended};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
|
||||
@@ -47,5 +47,5 @@ pub(crate) async fn appservice_ping(
|
||||
.await?
|
||||
.expect("We already validated if an appservice URL exists above");
|
||||
|
||||
Ok(request_ping::v1::Response { duration: timer.elapsed() })
|
||||
Ok(request_ping::v1::Response::new(timer.elapsed()))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) async fn create_backup_version_route(
|
||||
.key_backups
|
||||
.create_backup(body.sender_user(), &body.algorithm)?;
|
||||
|
||||
Ok(create_backup_version::v3::Response { version })
|
||||
Ok(create_backup_version::v3::Response::new(version))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
|
||||
@@ -44,7 +44,7 @@ pub(crate) async fn update_backup_version_route(
|
||||
.update_backup(body.sender_user(), &body.version, &body.algorithm)
|
||||
.await?;
|
||||
|
||||
Ok(update_backup_version::v3::Response {})
|
||||
Ok(update_backup_version::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
@@ -105,7 +105,7 @@ pub(crate) async fn delete_backup_version_route(
|
||||
.delete_backup(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_version::v3::Response {})
|
||||
Ok(delete_backup_version::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys`
|
||||
@@ -292,7 +292,7 @@ pub(crate) async fn get_backup_keys_route(
|
||||
.get_all(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys::v3::Response { rooms })
|
||||
Ok(get_backup_keys::v3::Response::new(rooms))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
@@ -307,7 +307,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
|
||||
.get_room(body.sender_user(), &body.version, &body.room_id)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys_for_room::v3::Response { sessions })
|
||||
Ok(get_backup_keys_for_room::v3::Response::new(sessions))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
@@ -325,7 +325,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
|
||||
err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
|
||||
})?;
|
||||
|
||||
Ok(get_backup_keys_for_session::v3::Response { key_data })
|
||||
Ok(get_backup_keys_for_session::v3::Response::new(key_data))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys`
|
||||
@@ -342,7 +342,7 @@ pub(crate) async fn delete_backup_keys_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
@@ -359,7 +359,7 @@ pub(crate) async fn delete_backup_keys_for_room_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys_for_room::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
@@ -376,7 +376,7 @@ pub(crate) async fn delete_backup_keys_for_session_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys_for_session::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
async fn get_count_etag(
|
||||
|
||||
@@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route(
|
||||
default: services.server.config.default_room_version.clone(),
|
||||
};
|
||||
|
||||
// we do not implement 3PID stuff
|
||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };
|
||||
// Only allow 3pid changes if SMTP is configured
|
||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
|
||||
enabled: services.mailer.mailer().is_some(),
|
||||
};
|
||||
|
||||
capabilities.get_login_token = GetLoginTokenCapability {
|
||||
enabled: services.server.config.login_via_existing_session,
|
||||
@@ -51,8 +53,8 @@ pub(crate) async fn get_capabilities_route(
|
||||
.await
|
||||
{
|
||||
// Advertise suspension API
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend":true, "lock": false}))?;
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
|
||||
}
|
||||
|
||||
Ok(get_capabilities::v3::Response { capabilities })
|
||||
Ok(get_capabilities::v3::Response::new(capabilities))
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) async fn put_dehydrated_device_route(
|
||||
.set_dehydrated_device(sender_user, body.body)
|
||||
.await?;
|
||||
|
||||
Ok(put_dehydrated_device::Response { device_id })
|
||||
Ok(put_dehydrated_device::Response::new(device_id))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/../dehydrated_device`
|
||||
@@ -51,7 +51,7 @@ pub(crate) async fn delete_dehydrated_device_route(
|
||||
|
||||
services.users.remove_device(sender_user, &device_id).await;
|
||||
|
||||
Ok(delete_dehydrated_device::Response { device_id })
|
||||
Ok(delete_dehydrated_device::Response::new(device_id))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device`
|
||||
@@ -67,10 +67,7 @@ pub(crate) async fn get_dehydrated_device_route(
|
||||
|
||||
let device = services.users.get_dehydrated_device(sender_user).await?;
|
||||
|
||||
Ok(get_dehydrated_device::Response {
|
||||
device_id: device.device_id,
|
||||
device_data: device.device_data,
|
||||
})
|
||||
Ok(get_dehydrated_device::Response::new(device.device_id, device.device_data))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
|
||||
|
||||
+25
-93
@@ -1,17 +1,15 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Error, Result, debug, err, utils};
|
||||
use conduwuit::{Err, Result, debug, err, utils};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
||||
api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
api::client::device::{
|
||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||
},
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices`
|
||||
@@ -27,7 +25,7 @@ pub(crate) async fn get_devices_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(get_devices::v3::Response { devices })
|
||||
Ok(get_devices::v3::Response::new(devices))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices/{deviceId}`
|
||||
@@ -43,7 +41,7 @@ pub(crate) async fn get_device_route(
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
|
||||
|
||||
Ok(get_device::v3::Response { device })
|
||||
Ok(get_device::v3::Response::new(device))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
@@ -75,19 +73,16 @@ pub(crate) async fn update_device_route(
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)
|
||||
.await?;
|
||||
|
||||
Ok(update_device::v3::Response {})
|
||||
Ok(update_device::v3::Response::new())
|
||||
},
|
||||
| Err(_) => {
|
||||
let Some(appservice) = appservice else {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
};
|
||||
if !appservice.registration.device_management {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
|
||||
and device ID does not exist",
|
||||
"Creating new device for {sender_user} from appservice {} as device ID does not \
|
||||
exist",
|
||||
appservice.registration.id
|
||||
);
|
||||
|
||||
@@ -104,7 +99,7 @@ pub(crate) async fn update_device_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(update_device::v3::Response {});
|
||||
return Ok(update_device::v3::Response::new());
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -123,57 +118,24 @@ pub(crate) async fn delete_device_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_device::v3::Request>,
|
||||
) -> Result<delete_device::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let sender_user = body.sender_user();
|
||||
let appservice = body.appservice_info.as_ref();
|
||||
|
||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||
debug!(
|
||||
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
|
||||
enabled"
|
||||
);
|
||||
if appservice.is_some() {
|
||||
debug!("Skipping UIAA for {sender_user} as this is from an appservice");
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, &body.device_id)
|
||||
.await;
|
||||
|
||||
return Ok(delete_device::v3::Response {});
|
||||
return Ok(delete_device::v3::Response::new());
|
||||
}
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("Not json.")));
|
||||
},
|
||||
},
|
||||
}
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
services
|
||||
.users
|
||||
@@ -200,7 +162,7 @@ pub(crate) async fn delete_devices_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_devices::v3::Request>,
|
||||
) -> Result<delete_devices::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let sender_user = body.sender_user();
|
||||
let appservice = body.appservice_info.as_ref();
|
||||
|
||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||
@@ -215,41 +177,11 @@ pub(crate) async fn delete_devices_route(
|
||||
return Ok(delete_devices::v3::Response {});
|
||||
}
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
},
|
||||
},
|
||||
}
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
for device_id in &body.devices {
|
||||
services.users.remove_device(sender_user, device_id).await;
|
||||
|
||||
+10
-36
@@ -26,7 +26,7 @@
|
||||
},
|
||||
federation,
|
||||
},
|
||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
@@ -34,6 +34,7 @@
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
room::JoinRuleKind,
|
||||
uint,
|
||||
};
|
||||
|
||||
@@ -142,7 +143,13 @@ pub(crate) async fn set_room_visibility_route(
|
||||
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
|
||||
}
|
||||
|
||||
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_power_levels(&body.room_id)
|
||||
.await;
|
||||
|
||||
if !room_power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility) {
|
||||
return Err!(Request(Forbidden("User is not allowed to publish this room")));
|
||||
}
|
||||
|
||||
@@ -339,39 +346,6 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether the user can publish to the room directory via power levels of
|
||||
/// room history visibility event or room creator
|
||||
async fn user_can_publish_room(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
) -> Result<bool> {
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
{
|
||||
| Ok(event) => serde_json::from_str(event.content().get())
|
||||
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
|
||||
.map(|content: RoomPowerLevelsEventContent| {
|
||||
RoomPowerLevels::from(content)
|
||||
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
||||
}),
|
||||
| _ => {
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
{
|
||||
| Ok(event) => Ok(event.sender() == user_id),
|
||||
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
|
||||
let name = services.rooms.state_accessor.get_name(&room_id).ok();
|
||||
|
||||
@@ -394,7 +368,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
||||
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
| JoinRule::Public => PublicRoomJoinRule::Public,
|
||||
| JoinRule::Public => JoinRuleKind::Public,
|
||||
| JoinRule::Knock => "knock".into(),
|
||||
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
||||
| _ => "invite".into(),
|
||||
|
||||
+6
-39
@@ -7,7 +7,6 @@
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, debug_warn, err,
|
||||
result::NotFound,
|
||||
utils,
|
||||
utils::{IterStream, stream::WidebandExt},
|
||||
};
|
||||
use conduwuit_service::{Services, users::parse_master_key};
|
||||
@@ -22,7 +21,6 @@
|
||||
upload_signatures::{self},
|
||||
upload_signing_keys,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
@@ -30,8 +28,8 @@
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/upload`
|
||||
@@ -174,16 +172,7 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<upload_signing_keys::v3::Request>,
|
||||
) -> Result<upload_signing_keys::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
match check_for_new_keys(
|
||||
services,
|
||||
@@ -207,32 +196,10 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
// Some of the keys weren't found, so we let them upload
|
||||
},
|
||||
| _ => {
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body.as_ref() {
|
||||
| Some(json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
},
|
||||
},
|
||||
}
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
};
|
||||
use reqwest::Url;
|
||||
use ruma::{
|
||||
Mxc, UserId,
|
||||
UserId,
|
||||
api::client::{
|
||||
authenticated_media::{
|
||||
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::membership::ban_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -24,30 +24,29 @@ pub(crate) async fn ban_user_route(
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let current_member_content = services
|
||||
let mut content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
|
||||
|
||||
content.membership = MembershipState::Ban;
|
||||
content.reason = body.reason.clone();
|
||||
content.displayname = None;
|
||||
content.avatar_url = None;
|
||||
content.is_direct = None;
|
||||
content.join_authorized_via_users_server = None;
|
||||
content.third_party_invite = None;
|
||||
// TODO(upstream): MSC4293
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Ban,
|
||||
reason: body.reason.clone(),
|
||||
displayname: None, // display name may be offensive
|
||||
avatar_url: None, // avatar may be offensive
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
redact_events: body.redact_events,
|
||||
..current_member_content
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), &content),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug_error, err, info,
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
|
||||
warn,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
RoomId, UserId,
|
||||
api::{client::membership::invite_user, federation::membership::create_invite},
|
||||
events::{
|
||||
invite_permission_config::FilterLevel,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
api::{
|
||||
client::membership::invite_user::{self, v3::InviteUserId},
|
||||
federation::membership::{RawStrippedState, create_invite},
|
||||
},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use ruminuwuity::invite_permission_config::FilterLevel;
|
||||
use service::Services;
|
||||
|
||||
use super::banned_room_check;
|
||||
@@ -51,7 +52,11 @@ pub(crate) async fn invite_user_route(
|
||||
.await?;
|
||||
|
||||
match &body.recipient {
|
||||
| invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
|
||||
| invite_user::v3::InvitationRecipient::UserId(InviteUserId {
|
||||
user_id: recipient_user,
|
||||
reason,
|
||||
..
|
||||
}) => {
|
||||
let sender_filter_level = services
|
||||
.users
|
||||
.invite_filter_level(recipient_user, sender_user)
|
||||
@@ -59,7 +64,7 @@ pub(crate) async fn invite_user_route(
|
||||
|
||||
if !matches!(sender_filter_level, FilterLevel::Allow) {
|
||||
// drop invites if the sender has the recipient filtered
|
||||
return Ok(invite_user::v3::Response {});
|
||||
return Ok(invite_user::v3::Response::new());
|
||||
}
|
||||
|
||||
if let Ok(target_user_membership) = services
|
||||
@@ -95,13 +100,13 @@ pub(crate) async fn invite_user_route(
|
||||
sender_user,
|
||||
recipient_user,
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
reason.clone(),
|
||||
false,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(invite_user::v3::Response {})
|
||||
Ok(invite_user::v3::Response::new())
|
||||
},
|
||||
| _ => {
|
||||
Err!(Request(NotFound("User not found.")))
|
||||
@@ -141,25 +146,32 @@ pub(crate) async fn invite_helper(
|
||||
let (pdu, pdu_json, invite_room_state) = {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
|
||||
is_direct: Some(is_direct),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Invite)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
|
||||
content.displayname = services.users.displayname(recipient_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
|
||||
content.is_direct = Some(is_direct);
|
||||
content.reason = reason;
|
||||
|
||||
let (pdu, pdu_json) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(recipient_user.to_string(), &content),
|
||||
PartialPdu::state(recipient_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
|
||||
#[allow(deprecated)]
|
||||
let invite_room_state = services
|
||||
.rooms
|
||||
.state
|
||||
.summary_stripped(&pdu, room_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|event| RawStrippedState::Stripped(event))
|
||||
.collect();
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
@@ -168,32 +180,39 @@ pub(crate) async fn invite_helper(
|
||||
|
||||
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
|
||||
|
||||
let mut request = create_invite::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
(*pdu.event_id).to_owned(),
|
||||
room_version_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await,
|
||||
invite_room_state,
|
||||
);
|
||||
request.via = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_route_via(room_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let response = services
|
||||
.sending
|
||||
.send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: (*pdu.event_id).to_owned(),
|
||||
room_version: room_version_id.clone(),
|
||||
event: services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await,
|
||||
invite_room_state,
|
||||
via: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_route_via(room_id)
|
||||
.await
|
||||
.ok(),
|
||||
})
|
||||
.send_federation_request(recipient_user.server_name(), request)
|
||||
.await?;
|
||||
|
||||
// We do not add the event_id field to the pdu here because of signature and
|
||||
// hashes checks
|
||||
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
|
||||
})?;
|
||||
let (event_id, value) = gen_event_id_canonical_json(
|
||||
&response.event,
|
||||
&room_version_id
|
||||
.rules()
|
||||
.expect("room version should have defined rules"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
|
||||
})?;
|
||||
|
||||
if pdu.event_id != event_id {
|
||||
return Err!(Request(BadJson(warn!(
|
||||
@@ -229,19 +248,17 @@ pub(crate) async fn invite_helper(
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(recipient_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
|
||||
blurhash: services.users.blurhash(recipient_user).await.ok(),
|
||||
is_direct: Some(is_direct),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Invite)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
|
||||
content.displayname = services.users.displayname(recipient_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(recipient_user).await.ok();
|
||||
content.is_direct = Some(is_direct);
|
||||
content.reason = reason;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(recipient_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
matrix::{
|
||||
StateKey,
|
||||
event::{gen_event_id, gen_event_id_canonical_json},
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
pdu::{PartialPdu, PduEvent},
|
||||
state_res,
|
||||
},
|
||||
result::FlatOk,
|
||||
@@ -24,10 +24,8 @@
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
|
||||
RoomVersionId, UserId,
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
},
|
||||
client::membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
error::{ErrorKind, IncompatibleRoomVersionErrorData},
|
||||
federation::{self},
|
||||
},
|
||||
canonical_json::to_canonical_value,
|
||||
@@ -89,7 +87,6 @@ pub(crate) async fn join_room_by_id_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
@@ -168,7 +165,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
@@ -209,11 +205,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let addl_via_servers = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
|
||||
|
||||
let addl_state_servers = services
|
||||
.rooms
|
||||
@@ -226,7 +218,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.iter()
|
||||
.map(|event| event.get_field("sender"))
|
||||
.filter_map(FlatOk::flat_ok)
|
||||
.map(|user: &UserId| user.server_name().to_owned())
|
||||
.map(|user: OwnedUserId| user.server_name().to_owned())
|
||||
.stream()
|
||||
.chain(addl_via_servers)
|
||||
.collect()
|
||||
@@ -252,7 +244,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
|
||||
Ok(join_room_by_id_or_alias::v3::Response::new(join_room_response.room_id))
|
||||
}
|
||||
|
||||
pub async fn join_room_by_id_helper(
|
||||
@@ -283,7 +275,7 @@ pub async fn join_room_by_id_helper(
|
||||
.await
|
||||
{
|
||||
debug_warn!("{sender_user} is already joined in {room_id}");
|
||||
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
|
||||
return Ok(join_room_by_id::v3::Response::new(room_id.to_owned()));
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
@@ -383,12 +375,15 @@ async fn join_room_by_id_helper_remote(
|
||||
|
||||
info!("make_join finished");
|
||||
|
||||
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
|
||||
let room_version = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
// How did we get here?
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
"Remote room version {room_version} is not supported by conduwuit"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -401,7 +396,7 @@ async fn join_room_by_id_helper_remote(
|
||||
|
||||
let join_authorized_via_users_server = {
|
||||
use RoomVersionId::*;
|
||||
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
join_event_stub
|
||||
.get("content")
|
||||
.map(|s| {
|
||||
@@ -423,36 +418,30 @@ async fn join_room_by_id_helper_remote(
|
||||
.expect("Timestamp is valid js_int value"),
|
||||
),
|
||||
);
|
||||
|
||||
let mut join_content = RoomMemberEventContent::new(MembershipState::Join);
|
||||
join_content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
join_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
join_content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
join_content.reason = reason;
|
||||
join_content.join_authorized_via_users_server = join_authorized_via_users_server.clone();
|
||||
|
||||
join_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(join_content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// We keep the "event_id" in the pdu only in v1 or
|
||||
// v2 rooms
|
||||
match room_version_id {
|
||||
| RoomVersionId::V1 | RoomVersionId::V2 => {},
|
||||
| _ => {
|
||||
join_event_stub.remove("event_id");
|
||||
},
|
||||
}
|
||||
// Remove event id if it exists
|
||||
join_event_stub.remove("event_id");
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut join_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&join_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id back
|
||||
join_event_stub
|
||||
@@ -462,15 +451,14 @@ async fn join_room_by_id_helper_remote(
|
||||
let mut join_event = join_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_join in room {room_id}");
|
||||
let send_join_request = federation::membership::create_join_event::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
omit_members: false,
|
||||
pdu: services
|
||||
let send_join_request = federation::membership::create_join_event::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(join_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
let send_join_response = match services
|
||||
.sending
|
||||
@@ -494,7 +482,7 @@ async fn join_room_by_id_helper_remote(
|
||||
);
|
||||
|
||||
let (signed_event_id, signed_value) =
|
||||
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
|
||||
gen_event_id_canonical_json(signed_raw, &room_version_rules).map_err(|e| {
|
||||
err!(Request(BadJson(warn!(
|
||||
"Could not convert event to canonical JSON: {e}"
|
||||
))))
|
||||
@@ -569,7 +557,7 @@ async fn join_room_by_id_helper_remote(
|
||||
.then(|pdu| {
|
||||
services
|
||||
.server_keys
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
|
||||
.inspect_err(|e| {
|
||||
debug_warn!("Could not validate send_join response room_state event: {e:?}");
|
||||
})
|
||||
@@ -617,7 +605,7 @@ async fn join_room_by_id_helper_remote(
|
||||
.then(|pdu| {
|
||||
services
|
||||
.server_keys
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
|
||||
})
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_for_each(|(event_id, value)| {
|
||||
@@ -638,7 +626,7 @@ async fn join_room_by_id_helper_remote(
|
||||
};
|
||||
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&state_res::RoomVersion::new(&room_version_id)?,
|
||||
&room_version.rules().unwrap(),
|
||||
&parsed_join_pdu,
|
||||
None, // TODO: third party invite
|
||||
|k, s| state_fetch(k.clone(), s.into()),
|
||||
@@ -759,21 +747,19 @@ async fn join_room_by_id_helper_local(
|
||||
}
|
||||
}
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason: reason.clone(),
|
||||
join_authorized_via_users_server: auth_user,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Join);
|
||||
content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
content.reason = reason.clone();
|
||||
content.join_authorized_via_users_server = auth_user;
|
||||
|
||||
// Try normal join first
|
||||
let Err(error) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &content),
|
||||
PartialPdu::state(sender_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -785,6 +771,15 @@ async fn join_room_by_id_helper_local(
|
||||
};
|
||||
|
||||
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
|
||||
if !services.rooms.metadata.exists(room_id).await {
|
||||
return Err!(Request(
|
||||
Unknown(
|
||||
"Room was not found locally and no servers were found to help us discover it"
|
||||
),
|
||||
NOT_FOUND
|
||||
));
|
||||
}
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
@@ -813,16 +808,16 @@ async fn make_join_request(
|
||||
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
|
||||
servers.len()
|
||||
);
|
||||
|
||||
let mut request = federation::membership::prepare_join_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
sender_user.to_owned(),
|
||||
);
|
||||
request.ver = services.server.supported_room_versions().collect();
|
||||
|
||||
let make_join_response = services
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server,
|
||||
federation::membership::prepare_join_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: sender_user.to_owned(),
|
||||
ver: services.server.supported_room_versions().collect(),
|
||||
},
|
||||
)
|
||||
.send_federation_request(remote_server, request)
|
||||
.await;
|
||||
|
||||
trace!("make_join response: {:?}", make_join_response);
|
||||
@@ -855,7 +850,10 @@ async fn make_join_request(
|
||||
rules, but is unable to authorise a join for us. Will continue trying."
|
||||
);
|
||||
},
|
||||
| ErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
| ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
|
||||
room_version,
|
||||
..
|
||||
}) => {
|
||||
warn!(
|
||||
"{remote_server} reports the room we are trying to join is \
|
||||
v{room_version}, which we do not support."
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use ruma::{
|
||||
api::client::membership::kick_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{api::client::membership::kick_user, events::room::member::MembershipState};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -18,9 +15,9 @@ pub(crate) async fn kick_user_route(
|
||||
if services.users.is_suspended(sender_user).await? {
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let Ok(event) = services
|
||||
let Ok(mut event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
@@ -41,18 +38,17 @@ pub(crate) async fn kick_user_route(
|
||||
)));
|
||||
}
|
||||
|
||||
event.membership = MembershipState::Leave;
|
||||
event.reason = body.reason.clone();
|
||||
event.is_direct = None;
|
||||
event.join_authorized_via_users_server = None;
|
||||
event.third_party_invite = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: body.reason.clone(),
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
..event
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), &event),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{
|
||||
event::gen_event_id,
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
pdu::{PartialPdu, PduEvent},
|
||||
},
|
||||
result::FlatOk,
|
||||
trace,
|
||||
@@ -15,8 +15,8 @@
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
|
||||
RoomVersionId, UserId,
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName,
|
||||
OwnedUserId, RoomId, UserId,
|
||||
api::{
|
||||
client::knock::knock_room,
|
||||
federation::{self},
|
||||
@@ -73,7 +73,6 @@ pub(crate) async fn knock_room_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
@@ -113,11 +112,7 @@ pub(crate) async fn knock_room_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let addl_via_servers = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
|
||||
|
||||
let addl_state_servers = services
|
||||
.rooms
|
||||
@@ -130,7 +125,7 @@ pub(crate) async fn knock_room_route(
|
||||
.iter()
|
||||
.map(|event| event.get_field("sender"))
|
||||
.filter_map(FlatOk::flat_ok)
|
||||
.map(|user: &UserId| user.server_name().to_owned())
|
||||
.map(|user: OwnedUserId| user.server_name().to_owned())
|
||||
.stream()
|
||||
.chain(addl_via_servers)
|
||||
.collect()
|
||||
@@ -188,7 +183,7 @@ async fn knock_room_by_id_helper(
|
||||
.await
|
||||
{
|
||||
debug_warn!("{sender_user} is already knocked in {room_id}");
|
||||
return Ok(knock_room::v3::Response { room_id: room_id.into() });
|
||||
return Ok(knock_room::v3::Response::new(room_id.into()));
|
||||
}
|
||||
|
||||
if let Ok(membership) = services
|
||||
@@ -339,34 +334,27 @@ async fn knock_room_helper_local(
|
||||
) -> Result {
|
||||
debug_info!("We can knock locally");
|
||||
|
||||
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
|
||||
let room_version = services.rooms.state.get_room_version(room_id).await?;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if matches!(
|
||||
room_version_id,
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
) {
|
||||
if !room_version_rules.authorization.knocking {
|
||||
return Err!(Request(Forbidden("This room does not support knocking.")));
|
||||
}
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason: reason.clone(),
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Knock);
|
||||
content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
content.reason = reason.clone();
|
||||
|
||||
// Try normal knock first
|
||||
let Err(error) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &content),
|
||||
PartialPdu::state(sender_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -381,19 +369,18 @@ async fn knock_room_helper_local(
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
|
||||
|
||||
let (make_knock_response, remote_server) =
|
||||
make_knock_request(services, sender_user, room_id, servers).await?;
|
||||
|
||||
info!("make_knock finished");
|
||||
|
||||
let room_version_id = make_knock_response.room_version;
|
||||
let room_version = make_knock_response.room_version;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
));
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
return Err!(BadServerResponse("Remote room version {room_version} is not supported"));
|
||||
}
|
||||
|
||||
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
||||
@@ -424,24 +411,17 @@ async fn knock_room_helper_local(
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
@@ -451,14 +431,14 @@ async fn knock_room_helper_local(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
let send_knock_request = federation::membership::create_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(knock_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
services
|
||||
.sending
|
||||
@@ -520,11 +500,14 @@ async fn knock_room_helper_remote(
|
||||
|
||||
info!("make_knock finished");
|
||||
|
||||
let room_version_id = make_knock_response.room_version;
|
||||
let room_version = make_knock_response.room_version;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
"Remote room version {room_version} is not supported by conduwuit"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -545,26 +528,26 @@ async fn knock_room_helper_remote(
|
||||
.expect("Timestamp is valid js_int value"),
|
||||
),
|
||||
);
|
||||
|
||||
let mut knock_content = RoomMemberEventContent::new(MembershipState::Knock);
|
||||
knock_content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
knock_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
knock_content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
knock_content.reason = reason;
|
||||
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(knock_content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
@@ -574,18 +557,18 @@ async fn knock_room_helper_remote(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
let request = federation::membership::create_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(knock_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
let send_knock_response = services
|
||||
.sending
|
||||
.send_federation_request(&remote_server, send_knock_request)
|
||||
.send_federation_request(&remote_server, request)
|
||||
.await?;
|
||||
|
||||
info!("send_knock finished");
|
||||
@@ -604,7 +587,17 @@ async fn knock_room_helper_remote(
|
||||
let state = send_knock_response
|
||||
.knock_room_state
|
||||
.iter()
|
||||
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
|
||||
.map(|event| {
|
||||
#[allow(deprecated)]
|
||||
let raw_value = match event {
|
||||
| federation::membership::RawStrippedState::Stripped(raw_state) =>
|
||||
&raw_state.clone().into_json(),
|
||||
| federation::membership::RawStrippedState::Pdu(raw_value) => raw_value,
|
||||
| _ => panic!("unknown raw stripped state type"),
|
||||
};
|
||||
|
||||
serde_json::from_str::<CanonicalJsonObject>(raw_value.get())
|
||||
})
|
||||
.filter_map(Result::ok);
|
||||
|
||||
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
||||
@@ -629,7 +622,7 @@ async fn knock_room_helper_remote(
|
||||
continue;
|
||||
};
|
||||
|
||||
let event_id = gen_event_id(&event, &room_version_id)?;
|
||||
let event_id = gen_event_id(&event, &room_version_rules)?;
|
||||
let shortstatekey = services
|
||||
.rooms
|
||||
.short
|
||||
@@ -709,7 +702,7 @@ async fn make_knock_request(
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
|
||||
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
|
||||
let mut make_knock_response_and_server =
|
||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||
|
||||
@@ -722,16 +715,15 @@ async fn make_knock_request(
|
||||
|
||||
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
|
||||
|
||||
let mut request = federation::membership::prepare_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
sender_user.to_owned(),
|
||||
);
|
||||
request.ver = services.server.supported_room_versions().collect();
|
||||
|
||||
let make_knock_response = services
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server,
|
||||
federation::knock::create_knock_event_template::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: sender_user.to_owned(),
|
||||
ver: services.server.supported_room_versions().collect(),
|
||||
},
|
||||
)
|
||||
.send_federation_request(remote_server, request)
|
||||
.await;
|
||||
|
||||
trace!("make_knock response: {make_knock_response:?}");
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Pdu, Result, debug_info, debug_warn, err,
|
||||
matrix::{event::gen_event_id, pdu::PduBuilder},
|
||||
matrix::{event::gen_event_id, pdu::PartialPdu},
|
||||
utils::{self, FutureBoolExt, future::ReadyEqExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, pin_mut};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, UserId,
|
||||
api::{
|
||||
client::membership::leave_room,
|
||||
federation::{self},
|
||||
@@ -42,11 +42,7 @@ pub(crate) async fn leave_room_route(
|
||||
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
|
||||
// and ignores errors
|
||||
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
|
||||
let rooms_joined = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(user_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let rooms_joined = services.rooms.state_cache.rooms_joined(user_id);
|
||||
|
||||
let rooms_invited = services
|
||||
.rooms
|
||||
@@ -142,18 +138,17 @@ pub async fn leave_room(
|
||||
.await;
|
||||
|
||||
match user_member_event_content {
|
||||
| Ok(content) => {
|
||||
| Ok(mut content) => {
|
||||
content.membership = MembershipState::Leave;
|
||||
content.reason = reason;
|
||||
content.join_authorized_via_users_server = None;
|
||||
content.is_direct = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason,
|
||||
join_authorized_via_users_server: None,
|
||||
is_direct: None,
|
||||
..content
|
||||
}),
|
||||
PartialPdu::state(user_id.to_string(), &content),
|
||||
user_id,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -226,7 +221,6 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<HashSet<OwnedServerName>>()
|
||||
.await,
|
||||
);
|
||||
@@ -260,7 +254,7 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.filter_map(|event| event.get_field("sender").ok().flatten())
|
||||
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
||||
.filter_map(|sender| {
|
||||
if !services.globals.user_is_local(sender) {
|
||||
if !services.globals.user_is_local(&sender) {
|
||||
Some(sender.server_name().to_owned())
|
||||
} else {
|
||||
None
|
||||
@@ -289,10 +283,10 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server.as_ref(),
|
||||
federation::membership::prepare_leave_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: user_id.to_owned(),
|
||||
},
|
||||
federation::membership::prepare_leave_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
user_id.to_owned(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -329,6 +323,10 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
)));
|
||||
}
|
||||
|
||||
let room_version_rules = room_version_id
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
||||
make_leave_response.event.get(),
|
||||
)
|
||||
@@ -366,21 +364,16 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
}
|
||||
|
||||
// room v3 and above removed the "event_id" field from remote PDU format
|
||||
match room_version_id {
|
||||
| RoomVersionId::V1 | RoomVersionId::V2 => {},
|
||||
| _ => {
|
||||
leave_event_stub.remove("event_id");
|
||||
},
|
||||
}
|
||||
leave_event_stub.remove("event_id");
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut leave_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&leave_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id back
|
||||
leave_event_stub
|
||||
@@ -393,14 +386,14 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&remote_server,
|
||||
federation::membership::create_leave_event::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
federation::membership::create_leave_event::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(leave_event.clone())
|
||||
.await,
|
||||
},
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use futures::{FutureExt, StreamExt, future::join};
|
||||
use ruma::{
|
||||
api::client::membership::{
|
||||
get_member_events::{self, v3::MembershipEventFilter},
|
||||
get_member_events::{self},
|
||||
joined_members::{self, v3::RoomMember},
|
||||
},
|
||||
events::{
|
||||
@@ -43,20 +43,20 @@ pub(crate) async fn get_member_events_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.")));
|
||||
}
|
||||
|
||||
Ok(get_member_events::v3::Response {
|
||||
chunk: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
|
||||
.map(at!(1))
|
||||
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
|
||||
.map(Event::into_format)
|
||||
.collect()
|
||||
.boxed()
|
||||
.await,
|
||||
})
|
||||
let chunk = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
|
||||
.map(at!(1))
|
||||
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
|
||||
.map(Event::into_format)
|
||||
.collect()
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
Ok(get_member_events::v3::Response::new(chunk))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
|
||||
@@ -78,70 +78,46 @@ pub(crate) async fn joined_members_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.")));
|
||||
}
|
||||
|
||||
Ok(joined_members::v3::Response {
|
||||
joined: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_then(|user_id| async move {
|
||||
let (display_name, avatar_url) = join(
|
||||
services.users.displayname(&user_id).ok(),
|
||||
services.users.avatar_url(&user_id).ok(),
|
||||
)
|
||||
.await;
|
||||
let joined = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.broad_then(|user_id| async move {
|
||||
let mut member = RoomMember::new();
|
||||
let (display_name, avatar_url) = join(
|
||||
services.users.displayname(&user_id).ok(),
|
||||
services.users.avatar_url(&user_id).ok(),
|
||||
)
|
||||
.await;
|
||||
member.display_name = display_name;
|
||||
member.avatar_url = avatar_url;
|
||||
|
||||
(user_id, RoomMember { display_name, avatar_url })
|
||||
})
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
(user_id, member)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(joined_members::v3::Response::new(joined))
|
||||
}
|
||||
|
||||
fn membership_filter<Pdu: Event>(
|
||||
pdu: Pdu,
|
||||
for_membership: Option<&MembershipEventFilter>,
|
||||
not_membership: Option<&MembershipEventFilter>,
|
||||
membership_state_filter: Option<&MembershipState>,
|
||||
not_membership_state_filter: Option<&MembershipState>,
|
||||
) -> Option<impl Event> {
|
||||
let membership_state_filter = match for_membership {
|
||||
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
|
||||
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
|
||||
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
|
||||
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
|
||||
| Some(_) | None => MembershipState::Join,
|
||||
};
|
||||
|
||||
let not_membership_state_filter = match not_membership {
|
||||
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
|
||||
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
|
||||
| Some(MembershipEventFilter::Join) => MembershipState::Join,
|
||||
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
|
||||
| Some(_) | None => MembershipState::Leave,
|
||||
};
|
||||
|
||||
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
|
||||
|
||||
if for_membership.is_some() && not_membership.is_some() {
|
||||
if membership_state_filter != evt_membership
|
||||
|| not_membership_state_filter == evt_membership
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else if for_membership.is_some() && not_membership.is_none() {
|
||||
if membership_state_filter != evt_membership {
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else if not_membership.is_some() && for_membership.is_none() {
|
||||
if not_membership_state_filter == evt_membership {
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else {
|
||||
Some(pdu)
|
||||
if let Some(membership_state_filter) = membership_state_filter
|
||||
&& *membership_state_filter != evt_membership
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(not_membership_state_filter) = not_membership_state_filter
|
||||
&& *not_membership_state_filter == evt_membership
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(pdu);
|
||||
}
|
||||
|
||||
@@ -47,15 +47,14 @@ pub(crate) async fn joined_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<joined_rooms::v3::Request>,
|
||||
) -> Result<joined_rooms::v3::Response> {
|
||||
Ok(joined_rooms::v3::Response {
|
||||
joined_rooms: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(body.sender_user())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
let joined_rooms = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(body.sender_user())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(joined_rooms::v3::Response::new(joined_rooms))
|
||||
}
|
||||
|
||||
/// Checks if the room is banned in any way possible and the sender user is not
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::membership::unban_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -18,9 +18,9 @@ pub(crate) async fn unban_user_route(
|
||||
if services.users.is_suspended(sender_user).await? {
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let current_member_content = services
|
||||
let mut current_member_content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
@@ -34,18 +34,17 @@ pub(crate) async fn unban_user_route(
|
||||
)));
|
||||
}
|
||||
|
||||
current_member_content.membership = MembershipState::Leave;
|
||||
current_member_content.reason = body.reason.clone();
|
||||
current_member_content.join_authorized_via_users_server = None;
|
||||
current_member_content.third_party_invite = None;
|
||||
current_member_content.is_direct = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: body.reason.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
is_direct: None,
|
||||
..current_member_content
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), ¤t_member_content),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
pub(super) use presence::*;
|
||||
pub(super) use profile::*;
|
||||
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
|
||||
pub use push::recreate_push_rules_and_return;
|
||||
pub(super) use push::*;
|
||||
pub(super) use read_marker::*;
|
||||
pub(super) use redact::*;
|
||||
@@ -92,6 +93,3 @@
|
||||
|
||||
/// generated user access token length
|
||||
const TOKEN_LENGTH: usize = 32;
|
||||
|
||||
/// generated user session ID length
|
||||
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
matrix::pdu::PduBuilder,
|
||||
matrix::pdu::PartialPdu,
|
||||
utils::{IterStream, future::TryExtExt, stream::TryIgnore},
|
||||
warn,
|
||||
};
|
||||
@@ -326,7 +326,7 @@ pub async fn update_displayname(
|
||||
.iter()
|
||||
.try_stream()
|
||||
.and_then(|room_id: &OwnedRoomId| async move {
|
||||
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
let pdu = PartialPdu::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
displayname: displayname.clone(),
|
||||
membership: MembershipState::Join,
|
||||
avatar_url: avatar_url.clone(),
|
||||
@@ -376,7 +376,7 @@ pub async fn update_avatar_url(
|
||||
.iter()
|
||||
.try_stream()
|
||||
.and_then(|room_id: &OwnedRoomId| async move {
|
||||
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
let pdu = PartialPdu::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: avatar_url.clone(),
|
||||
blurhash: blurhash.clone(),
|
||||
membership: MembershipState::Join,
|
||||
@@ -399,15 +399,15 @@ pub async fn update_avatar_url(
|
||||
|
||||
pub async fn update_all_rooms(
|
||||
services: &Services,
|
||||
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
|
||||
all_joined_rooms: Vec<(PartialPdu, &OwnedRoomId)>,
|
||||
user_id: &UserId,
|
||||
) {
|
||||
for (pdu_builder, room_id) in all_joined_rooms {
|
||||
for (partial_pdu, room_id) in all_joined_rooms {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
if let Err(e) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, user_id, Some(room_id), &state_lock)
|
||||
.build_and_append_pdu(partial_pdu, user_id, Some(room_id), &state_lock)
|
||||
.await
|
||||
{
|
||||
warn!(%user_id, %room_id, "Failed to update/send new profile join membership update in room: {e}");
|
||||
|
||||
@@ -489,7 +489,7 @@ pub(crate) async fn set_pushers_route(
|
||||
|
||||
/// user somehow has bad push rules, these must always exist per spec.
|
||||
/// so recreate it and return server default silently
|
||||
async fn recreate_push_rules_and_return(
|
||||
pub async fn recreate_push_rules_and_return(
|
||||
services: &Services,
|
||||
sender_user: &ruma::UserId,
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
|
||||
};
|
||||
@@ -34,9 +34,9 @@ pub(crate) async fn redact_event_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
..PartialPdu::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
reason: body.reason.clone(),
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{
|
||||
report_user,
|
||||
reporting::report_user,
|
||||
room::{report_content, report_room},
|
||||
},
|
||||
events::{Mentions, room::message::RoomMessageEventContent},
|
||||
|
||||
@@ -26,13 +26,12 @@ pub(crate) async fn get_room_aliases_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.",)));
|
||||
}
|
||||
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
let aliases = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(aliases::v3::Response::new(aliases))
|
||||
}
|
||||
|
||||
+34
-102
@@ -2,8 +2,8 @@
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, RoomVersion, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{StateKey, pdu::PartialPdu},
|
||||
trace, warn,
|
||||
};
|
||||
use conduwuit_service::{Services, appservice::RegistrationInfo};
|
||||
@@ -13,7 +13,6 @@
|
||||
api::client::room::{self, create_room},
|
||||
events::{
|
||||
TimelineEventType,
|
||||
invite_permission_config::FilterLevel,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
@@ -27,8 +26,10 @@
|
||||
},
|
||||
},
|
||||
int,
|
||||
room_version_rules::RoomIdFormatVersion,
|
||||
serde::{JsonObject, Raw},
|
||||
};
|
||||
use ruminuwuity::invite_permission_config::FilterLevel;
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
|
||||
use crate::{Ruma, client::invite_helper};
|
||||
@@ -81,15 +82,11 @@ pub(crate) async fn create_room_route(
|
||||
},
|
||||
| None => services.server.config.default_room_version.clone(),
|
||||
};
|
||||
let room_features = RoomVersion::new(&room_version)?;
|
||||
let room_version_rules = room_version.rules().unwrap();
|
||||
|
||||
let room_id: Option<OwnedRoomId> = if !room_features.room_ids_as_hashes {
|
||||
match &body.room_id {
|
||||
| Some(custom_room_id) => Some(custom_room_id_check(&services, custom_room_id)?),
|
||||
| None => Some(RoomId::new(services.globals.server_name())),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
let room_id: Option<OwnedRoomId> = match room_version_rules.room_id_format {
|
||||
| RoomIdFormatVersion::V1 => Some(RoomId::new_v1(services.globals.server_name())),
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
@@ -167,7 +164,7 @@ pub(crate) async fn create_room_route(
|
||||
use RoomVersionId::*;
|
||||
|
||||
let mut content = content
|
||||
.deserialize_as::<CanonicalJsonObject>()
|
||||
.deserialize_as_unchecked::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(error!(
|
||||
"Failed to deserialise content as canonical JSON: {e}"
|
||||
@@ -201,8 +198,7 @@ pub(crate) async fn create_room_route(
|
||||
let content = match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||
RoomCreateEventContent::new_v1(sender_user.to_owned()),
|
||||
| V11 => RoomCreateEventContent::new_v11(),
|
||||
| _ => RoomCreateEventContent::new_v12(),
|
||||
| _ => RoomCreateEventContent::new_v11(),
|
||||
};
|
||||
let mut content =
|
||||
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
|
||||
@@ -234,7 +230,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
@@ -257,30 +253,23 @@ pub(crate) async fn create_room_route(
|
||||
},
|
||||
};
|
||||
drop(state_lock);
|
||||
if let Some(expected_room_id) = body.room_id.as_ref() {
|
||||
if expected_room_id.as_str() != room_id.as_str() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID {expected_room_id} does not match the generated room ID \
|
||||
{room_id}.",
|
||||
)));
|
||||
}
|
||||
}
|
||||
debug!("Room created with ID {room_id}");
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
|
||||
|
||||
// 2. Let the room creator join
|
||||
|
||||
let mut join_event = RoomMemberEventContent::new(MembershipState::Join);
|
||||
join_event.displayname = services.users.displayname(sender_user).await.ok();
|
||||
join_event.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
join_event.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
join_event.is_direct = Some(body.is_direct);
|
||||
|
||||
debug_info!("Joining {sender_user} to room {room_id}");
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
is_direct: Some(body.is_direct),
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
}),
|
||||
PartialPdu::state(sender_user.to_string(), &join_event),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -306,7 +295,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
// Do we care about additional_creators?
|
||||
if room_features.explicitly_privilege_room_creators {
|
||||
if room_version_rules.explicitly_privilege_room_creators {
|
||||
// Have they been specified?
|
||||
if let Some(additional_creators) = create_content.get("additional_creators") {
|
||||
// Are they a real array?
|
||||
@@ -341,7 +330,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
@@ -360,7 +349,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
|
||||
PartialPdu::state(String::new(), &RoomCanonicalAliasEventContent {
|
||||
alias: Some(room_alias_id.to_owned()),
|
||||
alt_aliases: vec![],
|
||||
}),
|
||||
@@ -379,7 +368,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomJoinRulesEventContent::new(match preset {
|
||||
| RoomPreset::PublicChat => JoinRule::Public,
|
||||
@@ -399,7 +388,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
|
||||
),
|
||||
@@ -415,7 +404,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomGuestAccessEventContent::new(match preset {
|
||||
| RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
@@ -431,7 +420,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
// 6. Events listed in initial_state
|
||||
for event in &body.initial_state {
|
||||
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
|
||||
let mut partial_pdu = event.deserialize_as::<PartialPdu>().map_err(|e| {
|
||||
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
||||
})?;
|
||||
|
||||
@@ -440,17 +429,17 @@ pub(crate) async fn create_room_route(
|
||||
// client/appservice workaround: if a user sends an initial_state event with a
|
||||
// state event in there with the content of literally `{}` (not null or empty
|
||||
// string), let's just skip it over and warn.
|
||||
if pdu_builder.content.get().eq("{}") {
|
||||
if partial_pdu.content.get().eq("{}") {
|
||||
debug_warn!("skipping empty initial state event with content of `{{}}`: {event:?}");
|
||||
debug_warn!("content: {}", pdu_builder.content.get());
|
||||
debug_warn!("content: {}", partial_pdu.content.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Implicit state key defaults to ""
|
||||
pdu_builder.state_key.get_or_insert_with(StateKey::new);
|
||||
partial_pdu.state_key.get_or_insert_with(StateKey::new);
|
||||
|
||||
// Silently skip encryption events if they are not allowed
|
||||
if pdu_builder.event_type == TimelineEventType::RoomEncryption
|
||||
if partial_pdu.event_type == TimelineEventType::RoomEncryption
|
||||
&& !services.config.allow_encryption
|
||||
{
|
||||
continue;
|
||||
@@ -459,7 +448,7 @@ pub(crate) async fn create_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, Some(&room_id), &state_lock)
|
||||
.build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
@@ -470,7 +459,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
|
||||
PartialPdu::state(String::new(), &RoomNameEventContent::new(name.clone())),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -484,7 +473,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
|
||||
PartialPdu::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -667,60 +656,3 @@ async fn room_alias_check(
|
||||
|
||||
Ok(full_room_alias)
|
||||
}
|
||||
|
||||
/// if a room is being created with a custom room ID, run our checks against it
|
||||
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
|
||||
// apply forbidden room alias checks to custom room IDs too
|
||||
if services
|
||||
.globals
|
||||
.forbidden_alias_names()
|
||||
.is_match(custom_room_id)
|
||||
{
|
||||
return Err!(Request(Unknown("Custom room ID is forbidden.")));
|
||||
}
|
||||
|
||||
if custom_room_id.contains(':') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contained `:` which is not allowed. Please note that this expects a \
|
||||
localpart, not the full room ID.",
|
||||
)));
|
||||
} else if custom_room_id.contains(char::is_whitespace) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contained spaces which is not valid."
|
||||
)));
|
||||
}
|
||||
|
||||
let server_name = services.globals.server_name();
|
||||
let mut room_id = custom_room_id.to_owned();
|
||||
if custom_room_id.contains(':') {
|
||||
if !custom_room_id.starts_with('!') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contains an unexpected `:` which is not allowed.",
|
||||
)));
|
||||
}
|
||||
} else if custom_room_id.starts_with('!') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \
|
||||
this.",
|
||||
)));
|
||||
} else {
|
||||
room_id = format!("!{custom_room_id}:{server_name}");
|
||||
}
|
||||
OwnedRoomId::parse(room_id)
|
||||
.map_err(Into::into)
|
||||
.and_then(|full_room_id| {
|
||||
if full_room_id
|
||||
.server_name()
|
||||
.expect("failed to extract server name from room ID")
|
||||
!= server_name
|
||||
{
|
||||
Err!(Request(InvalidParam("Custom room ID must be on this server.",)))
|
||||
} else {
|
||||
Ok(full_room_id)
|
||||
}
|
||||
})
|
||||
.inspect(|full_room_id| {
|
||||
debug_info!(%full_room_id, "Full custom room ID");
|
||||
})
|
||||
.inspect_err(|e| warn!(?e, %custom_room_id, "Failed to create room with custom room ID",))
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
mod upgrade;
|
||||
|
||||
pub(crate) use self::{
|
||||
aliases::get_room_aliases_route,
|
||||
create::create_room_route,
|
||||
event::get_room_event_route,
|
||||
initial_sync::room_initial_sync_route,
|
||||
summary::{get_room_summary, get_room_summary_legacy},
|
||||
aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route,
|
||||
initial_sync::room_initial_sync_route, summary::get_room_summary,
|
||||
upgrade::upgrade_room_route,
|
||||
};
|
||||
|
||||
+15
-318
@@ -1,48 +1,11 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_warn, info, trace,
|
||||
utils::{IterStream, future::TryExtExt},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryFutureExt,
|
||||
future::{OptionFuture, join3},
|
||||
stream::FuturesUnordered,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedServerName, RoomId, UserId,
|
||||
api::{
|
||||
client::room::get_summary,
|
||||
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
|
||||
},
|
||||
events::room::member::MembershipState,
|
||||
space::SpaceRoomJoinRule::{self, *},
|
||||
};
|
||||
use service::Services;
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::api::client::room::get_summary;
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::{Ruma, RumaResponse};
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary`
|
||||
///
|
||||
/// Returns a short description of the state of a room.
|
||||
///
|
||||
/// This is the "wrong" endpoint that some implementations/clients may use
|
||||
/// according to the MSC. Request and response bodies are the same as
|
||||
/// `get_room_summary`.
|
||||
///
|
||||
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
|
||||
pub(crate) async fn get_room_summary_legacy(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
|
||||
get_room_summary(State(services), InsecureClientIp(client), body)
|
||||
.boxed()
|
||||
.await
|
||||
.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`
|
||||
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
|
||||
///
|
||||
/// Returns a short description of the state of a room.
|
||||
@@ -50,8 +13,8 @@ pub(crate) async fn get_room_summary_legacy(
|
||||
pub(crate) async fn get_room_summary(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
body: Ruma<get_summary::v1::Request>,
|
||||
) -> Result<get_summary::v1::Response> {
|
||||
let (room_id, servers) = services
|
||||
.rooms
|
||||
.alias
|
||||
@@ -62,285 +25,19 @@ pub(crate) async fn get_room_summary(
|
||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||
}
|
||||
|
||||
room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref())
|
||||
.boxed()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn room_summary_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
if services
|
||||
let summary = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
match local_room_summary_response(services, room_id, sender_user)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Ok(response) => return Ok(response),
|
||||
| Err(e) => {
|
||||
debug_warn!("Failed to get local room summary: {e:?}, falling back to remote");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let room =
|
||||
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias: room.canonical_alias,
|
||||
avatar_url: room.avatar_url,
|
||||
guest_can_join: room.guest_can_join,
|
||||
name: room.name,
|
||||
num_joined_members: room.num_joined_members,
|
||||
topic: room.topic,
|
||||
world_readable: room.world_readable,
|
||||
join_rule: room.join_rule,
|
||||
room_type: room.room_type,
|
||||
room_version: room.room_version,
|
||||
encryption: room.encryption,
|
||||
allowed_room_ids: room.allowed_room_ids,
|
||||
membership: sender_user.is_some().then_some(MembershipState::Leave),
|
||||
})
|
||||
}
|
||||
|
||||
async fn local_room_summary_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
trace!(
|
||||
sender_user = sender_user.map(tracing::field::display),
|
||||
"Sending local room summary response for {room_id:?}"
|
||||
);
|
||||
let (join_rule, world_readable, guest_can_join) = join3(
|
||||
services.rooms.state_accessor.get_join_rules(room_id),
|
||||
services.rooms.state_accessor.is_world_readable(room_id),
|
||||
services.rooms.state_accessor.guest_can_join(room_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Synapse allows server admins to bypass visibility checks.
|
||||
// That seems neat so we'll copy that behaviour.
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&join_rule.clone().into(),
|
||||
guest_can_join,
|
||||
world_readable,
|
||||
join_rule.allowed_rooms(),
|
||||
sender_user,
|
||||
)
|
||||
.summary
|
||||
.get_room_summary_for_user(body.sender_user.as_deref(), &room_id, &servers)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let canonical_alias = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_canonical_alias(room_id)
|
||||
.ok();
|
||||
|
||||
let name = services.rooms.state_accessor.get_name(room_id).ok();
|
||||
|
||||
let topic = services.rooms.state_accessor.get_room_topic(room_id).ok();
|
||||
|
||||
let room_type = services.rooms.state_accessor.get_room_type(room_id).ok();
|
||||
|
||||
let avatar_url = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_avatar(room_id)
|
||||
.map(|res| res.into_option().unwrap_or_default().url);
|
||||
|
||||
let room_version = services.rooms.state.get_room_version(room_id).ok();
|
||||
|
||||
let encryption = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_encryption(room_id)
|
||||
.ok();
|
||||
|
||||
let num_joined_members = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(room_id)
|
||||
.unwrap_or(0);
|
||||
|
||||
let membership: OptionFuture<_> = sender_user
|
||||
.map(|sender_user| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(room_id, sender_user)
|
||||
.map_ok_or(MembershipState::Leave, |content| content.membership)
|
||||
})
|
||||
.into();
|
||||
|
||||
let (
|
||||
canonical_alias,
|
||||
name,
|
||||
num_joined_members,
|
||||
topic,
|
||||
avatar_url,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
) = futures::join!(
|
||||
canonical_alias,
|
||||
name,
|
||||
num_joined_members,
|
||||
topic,
|
||||
avatar_url,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
);
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias,
|
||||
avatar_url,
|
||||
guest_can_join,
|
||||
name,
|
||||
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
||||
topic,
|
||||
world_readable,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
allowed_room_ids: join_rule.allowed_rooms().map(Into::into).collect(),
|
||||
join_rule: join_rule.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// used by MSC3266 to fetch a room's info if we do not know about it
|
||||
async fn remote_room_summary_hierarchy_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<SpaceHierarchyParentSummary> {
|
||||
trace!(sender_user = ?sender_user.map(tracing::field::display), ?servers, "Sending remote room summary response for {room_id:?}");
|
||||
if !services.config.allow_federation {
|
||||
return Err!(Request(Forbidden("Federation is disabled.")));
|
||||
}
|
||||
|
||||
if services.rooms.metadata.is_disabled(room_id).await {
|
||||
return Err!(Request(Forbidden(
|
||||
"Federaton of room {room_id} is currently disabled on this server."
|
||||
)));
|
||||
}
|
||||
if servers.is_empty() {
|
||||
return Err!(Request(MissingParam(
|
||||
"No servers were provided to fetch the room over federation"
|
||||
)));
|
||||
}
|
||||
|
||||
let request = get_hierarchy::v1::Request::new(room_id.to_owned());
|
||||
|
||||
let mut requests: FuturesUnordered<_> = servers
|
||||
.iter()
|
||||
.map(|server| {
|
||||
info!("Fetching room summary for {room_id} from server {server}");
|
||||
services
|
||||
.sending
|
||||
.send_federation_request(server, request.clone())
|
||||
.inspect_ok(move |v| {
|
||||
debug!("Fetched room summary for {room_id} from server {server}: {v:?}");
|
||||
})
|
||||
.inspect_err(move |e| {
|
||||
info!("Failed to fetch room summary for {room_id} from server {server}: {e}");
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some(Ok(response)) = requests.next().await {
|
||||
trace!("{response:?}");
|
||||
let room = response.room.clone();
|
||||
if room.room_id != room_id {
|
||||
debug_warn!(
|
||||
"Room ID {} returned does not belong to the requested room ID {}",
|
||||
room.room_id,
|
||||
room_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
return user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&room.join_rule,
|
||||
room.guest_can_join,
|
||||
room.world_readable,
|
||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
||||
sender_user,
|
||||
)
|
||||
.await
|
||||
.map(|()| room);
|
||||
}
|
||||
return Ok(room);
|
||||
}
|
||||
|
||||
Err!(Request(NotFound("Room not found or is not accessible")))
|
||||
}
|
||||
|
||||
async fn user_can_see_summary<'a, I>(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
join_rule: &SpaceRoomJoinRule,
|
||||
guest_can_join: bool,
|
||||
world_readable: bool,
|
||||
allowed_room_ids: I,
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result
|
||||
where
|
||||
I: Iterator<Item = &'a RoomId> + Send,
|
||||
{
|
||||
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
|
||||
match sender_user {
|
||||
| Some(sender_user) => {
|
||||
let user_can_see_state_events = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id);
|
||||
let is_guest = services.users.is_deactivated(sender_user).unwrap_or(false);
|
||||
let user_in_allowed_restricted_room = allowed_room_ids
|
||||
.stream()
|
||||
.any(|room| services.rooms.state_cache.is_joined(sender_user, room));
|
||||
|
||||
let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
|
||||
join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
if user_can_see_state_events
|
||||
|| (is_guest && guest_can_join)
|
||||
|| is_public_room
|
||||
|| user_in_allowed_restricted_room
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
match summary {
|
||||
| Accessibility::Accessible(summary) => Ok(get_summary::v1::Response::new(summary)),
|
||||
| Accessibility::Inaccessible => {
|
||||
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
|
||||
},
|
||||
| None => {
|
||||
if is_public_room || world_readable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
| Accessibility::NotFound => {
|
||||
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, RoomVersion, debug, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
matrix::{StateKey, pdu::PartialPdu},
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
@@ -85,7 +85,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
PartialPdu::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: RoomId::new(services.globals.server_name()),
|
||||
}),
|
||||
@@ -128,7 +128,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
@@ -209,7 +209,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
let create_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content)
|
||||
@@ -237,7 +237,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
@@ -304,7 +304,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
@@ -364,7 +364,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
|
||||
events_default: new_level,
|
||||
invite: new_level,
|
||||
@@ -387,7 +387,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
@@ -434,7 +434,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: StateEventType::SpaceChild.into(),
|
||||
content: to_raw_value(&RedactedSpaceChildEventContent {})
|
||||
@@ -457,7 +457,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: StateEventType::SpaceChild.into(),
|
||||
content: to_raw_value(&SpaceChildEventContent {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result, err, matrix::pdu::PduBuilder, utils};
|
||||
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu, utils};
|
||||
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
|
||||
use serde_json::from_str;
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) async fn send_message_event_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: body.event_type.clone().into(),
|
||||
content,
|
||||
unsigned: Some(unsigned),
|
||||
|
||||
+69
-62
@@ -4,12 +4,13 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, err, info,
|
||||
utils::{self, ReadyExt, hash},
|
||||
utils::{self, ReadyExt, hash, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_core::{debug_error, debug_warn};
|
||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
@@ -26,9 +27,10 @@
|
||||
},
|
||||
logout, logout_all,
|
||||
},
|
||||
uiaa,
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::Ruma;
|
||||
@@ -80,7 +82,7 @@ pub(crate) async fn password_login(
|
||||
.password_hash(lowercased_user_id)
|
||||
.await
|
||||
.map(|hash| (hash, lowercased_user_id))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?,
|
||||
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?,
|
||||
};
|
||||
|
||||
if hash.is_empty() {
|
||||
@@ -89,7 +91,7 @@ pub(crate) async fn password_login(
|
||||
|
||||
hash::verify_password(password, &hash)
|
||||
.inspect_err(|e| debug_error!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
|
||||
|
||||
Ok(user_id.to_owned())
|
||||
}
|
||||
@@ -161,28 +163,38 @@ pub(super) async fn ldap_login(
|
||||
|
||||
pub(crate) async fn handle_login(
|
||||
services: &Services,
|
||||
body: &Ruma<login::v3::Request>,
|
||||
identifier: Option<&uiaa::UserIdentifier>,
|
||||
identifier: Option<&UserIdentifier>,
|
||||
password: &str,
|
||||
user: Option<&String>,
|
||||
) -> Result<OwnedUserId> {
|
||||
debug!("Got password login type");
|
||||
let user_id_or_localpart = match (identifier, user) {
|
||||
| (Some(UserIdentifier::UserIdOrLocalpart(localpart)), _) => localpart,
|
||||
| (Some(UserIdentifier::Email { address }), _) => {
|
||||
let email = Address::try_from(address.to_owned())
|
||||
.map_err(|_| err!(Request(InvalidParam("Email is malformed"))))?;
|
||||
|
||||
&services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.ok_or_else(|| err!(Request(Forbidden("Invalid identifier or password"))))?
|
||||
},
|
||||
| (None, Some(user)) => user,
|
||||
| _ => {
|
||||
return Err!(Request(InvalidParam("Identifier type not recognized")));
|
||||
},
|
||||
};
|
||||
|
||||
let user_id =
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
} else {
|
||||
return Err!(Request(Unknown(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
|
||||
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||
|
||||
let lowercased_user_id = UserId::parse_with_server_name(
|
||||
user_id.localpart().to_lowercase(),
|
||||
&services.config.server_name,
|
||||
)?;
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !services.globals.user_is_local(&user_id)
|
||||
|| !services.globals.user_is_local(&lowercased_user_id)
|
||||
@@ -244,7 +256,7 @@ pub(crate) async fn login_route(
|
||||
password,
|
||||
user,
|
||||
..
|
||||
}) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||
debug!("Got token login type");
|
||||
if !services.server.config.login_via_existing_session {
|
||||
@@ -264,7 +276,7 @@ pub(crate) async fn login_route(
|
||||
};
|
||||
|
||||
let user_id =
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
@@ -273,7 +285,7 @@ pub(crate) async fn login_route(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
.map_err(|_| err!(Request(InvalidUsername(warn!("User ID is malformed")))))?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||
@@ -370,45 +382,13 @@ pub(crate) async fn login_token_route(
|
||||
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
||||
}
|
||||
|
||||
// This route SHOULD have UIA
|
||||
// TODO: How do we make only UIA sessions that have not been used before valid?
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let mut uiaainfo = uiaa::UiaaInfo {
|
||||
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body.as_ref() {
|
||||
| Some(json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("No JSON body was sent when required.")));
|
||||
},
|
||||
},
|
||||
}
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
||||
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
||||
@@ -434,9 +414,28 @@ pub(crate) async fn logout_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<logout::v3::Request>,
|
||||
) -> Result<logout::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
services
|
||||
.users
|
||||
.remove_device(body.sender_user(), body.sender_device())
|
||||
.remove_device(sender_user, sender_device)
|
||||
.await;
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.as_ref()
|
||||
.is_some_and(|pusher_device| pusher_device == sender_device)
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(logout::v3::Response::new())
|
||||
@@ -461,10 +460,18 @@ pub(crate) async fn logout_all_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<logout_all::v3::Request>,
|
||||
) -> Result<logout_all::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
services
|
||||
.users
|
||||
.all_device_ids(body.sender_user())
|
||||
.for_each(|device_id| services.users.remove_device(body.sender_user(), device_id))
|
||||
.all_device_ids(sender_user)
|
||||
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
|
||||
.await;
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, pushkey).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(logout_all::v3::Response::new())
|
||||
|
||||
+28
-176
@@ -1,26 +1,12 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, VecDeque},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
utils::{future::TryExtExt, stream::IterStream},
|
||||
};
|
||||
use conduwuit_service::{
|
||||
Services,
|
||||
rooms::spaces::{
|
||||
PaginationToken, SummaryAccessibility, get_parent_children_via, summary_to_chunk,
|
||||
},
|
||||
};
|
||||
use futures::{StreamExt, TryFutureExt, future::OptionFuture};
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedServerName, RoomId, UInt, UserId, api::client::space::get_hierarchy,
|
||||
};
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::{UInt, api::client::space::get_hierarchy, assign};
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const MAX_MAX_DEPTH: u32 = 10;
|
||||
|
||||
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`
|
||||
///
|
||||
/// Paginates over the space tree in a depth-first manner to locate child rooms
|
||||
@@ -29,167 +15,33 @@ pub(crate) async fn get_hierarchy_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
let limit = body
|
||||
.limit
|
||||
.unwrap_or_else(|| UInt::from(10_u32))
|
||||
.min(UInt::from(100_u32));
|
||||
// We don't do pagination for this route (and therefore ignore `limit`), since
|
||||
// there's no reasonable way to handle a space hierarchy changing during
|
||||
// pagination.
|
||||
|
||||
let max_depth = body
|
||||
.max_depth
|
||||
.unwrap_or_else(|| UInt::from(3_u32))
|
||||
.min(UInt::from(10_u32));
|
||||
.map(|max_depth| max_depth.min(UInt::from(MAX_MAX_DEPTH)));
|
||||
|
||||
let key = body
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|s| PaginationToken::from_str(s).ok());
|
||||
let hierarchy = services
|
||||
.rooms
|
||||
.summary
|
||||
.get_room_hierarchy_for_user(
|
||||
body.sender_user(),
|
||||
body.room_id.clone(),
|
||||
max_depth,
|
||||
body.suggested_only,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Should prevent unexpected behaviour in (bad) clients
|
||||
if let Some(ref token) = key {
|
||||
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
|
||||
return Err!(Request(InvalidParam(
|
||||
"suggested_only and max_depth cannot change on paginated requests"
|
||||
)));
|
||||
}
|
||||
match hierarchy {
|
||||
| Accessibility::Accessible(rooms) =>
|
||||
Ok(assign!(get_hierarchy::v1::Response::new(), { rooms: rooms })),
|
||||
| Accessibility::Inaccessible => {
|
||||
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
|
||||
},
|
||||
| Accessibility::NotFound => {
|
||||
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
|
||||
},
|
||||
}
|
||||
|
||||
get_client_hierarchy(
|
||||
&services,
|
||||
body.sender_user(),
|
||||
&body.room_id,
|
||||
limit.try_into().unwrap_or(10),
|
||||
max_depth.try_into().unwrap_or(usize::MAX),
|
||||
body.suggested_only,
|
||||
key.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|t| t.short_room_ids.iter()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_client_hierarchy<'a, ShortRoomIds>(
|
||||
services: &Services,
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
limit: usize,
|
||||
max_depth: usize,
|
||||
suggested_only: bool,
|
||||
short_room_ids: ShortRoomIds,
|
||||
) -> Result<get_hierarchy::v1::Response>
|
||||
where
|
||||
ShortRoomIds: Iterator<Item = &'a u64> + Clone + Send + Sync + 'a,
|
||||
{
|
||||
type Via = Vec<OwnedServerName>;
|
||||
type Entry = (OwnedRoomId, Via);
|
||||
type Rooms = VecDeque<Entry>;
|
||||
|
||||
let mut queue: Rooms = [(
|
||||
room_id.to_owned(),
|
||||
room_id
|
||||
.server_name()
|
||||
.map(ToOwned::to_owned)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)]
|
||||
.into();
|
||||
|
||||
let mut rooms = Vec::with_capacity(limit);
|
||||
let mut parents = BTreeSet::new();
|
||||
while let Some((current_room, via)) = queue.pop_front() {
|
||||
let summary = services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_client(¤t_room, suggested_only, sender_user, &via)
|
||||
.await?;
|
||||
|
||||
match (summary, current_room == room_id) {
|
||||
| (None | Some(SummaryAccessibility::Inaccessible), false) => {
|
||||
// Just ignore other unavailable rooms
|
||||
},
|
||||
| (None, true) => {
|
||||
return Err!(Request(Forbidden("The requested room was not found")));
|
||||
},
|
||||
| (Some(SummaryAccessibility::Inaccessible), true) => {
|
||||
return Err!(Request(Forbidden("The requested room is inaccessible")));
|
||||
},
|
||||
| (Some(SummaryAccessibility::Accessible(summary)), _) => {
|
||||
let populate = parents.len() >= short_room_ids.clone().count();
|
||||
|
||||
let mut children: Vec<Entry> = get_parent_children_via(&summary, suggested_only)
|
||||
.filter(|(room, _)| !parents.contains(room))
|
||||
.rev()
|
||||
.map(|(key, val)| (key, val.collect()))
|
||||
.collect();
|
||||
|
||||
if populate {
|
||||
rooms.push(summary_to_chunk(summary.clone()));
|
||||
} else {
|
||||
children = children
|
||||
.iter()
|
||||
.rev()
|
||||
.stream()
|
||||
.skip_while(|(room, _)| {
|
||||
services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortroomid(room)
|
||||
.map_ok(|short| {
|
||||
Some(&short) != short_room_ids.clone().nth(parents.len())
|
||||
})
|
||||
.unwrap_or_else(|_| false)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect::<Vec<Entry>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !populate && queue.is_empty() && children.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
parents.insert(current_room.clone());
|
||||
if rooms.len() >= limit {
|
||||
break;
|
||||
}
|
||||
|
||||
if parents.len() > max_depth {
|
||||
continue;
|
||||
}
|
||||
|
||||
queue.extend(children);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let next_batch: OptionFuture<_> = queue
|
||||
.pop_front()
|
||||
.map(|(room, _)| async move {
|
||||
parents.insert(room);
|
||||
|
||||
let next_short_room_ids: Vec<_> = parents
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|room_id| services.rooms.short.get_shortroomid(room_id).ok())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
(next_short_room_ids.iter().ne(short_room_ids) && !next_short_room_ids.is_empty())
|
||||
.then_some(PaginationToken {
|
||||
short_room_ids: next_short_room_ids,
|
||||
limit: limit.try_into().ok()?,
|
||||
max_depth: max_depth.try_into().ok()?,
|
||||
suggested_only,
|
||||
})
|
||||
.as_ref()
|
||||
.map(PaginationToken::to_string)
|
||||
})
|
||||
.into();
|
||||
|
||||
Ok(get_hierarchy::v1::Response {
|
||||
next_batch: next_batch.await.flatten(),
|
||||
rooms,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, err,
|
||||
matrix::{Event, pdu::PduBuilder},
|
||||
matrix::{Event, pdu::PartialPdu},
|
||||
utils::BoolExt,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
@@ -203,7 +203,7 @@ async fn send_state_event_for_key_helper(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get())?,
|
||||
state_key: Some(state_key.into()),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use conduwuit::{Error, Result};
|
||||
use ruma::api::client::{
|
||||
discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||
discover_homeserver::{self, HomeserverInfo},
|
||||
discover_support::{self, Contact},
|
||||
},
|
||||
error::ErrorKind,
|
||||
@@ -23,9 +23,9 @@ pub(crate) async fn well_known_client(
|
||||
};
|
||||
|
||||
Ok(discover_homeserver::Response {
|
||||
homeserver: HomeserverInfo { base_url: client_url.clone() },
|
||||
homeserver: HomeserverInfo { base_url: client_url },
|
||||
identity_server: None,
|
||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
||||
sliding_sync_proxy: None,
|
||||
tile_server: None,
|
||||
rtc_foci: services
|
||||
.config
|
||||
@@ -71,6 +71,7 @@ pub(crate) async fn well_known_support(
|
||||
|
||||
let email_address = services.config.well_known.support_email.clone();
|
||||
let matrix_id = services.config.well_known.support_mxid.clone();
|
||||
let pgp_key = services.config.well_known.support_pgp_key.clone();
|
||||
|
||||
// TODO: support defining multiple contacts in the config
|
||||
let mut contacts: Vec<Contact> = vec![];
|
||||
@@ -88,6 +89,7 @@ pub(crate) async fn well_known_support(
|
||||
role: role_value.clone(),
|
||||
email_address: email_address.clone(),
|
||||
matrix_id: matrix_id.clone(),
|
||||
pgp_key: pgp_key.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,6 +106,7 @@ pub(crate) async fn well_known_support(
|
||||
role: role_value.clone(),
|
||||
email_address: None,
|
||||
matrix_id: Some(user_id.to_owned()),
|
||||
pgp_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -28,7 +28,8 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::appservice_ping)
|
||||
.ruma_route(&client::get_supported_versions_route)
|
||||
.ruma_route(&client::get_register_available_route)
|
||||
.ruma_route(&client::register_route)
|
||||
.ruma_route(&client::register::register_route)
|
||||
.ruma_route(&client::register::request_registration_token_via_email_route)
|
||||
.ruma_route(&client::get_login_types_route)
|
||||
.ruma_route(&client::login_route)
|
||||
.ruma_route(&client::login_token_route)
|
||||
@@ -36,10 +37,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::logout_route)
|
||||
.ruma_route(&client::logout_all_route)
|
||||
.ruma_route(&client::change_password_route)
|
||||
.ruma_route(&client::request_password_change_token_via_email_route)
|
||||
.ruma_route(&client::deactivate_route)
|
||||
.ruma_route(&client::third_party_route)
|
||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
||||
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
|
||||
.ruma_route(&client::threepid::third_party_route)
|
||||
.ruma_route(&client::threepid::request_3pid_management_token_via_email_route)
|
||||
.ruma_route(&client::threepid::request_3pid_management_token_via_msisdn_route)
|
||||
.ruma_route(&client::threepid::add_3pid_route)
|
||||
.ruma_route(&client::threepid::delete_3pid_route)
|
||||
.ruma_route(&client::check_registration_token_validity)
|
||||
.ruma_route(&client::get_capabilities_route)
|
||||
.ruma_route(&client::get_pushrules_all_route)
|
||||
@@ -180,18 +184,14 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::get_hierarchy_route)
|
||||
.ruma_route(&client::get_mutual_rooms_route)
|
||||
.ruma_route(&client::get_room_summary)
|
||||
.route(
|
||||
"/_matrix/client/unstable/im.nheko.summary/rooms/{room_id_or_alias}/summary",
|
||||
get(client::get_room_summary_legacy)
|
||||
)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
.ruma_route(&client::put_suspended_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.ruma_route(&client::get_rtc_transports)
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.route("/client/server.json", get(client::syncv3_client_server_json))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
|
||||
+6
-39
@@ -2,14 +2,13 @@
|
||||
|
||||
use axum::{body::Body, extract::FromRequest};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
|
||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
||||
};
|
||||
use service::Services;
|
||||
|
||||
use super::{auth, auth::Auth, request, request::Request};
|
||||
use super::{auth, request, request::Request};
|
||||
use crate::{State, service::appservice::RegistrationInfo};
|
||||
|
||||
/// Extractor for Ruma request structs
|
||||
@@ -108,7 +107,7 @@ async fn from_request(
|
||||
}
|
||||
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
|
||||
Ok(Self {
|
||||
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
|
||||
body: make_body::<T>(&mut request, json_body.as_mut())?,
|
||||
origin: auth.origin,
|
||||
sender_user: auth.sender_user,
|
||||
sender_device: auth.sender_device,
|
||||
@@ -118,16 +117,11 @@ async fn from_request(
|
||||
}
|
||||
}
|
||||
|
||||
fn make_body<T>(
|
||||
services: &Services,
|
||||
request: &mut Request,
|
||||
json_body: Option<&mut CanonicalJsonValue>,
|
||||
auth: &Auth,
|
||||
) -> Result<T>
|
||||
fn make_body<T>(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Result<T>
|
||||
where
|
||||
T: IncomingRequest,
|
||||
{
|
||||
let body = take_body(services, request, json_body, auth);
|
||||
let body = take_body(request, json_body);
|
||||
let http_request = into_http_request(request, body);
|
||||
T::try_from_http_request(http_request, &request.path)
|
||||
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))
|
||||
@@ -151,38 +145,11 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn take_body(
|
||||
services: &Services,
|
||||
request: &mut Request,
|
||||
json_body: Option<&mut CanonicalJsonValue>,
|
||||
auth: &Auth,
|
||||
) -> Bytes {
|
||||
fn take_body(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Bytes {
|
||||
let Some(CanonicalJsonValue::Object(json_body)) = json_body else {
|
||||
return mem::take(&mut request.body);
|
||||
};
|
||||
|
||||
let user_id = auth.sender_user.clone().unwrap_or_else(|| {
|
||||
let server_name = services.globals.server_name();
|
||||
UserId::parse_with_server_name(EMPTY, server_name).expect("valid user_id")
|
||||
});
|
||||
|
||||
let uiaa_request = json_body
|
||||
.get("auth")
|
||||
.and_then(CanonicalJsonValue::as_object)
|
||||
.and_then(|auth| auth.get("session"))
|
||||
.and_then(CanonicalJsonValue::as_str)
|
||||
.and_then(|session| {
|
||||
services
|
||||
.uiaa
|
||||
.get_uiaa_request(&user_id, auth.sender_device.as_deref(), session)
|
||||
});
|
||||
|
||||
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
|
||||
for (key, value) in initial_request {
|
||||
json_body.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::new().writer();
|
||||
serde_json::to_writer(&mut buf, &json_body).expect("value serialization can't fail");
|
||||
buf.into_inner().freeze()
|
||||
|
||||
@@ -44,15 +44,16 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
|
||||
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
|
||||
{
|
||||
fn add_routes(&'static self, router: Router<State>) -> Router<State> {
|
||||
Req::METADATA
|
||||
.history
|
||||
use ruma::api::path_builder::PathBuilder;
|
||||
|
||||
Req::PATH_BUILDER
|
||||
.all_paths()
|
||||
.fold(router, |router, path| self.add_route(router, path))
|
||||
}
|
||||
|
||||
fn add_route(&'static self, router: Router<State>, path: &str) -> Router<State> {
|
||||
let action = |$($tx,)* req| self($($tx,)* req).map_ok(RumaResponse);
|
||||
let method = method_to_filter(&Req::METADATA.method);
|
||||
let method = method_to_filter(&Req::METHOD);
|
||||
router.route(path, on(method, action))
|
||||
}
|
||||
}
|
||||
|
||||
+10
-57
@@ -1,13 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, info,
|
||||
utils::stream::{BroadbandExt, IterStream},
|
||||
};
|
||||
use conduwuit_service::rooms::spaces::{
|
||||
Identifier, SummaryAccessibility, get_parent_children_via,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use conduwuit::{Err, Result, info};
|
||||
use ruma::api::federation::space::get_hierarchy;
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -19,10 +13,6 @@ pub(crate) async fn get_hierarchy_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
return Err!(Request(NotFound("Room does not exist.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -36,52 +26,15 @@ pub(crate) async fn get_hierarchy_route(
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let room_id = &body.room_id;
|
||||
let suggested_only = body.suggested_only;
|
||||
let ref identifier = Identifier::ServerName(body.origin());
|
||||
match services
|
||||
let response = services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_local(room_id, identifier)
|
||||
.await?
|
||||
{
|
||||
| None => Err!(Request(NotFound("The requested room was not found"))),
|
||||
.summary
|
||||
.get_local_room_summary_for_server(body.origin(), &body.room_id, body.suggested_only)
|
||||
.await;
|
||||
|
||||
| Some(SummaryAccessibility::Inaccessible) => {
|
||||
Err!(Request(NotFound("The requested room is inaccessible")))
|
||||
},
|
||||
|
||||
| Some(SummaryAccessibility::Accessible(room)) => {
|
||||
let (children, inaccessible_children) =
|
||||
get_parent_children_via(&room, suggested_only)
|
||||
.stream()
|
||||
.broad_filter_map(|(child, _via)| async move {
|
||||
match services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_local(&child, identifier)
|
||||
.await
|
||||
.ok()?
|
||||
{
|
||||
| None => None,
|
||||
|
||||
| Some(SummaryAccessibility::Inaccessible) =>
|
||||
Some((None, Some(child))),
|
||||
|
||||
| Some(SummaryAccessibility::Accessible(summary)) =>
|
||||
Some((Some(summary), None)),
|
||||
}
|
||||
})
|
||||
.unzip()
|
||||
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
|
||||
(
|
||||
children.into_iter().flatten().map(Into::into).collect(),
|
||||
inaccessible_children.into_iter().flatten().collect(),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(get_hierarchy::v1::Response { room, children, inaccessible_children })
|
||||
},
|
||||
if let Accessibility::Accessible(response) = response {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err!(Request(NotFound("This room is not accessible.")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
|
||||
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PartialPdu, warn};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
@@ -137,7 +137,7 @@ pub(crate) async fn create_join_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
PartialPdu::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
join_authorized_via_users_server,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
}),
|
||||
@@ -172,7 +172,7 @@ pub(crate) async fn select_authorising_user(
|
||||
.state_accessor
|
||||
.user_can_invite(room_id, user, user_id, state_lock)
|
||||
})
|
||||
.boxed()
|
||||
.boPartialPdu
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use RoomVersionId::*;
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn};
|
||||
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PartialPdu, warn};
|
||||
use ruma::{
|
||||
RoomVersionId,
|
||||
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
|
||||
@@ -102,7 +102,7 @@ pub(crate) async fn create_knock_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
body.user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Knock),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, info, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::federation::membership::prepare_leave_event,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -53,7 +53,7 @@ pub(crate) async fn create_leave_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
body.user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Leave),
|
||||
),
|
||||
|
||||
@@ -84,12 +84,14 @@ libc.workspace = true
|
||||
libloading.workspace = true
|
||||
libloading.optional = true
|
||||
log.workspace = true
|
||||
lettre.workspace = true
|
||||
num-traits.workspace = true
|
||||
rand.workspace = true
|
||||
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
ring.workspace = true
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
sanitize-filename.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
};
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
pub use figment::{Figment, value::Value as FigmentValue};
|
||||
use lettre::message::Mailbox;
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
@@ -760,6 +761,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub well_known: WellKnownConfig,
|
||||
|
||||
/// display: nested
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
|
||||
/// Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
|
||||
/// Jaeger exporter. Traces will be sent via OTLP to a collector (such as
|
||||
/// Jaeger) that supports the OpenTelemetry Protocol.
|
||||
@@ -2187,6 +2191,10 @@ pub struct WellKnownConfig {
|
||||
/// listed.
|
||||
pub support_mxid: Option<OwnedUserId>,
|
||||
|
||||
/// PGP key URI for server support contacts, to be served as part of the
|
||||
/// MSC1929 server support endpoint.
|
||||
pub support_pgp_key: Option<String>,
|
||||
|
||||
/// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
|
||||
///
|
||||
/// A list of MatrixRTC foci URLs which will be served as part of the
|
||||
@@ -2444,6 +2452,52 @@ pub struct DraupnirConfig {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.smtp",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct SmtpConfig {
|
||||
/// A `smtp://`` URI which will be used to connect to a mail server.
|
||||
/// Uncommenting the [global.smtp] group and setting this option enables
|
||||
/// features which depend on the ability to send email,
|
||||
/// such as self-service password resets.
|
||||
///
|
||||
/// For most modern mail servers, format the URI like this:
|
||||
/// `smtps://username:password@hostname:port`
|
||||
/// Note that you will need to URL-encode the username and password. If your
|
||||
/// username _is_ your email address, you will need to replace the `@` with
|
||||
/// `%40`.
|
||||
///
|
||||
/// For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||
/// https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||
pub connection_uri: String,
|
||||
|
||||
/// The outgoing address which will be used for sending emails.
|
||||
///
|
||||
/// For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
///
|
||||
/// ...or if you don't want to read the RFC, for some reason:
|
||||
/// - `Name <address@domain.org>` to specify a sender name
|
||||
/// - `address@domain.org` to not use a name
|
||||
pub sender: Mailbox,
|
||||
|
||||
/// Whether to require that users provide an email address when they
|
||||
/// register.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub require_email_for_registration: bool,
|
||||
|
||||
/// Whether to require that users who register with a registration token
|
||||
/// provide an email address.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub require_email_for_token_registration: bool,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
|
||||
+12
-4
@@ -46,7 +46,7 @@ macro_rules! err {
|
||||
(Request(Forbidden($level:ident!($($args:tt)+)))) => {{
|
||||
let mut buf = String::new();
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::forbidden(),
|
||||
$crate::ruma::api::error::ErrorKind::Forbidden,
|
||||
$crate::err_log!(buf, $level, $($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -54,7 +54,7 @@ macro_rules! err {
|
||||
|
||||
(Request(Forbidden($($args:tt)+))) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::forbidden(),
|
||||
$crate::ruma::api::error::ErrorKind::Forbidden,
|
||||
$crate::format_maybe!($($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -63,7 +63,7 @@ macro_rules! err {
|
||||
(Request($variant:ident($level:ident!($($args:tt)+)))) => {{
|
||||
let mut buf = String::new();
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::$variant,
|
||||
$crate::ruma::api::error::ErrorKind::$variant,
|
||||
$crate::err_log!(buf, $level, $($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -71,12 +71,20 @@ macro_rules! err {
|
||||
|
||||
(Request($variant:ident($($args:tt)+))) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::$variant,
|
||||
$crate::ruma::api::error::ErrorKind::$variant,
|
||||
$crate::format_maybe!($($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
};
|
||||
|
||||
(Request($variant:ident($($args:tt)+), $status_code:ident)) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::error::ErrorKind::$variant,
|
||||
$crate::format_maybe!($($args)+),
|
||||
$crate::http::StatusCode::$status_code,
|
||||
)
|
||||
};
|
||||
|
||||
(Config($item:literal, $($args:tt)+)) => {{
|
||||
let mut buf = String::new();
|
||||
$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))
|
||||
|
||||
+12
-14
@@ -57,10 +57,6 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
|
||||
#[error(transparent)]
|
||||
JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
|
||||
#[error(transparent)]
|
||||
Path(#[from] axum::extract::rejection::PathRejection),
|
||||
#[error("Mutex poisoned: {0}")]
|
||||
Poison(Cow<'static, str>),
|
||||
@@ -90,8 +86,8 @@ pub enum Error {
|
||||
// ruma/conduwuit
|
||||
#[error("Arithmetic operation failed: {0}")]
|
||||
Arithmetic(Cow<'static, str>),
|
||||
#[error("{0}: {1}")]
|
||||
BadRequest(ruma::api::client::error::ErrorKind, &'static str), //TODO: remove
|
||||
#[error("{0:?}: {1}")]
|
||||
BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
|
||||
#[error("{0}")]
|
||||
BadServerResponse(Cow<'static, str>),
|
||||
#[error(transparent)]
|
||||
@@ -107,7 +103,7 @@ pub enum Error {
|
||||
#[error("Feature '{0}' is not available on this server.")]
|
||||
FeatureDisabled(Cow<'static, str>),
|
||||
#[error("Remote server {0} responded with: {1}")]
|
||||
Federation(ruma::OwnedServerName, ruma::api::client::error::Error),
|
||||
Federation(ruma::OwnedServerName, ruma::api::error::Error),
|
||||
#[error("{0} in {1}")]
|
||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||
#[error(transparent)]
|
||||
@@ -120,12 +116,14 @@ pub enum Error {
|
||||
Mxid(#[from] ruma::IdParseError),
|
||||
#[error("from {0}: {1}")]
|
||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||
#[error("{0}: {1}")]
|
||||
Request(ruma::api::client::error::ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error("{0:?}: {1}")]
|
||||
Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error(transparent)]
|
||||
Ruma(#[from] ruma::api::client::error::Error),
|
||||
Ruma(#[from] ruma::api::error::Error),
|
||||
#[error(transparent)]
|
||||
Signatures(#[from] ruma::signatures::Error),
|
||||
SignatureJson(#[from] ruma::signatures::JsonError),
|
||||
#[error(transparent)]
|
||||
SignatureVerification(#[from] ruma::signatures::VerificationError),
|
||||
#[error(transparent)]
|
||||
StateRes(#[from] crate::state_res::Error),
|
||||
#[error("uiaa")]
|
||||
@@ -166,14 +164,14 @@ pub fn message(&self) -> String {
|
||||
|
||||
/// Returns the Matrix error code / error kind
|
||||
#[inline]
|
||||
pub fn kind(&self) -> ruma::api::client::error::ErrorKind {
|
||||
use ruma::api::client::error::ErrorKind::{FeatureDisabled, Unknown};
|
||||
pub fn kind(&self) -> ruma::api::error::ErrorKind {
|
||||
use ruma::api::error::ErrorKind::{Unknown, Unrecognized};
|
||||
|
||||
match self {
|
||||
| Self::Federation(_, error) | Self::Ruma(error) =>
|
||||
response::ruma_error_kind(error).clone(),
|
||||
| Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
|
||||
| Self::FeatureDisabled(..) => FeatureDisabled,
|
||||
| Self::FeatureDisabled(..) => Unrecognized,
|
||||
| _ => Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
+12
-23
@@ -3,10 +3,8 @@
|
||||
use http_body_util::Full;
|
||||
use ruma::api::{
|
||||
OutgoingResponse,
|
||||
client::{
|
||||
error::{ErrorBody, ErrorKind},
|
||||
uiaa::UiaaResponse,
|
||||
},
|
||||
client::uiaa::UiaaResponse,
|
||||
error::{ErrorBody, ErrorKind, StandardErrorBody},
|
||||
};
|
||||
|
||||
use super::Error;
|
||||
@@ -51,15 +49,9 @@ fn from(error: Error) -> Self {
|
||||
return Self::AuthResponse(uiaainfo);
|
||||
}
|
||||
|
||||
let body = ErrorBody::Standard {
|
||||
kind: error.kind(),
|
||||
message: error.message(),
|
||||
};
|
||||
let body = ErrorBody::Standard(StandardErrorBody::new(error.kind(), error.message()));
|
||||
|
||||
Self::MatrixError(ruma::api::client::error::Error {
|
||||
status_code: error.status_code(),
|
||||
body,
|
||||
})
|
||||
Self::MatrixError(ruma::api::error::Error::new(error.status_code(), body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +68,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
|
||||
match kind {
|
||||
// 429
|
||||
| LimitExceeded { .. } => StatusCode::TOO_MANY_REQUESTS,
|
||||
| LimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
|
||||
|
||||
// 413
|
||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||
@@ -85,37 +77,34 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
// 404
|
||||
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } =>
|
||||
StatusCode::NOT_FOUND,
|
||||
| NotFound => StatusCode::NOT_FOUND,
|
||||
|
||||
// 403
|
||||
| GuestAccessForbidden
|
||||
| ThreepidAuthFailed
|
||||
| UserDeactivated
|
||||
| ThreepidDenied
|
||||
| InviteBlocked
|
||||
| WrongRoomKeysVersion { .. }
|
||||
| WrongRoomKeysVersion(_)
|
||||
| UserSuspended
|
||||
| Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||
| Forbidden => StatusCode::FORBIDDEN,
|
||||
|
||||
// 401
|
||||
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
|
||||
StatusCode::UNAUTHORIZED,
|
||||
| UnknownToken(_) | MissingToken | Unauthorized | UserLocked => StatusCode::UNAUTHORIZED,
|
||||
|
||||
// 400
|
||||
| _ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ruma_error_message(error: &ruma::api::client::error::Error) -> String {
|
||||
if let ErrorBody::Standard { message, .. } = &error.body {
|
||||
pub(super) fn ruma_error_message(error: &ruma::api::error::Error) -> String {
|
||||
if let ErrorBody::Standard(StandardErrorBody { message, .. }) = &error.body {
|
||||
return message.clone();
|
||||
}
|
||||
|
||||
format!("{error}")
|
||||
}
|
||||
|
||||
pub(super) fn ruma_error_kind(e: &ruma::api::client::error::Error) -> &ErrorKind {
|
||||
pub(super) fn ruma_error_kind(e: &ruma::api::error::Error) -> &ErrorKind {
|
||||
e.error_kind().unwrap_or(&ErrorKind::Unknown)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::iter::once;
|
||||
|
||||
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::RoomVersionStability};
|
||||
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::v3::RoomVersionStability};
|
||||
|
||||
use crate::{at, is_equal_to};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
use ruma::{
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId,
|
||||
RoomVersionId, UserId, events::TimelineEventType,
|
||||
RoomVersionId, UserId, events::TimelineEventType, room_version_rules::RoomVersionRules,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
|
||||
@@ -95,11 +95,11 @@ fn get_content<T>(&self) -> Result<T>
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId>
|
||||
fn redacts_id(&self, room_version_rules: &RoomVersionRules) -> Option<OwnedEventId>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
redact::redacts_id(self, room_version)
|
||||
redact::redacts_id(self, room_version_rules)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -67,7 +67,7 @@ fn matches_sender<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
|
||||
}
|
||||
|
||||
fn matches_type<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
|
||||
let kind = event.kind().to_cow_str();
|
||||
let kind = event.kind().to_string();
|
||||
|
||||
if filter.not_types.iter().any(is_equal_to!(&kind)) {
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruma::{CanonicalJsonObject, OwnedEventId, RoomVersionId};
|
||||
use ruma::{CanonicalJsonObject, OwnedEventId, room_version_rules::RoomVersionRules};
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use crate::{Result, err};
|
||||
use crate::{Err, Result, err};
|
||||
|
||||
/// Generates a correct eventId for the incoming pdu.
|
||||
///
|
||||
@@ -9,12 +9,12 @@
|
||||
/// CanonicalJsonValue>`.
|
||||
pub fn gen_event_id_canonical_json(
|
||||
pdu: &RawJsonValue,
|
||||
room_version_id: &RoomVersionId,
|
||||
room_version_rules: &RoomVersionRules,
|
||||
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
|
||||
let value: CanonicalJsonObject = serde_json::from_str(pdu.get())
|
||||
.map_err(|e| err!(BadServerResponse(warn!("Error parsing incoming event: {e:?}"))))?;
|
||||
|
||||
let event_id = gen_event_id(&value, room_version_id)?;
|
||||
let event_id = gen_event_id(&value, room_version_rules)?;
|
||||
|
||||
Ok((event_id, value))
|
||||
}
|
||||
@@ -22,9 +22,9 @@ pub fn gen_event_id_canonical_json(
|
||||
/// Generates a correct eventId for the incoming pdu.
|
||||
pub fn gen_event_id(
|
||||
value: &CanonicalJsonObject,
|
||||
room_version_id: &RoomVersionId,
|
||||
room_version_rules: &RoomVersionRules,
|
||||
) -> Result<OwnedEventId> {
|
||||
let reference_hash = ruma::signatures::reference_hash(value, room_version_id)?;
|
||||
let reference_hash = ruma::signatures::reference_hash(value, room_version_rules)?;
|
||||
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
|
||||
|
||||
Ok(event_id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruma::{
|
||||
OwnedEventId, RoomVersionId,
|
||||
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
|
||||
room_version_rules::RoomVersionRules,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
||||
@@ -61,7 +62,7 @@ pub(super) fn is_redacted<E: Event>(event: &E) -> bool {
|
||||
#[must_use]
|
||||
pub(super) fn redacts_id<E: Event>(
|
||||
event: &E,
|
||||
room_version: &RoomVersionId,
|
||||
room_version_rules: &RoomVersionRules,
|
||||
) -> Option<OwnedEventId> {
|
||||
use RoomVersionId::*;
|
||||
|
||||
@@ -69,14 +70,13 @@ pub(super) fn redacts_id<E: Event>(
|
||||
return None;
|
||||
}
|
||||
|
||||
match *room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||
event.redacts().map(ToOwned::to_owned),
|
||||
| _ =>
|
||||
event
|
||||
.get_content::<RoomRedactionEventContent>()
|
||||
.ok()?
|
||||
.redacts,
|
||||
if room_version_rules.redaction.content_field_redacts {
|
||||
event.redacts().map(ToOwned::to_owned)
|
||||
} else {
|
||||
event
|
||||
.get_content::<RoomRedactionEventContent>()
|
||||
.ok()?
|
||||
.redacts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, Stat
|
||||
|
||||
impl TypeExt for TimelineEventType {
|
||||
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
|
||||
(self.into(), state_key.into())
|
||||
(self.to_string().into(), state_key.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeExt for &TimelineEventType {
|
||||
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
|
||||
(self.clone().into(), state_key.into())
|
||||
(self.clone().to_string().into(), state_key.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
pub mod pdu;
|
||||
pub mod state_key;
|
||||
pub mod state_res;
|
||||
pub mod versions;
|
||||
|
||||
pub use event::{Event, TypeExt as EventTypeExt};
|
||||
pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
|
||||
pub use pdu::{PartialPdu, Pdu, PduCount, PduEvent, PduId, RawPduId, ShortId};
|
||||
pub use state_key::StateKey;
|
||||
pub use state_res::{RoomVersion, StateMap, TypeStateKey};
|
||||
pub use state_res::{StateMap, TypeStateKey};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod builder;
|
||||
mod count;
|
||||
mod id;
|
||||
mod partial;
|
||||
mod raw_id;
|
||||
mod redact;
|
||||
#[cfg(test)]
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
pub use self::{
|
||||
Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId,
|
||||
builder::{Builder, Builder as PduBuilder},
|
||||
count::Count,
|
||||
id::{ShortId, *},
|
||||
partial::PartialPdu,
|
||||
raw_id::*,
|
||||
};
|
||||
use super::{Event, StateKey};
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
events::{EventContent, MessageLikeEventType, StateEventType, TimelineEventType},
|
||||
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
||||
|
||||
use super::StateKey;
|
||||
|
||||
/// Build the start of a PDU in order to add it to the Database.
|
||||
/// An event and its associated metadata, without an ID, signatures, or hashes.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Builder {
|
||||
pub struct PartialPdu {
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: TimelineEventType,
|
||||
|
||||
@@ -30,10 +30,10 @@ pub struct Builder {
|
||||
|
||||
type Unsigned = BTreeMap<String, serde_json::Value>;
|
||||
|
||||
impl Builder {
|
||||
impl PartialPdu {
|
||||
pub fn state<S, T>(state_key: S, content: &T) -> Self
|
||||
where
|
||||
T: EventContent<EventType = StateEventType>,
|
||||
T: StateEventContent,
|
||||
S: Into<StateKey>,
|
||||
{
|
||||
Self {
|
||||
@@ -47,7 +47,7 @@ pub fn state<S, T>(state_key: S, content: &T) -> Self
|
||||
|
||||
pub fn timeline<T>(content: &T) -> Self
|
||||
where
|
||||
T: EventContent<EventType = MessageLikeEventType>,
|
||||
T: MessageLikeEventContent,
|
||||
{
|
||||
Self {
|
||||
event_type: content.event_type().into(),
|
||||
@@ -58,7 +58,7 @@ pub fn timeline<T>(content: &T) -> Self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
impl Default for PartialPdu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
event_type: "m.room.message".into(),
|
||||
@@ -1,17 +1,20 @@
|
||||
use ruma::{RoomVersionId, canonical_json::redact_content_in_place};
|
||||
use serde_json::{Value as JsonValue, json, value::to_raw_value};
|
||||
|
||||
use crate::{Error, Result, err, implement};
|
||||
use crate::{Err, Error, Result, err, implement};
|
||||
|
||||
#[implement(super::Pdu)]
|
||||
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result {
|
||||
let Some(rules) = room_version_id.rules() else {
|
||||
return Err!("Cannot redact event for unknown room version {room_version_id}");
|
||||
};
|
||||
|
||||
self.unsigned = None;
|
||||
|
||||
let mut content = serde_json::from_str(self.content.get())
|
||||
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
|
||||
|
||||
redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
|
||||
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
|
||||
redact_content_in_place(&mut content, &rules.redaction, self.kind.to_string());
|
||||
|
||||
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ fn set_up(
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
@@ -328,7 +328,7 @@ fn set_up(
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
@@ -338,7 +338,7 @@ fn set_up(
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
(ev.event_type().to_string().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, ThirdPartyInvite},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
third_party_invite::RoomThirdPartyInviteEventContent,
|
||||
},
|
||||
int,
|
||||
serde::{Base64, Raw},
|
||||
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
@@ -28,7 +28,6 @@
|
||||
deserialize_power_levels, deserialize_power_levels_content_fields,
|
||||
deserialize_power_levels_content_invite, deserialize_power_levels_content_redact,
|
||||
},
|
||||
room_version::RoomVersion,
|
||||
};
|
||||
use crate::{debug, error, trace, warn};
|
||||
|
||||
@@ -65,13 +64,13 @@ pub fn auth_types_for_event(
|
||||
sender: &UserId,
|
||||
state_key: Option<&str>,
|
||||
content: &RawJsonValue,
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
) -> serde_json::Result<Vec<(StateEventType, StateKey)>> {
|
||||
if kind == &TimelineEventType::RoomCreate {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut auth_types = if room_version.room_ids_as_hashes {
|
||||
let mut auth_types = if room_version.room_id_format == RoomIdFormatVersion::V2 {
|
||||
vec![
|
||||
(StateEventType::RoomPowerLevels, StateKey::new()),
|
||||
(StateEventType::RoomMember, sender.as_str().into()),
|
||||
@@ -120,15 +119,16 @@ struct RoomMemberContentFields {
|
||||
auth_types.push(key);
|
||||
}
|
||||
|
||||
if membership == MembershipState::Invite {
|
||||
if let Some(Ok(t_id)) = content.third_party_invite.map(|t| t.deserialize()) {
|
||||
let key =
|
||||
(StateEventType::RoomThirdPartyInvite, t_id.signed.token.into());
|
||||
if !auth_types.contains(&key) {
|
||||
auth_types.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: restore this once 3pid support isn't broken
|
||||
// if membership == MembershipState::Invite {
|
||||
// if let Some(Ok(t_id)) = content.third_party_invite.map(|t|
|
||||
// t.deserialize()) { let key =
|
||||
// (StateEventType::RoomThirdPartyInvite,
|
||||
// t_id.signed.token.into()); if !auth_types.contains(&
|
||||
// key) { auth_types.push(key);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ struct RoomMemberContentFields {
|
||||
)]
|
||||
#[allow(clippy::suspicious_operation_groupings)]
|
||||
pub async fn auth_check<E, F, Fut>(
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
incoming_event: &E,
|
||||
current_third_party_invite: Option<&E>,
|
||||
fetch_state: F,
|
||||
@@ -218,13 +218,17 @@ pub async fn auth_check<E, F, Fut>(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if room_version.room_ids_as_hashes && incoming_event.room_id().is_some() {
|
||||
if room_version.room_id_format == RoomIdFormatVersion::V2
|
||||
&& incoming_event.room_id().is_some()
|
||||
{
|
||||
warn!("room create event incorrectly claims to have a room ID when it should not");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !room_version.use_room_create_sender
|
||||
&& !room_version.explicitly_privilege_room_creators
|
||||
if !room_version.authorization.use_room_create_sender
|
||||
&& !room_version
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
// If content has no creator field, reject
|
||||
if content.creator.is_none() {
|
||||
@@ -305,10 +309,10 @@ pub async fn auth_check<E, F, Fut>(
|
||||
let claims_create_event = incoming_event
|
||||
.auth_events()
|
||||
.any(|id| id == room_create_event.event_id());
|
||||
if room_version.room_ids_as_hashes && claims_create_event {
|
||||
if room_version.room_id_format == RoomIdFormatVersion::V2 && claims_create_event {
|
||||
warn!("event incorrectly references m.room.create event in auth events");
|
||||
return Ok(false);
|
||||
} else if !room_version.room_ids_as_hashes && !claims_create_event {
|
||||
} else if !(room_version.room_id_format == RoomIdFormatVersion::V2) && !claims_create_event {
|
||||
// If the create event is not referenced in the event's auth events, and this is
|
||||
// a v11 room, reject
|
||||
warn!(
|
||||
@@ -332,7 +336,7 @@ pub async fn auth_check<E, F, Fut>(
|
||||
// If the create event content has the field m.federate set to false and the
|
||||
// sender domain of the event does not match the sender domain of the create
|
||||
// event, reject.
|
||||
if !room_version.room_ids_as_hashes
|
||||
if !(room_version.room_id_format == RoomIdFormatVersion::V2)
|
||||
&& !room_create_content.federate
|
||||
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
|
||||
{
|
||||
@@ -345,9 +349,9 @@ pub async fn auth_check<E, F, Fut>(
|
||||
}
|
||||
|
||||
// Only in some room versions 6 and below
|
||||
if room_version.special_case_aliases_auth {
|
||||
if room_version.authorization.special_case_room_aliases {
|
||||
// 4. If type is m.room.aliases
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomAliases {
|
||||
if *incoming_event.event_type() == TimelineEventType::from("m.room.aliases") {
|
||||
debug!("starting m.room.aliases check");
|
||||
|
||||
// If sender's domain doesn't matches state_key, reject
|
||||
@@ -486,7 +490,7 @@ pub async fn auth_check<E, F, Fut>(
|
||||
},
|
||||
| _ => {
|
||||
// If no power level event found the creator gets 100 everyone else gets 0
|
||||
let is_creator = if room_version.use_room_create_sender {
|
||||
let is_creator = if room_version.authorization.use_room_create_sender {
|
||||
room_create_event.sender() == sender
|
||||
} else {
|
||||
#[allow(deprecated)]
|
||||
@@ -497,7 +501,10 @@ pub async fn auth_check<E, F, Fut>(
|
||||
if is_creator { int!(100) } else { int!(0) }
|
||||
},
|
||||
};
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
if room_version
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
// If the user sent the create event, or is listed in additional_creators, just
|
||||
// give them Int::MAX
|
||||
if sender == room_create_event.sender()
|
||||
@@ -559,7 +566,10 @@ pub async fn auth_check<E, F, Fut>(
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
|
||||
debug!("starting m.room.power_levels check");
|
||||
let mut creators = BTreeSet::new();
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
if room_version
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
creators.insert(create_event.sender().to_owned());
|
||||
for creator in room_create_content.additional_creators.iter().flatten() {
|
||||
creators.insert(creator.deserialize()?);
|
||||
@@ -593,7 +603,7 @@ pub async fn auth_check<E, F, Fut>(
|
||||
// the sender of the redaction has the appropriate permissions per the
|
||||
// power levels.
|
||||
|
||||
if room_version.extra_redaction_checks
|
||||
if room_version.authorization.special_case_room_redaction
|
||||
&& *incoming_event.event_type() == TimelineEventType::RoomRedaction
|
||||
{
|
||||
let redact_level = match power_levels_event {
|
||||
@@ -618,7 +628,7 @@ pub async fn auth_check<E, F, Fut>(
|
||||
}
|
||||
|
||||
fn is_creator<EV>(
|
||||
v: &RoomVersion,
|
||||
v: &RoomVersionRules,
|
||||
c: &BTreeSet<OwnedUserId>,
|
||||
ce: &EV,
|
||||
user_id: &UserId,
|
||||
@@ -627,9 +637,9 @@ fn is_creator<EV>(
|
||||
where
|
||||
EV: Event + Send + Sync,
|
||||
{
|
||||
if v.explicitly_privilege_room_creators {
|
||||
if v.authorization.explicitly_privilege_room_creators {
|
||||
c.contains(user_id)
|
||||
} else if v.use_room_create_sender && !have_pls {
|
||||
} else if v.authorization.use_room_create_sender && !have_pls {
|
||||
ce.sender() == user_id
|
||||
} else if !have_pls {
|
||||
#[allow(deprecated)]
|
||||
@@ -659,7 +669,7 @@ fn is_creator<EV>(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn valid_membership_change<E>(
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
target_user: &UserId,
|
||||
target_user_membership_event: Option<&E>,
|
||||
sender: &UserId,
|
||||
@@ -700,7 +710,7 @@ struct GetThirdPartyInvite {
|
||||
|
||||
let power_levels: RoomPowerLevelsEventContent = match &power_levels_event {
|
||||
| Some(ev) => from_json_str(ev.content().get())?,
|
||||
| None => RoomPowerLevelsEventContent::default(),
|
||||
| None => RoomPowerLevelsEventContent::new(&room_version.authorization),
|
||||
};
|
||||
|
||||
let mut sender_power = power_levels
|
||||
@@ -714,7 +724,10 @@ struct GetThirdPartyInvite {
|
||||
|
||||
let mut creators = BTreeSet::new();
|
||||
creators.insert(create_room.sender().to_owned());
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
if room_version
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
// Explicitly privilege room creators
|
||||
// If the sender sent the create event, or in additional_creators, give them
|
||||
// Int::MAX. Same case for target.
|
||||
@@ -865,7 +878,7 @@ struct GetThirdPartyInvite {
|
||||
trace!(sender=%sender, "sender is invited to room, allowing join");
|
||||
true
|
||||
},
|
||||
| JoinRule::Knock if !room_version.allow_knocking => {
|
||||
| JoinRule::Knock if !room_version.authorization.knocking => {
|
||||
warn!("Join rule is knock but room version does not allow knocking");
|
||||
false
|
||||
},
|
||||
@@ -882,7 +895,8 @@ struct GetThirdPartyInvite {
|
||||
trace!(sender=%sender, "sender is invited or already joined to room, allowing join");
|
||||
true
|
||||
},
|
||||
| JoinRule::KnockRestricted(_) if !room_version.knock_restricted_join_rule =>
|
||||
| JoinRule::KnockRestricted(_)
|
||||
if !room_version.authorization.knock_restricted_join_rule =>
|
||||
{
|
||||
warn!(
|
||||
"Join rule is knock_restricted but room version does not support it"
|
||||
@@ -1127,7 +1141,7 @@ struct GetThirdPartyInvite {
|
||||
}
|
||||
allow
|
||||
},
|
||||
| MembershipState::Knock if room_version.allow_knocking => {
|
||||
| MembershipState::Knock if room_version.authorization.knocking => {
|
||||
// 1. If the `join_rule` is anything other than `knock` or `knock_restricted`,
|
||||
// reject.
|
||||
if !matches!(join_rules, JoinRule::KnockRestricted(_) | JoinRule::Knock) {
|
||||
@@ -1136,7 +1150,7 @@ struct GetThirdPartyInvite {
|
||||
);
|
||||
false
|
||||
} else if matches!(join_rules, JoinRule::KnockRestricted(_))
|
||||
&& !room_version.knock_restricted_join_rule
|
||||
&& !room_version.authorization.knock_restricted_join_rule
|
||||
{
|
||||
// 2. If the `join_rule` is `knock_restricted`, but the room does not support
|
||||
// `knock_restricted`, reject.
|
||||
@@ -1226,7 +1240,7 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
|
||||
/// Confirm that the event sender has the required power levels.
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn check_power_levels(
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
power_event: &impl Event,
|
||||
previous_power_event: Option<&impl Event>,
|
||||
user_level: Int,
|
||||
@@ -1382,7 +1396,7 @@ fn check_power_levels(
|
||||
}
|
||||
|
||||
// Notifications, currently there is only @room
|
||||
if room_version.limit_notifications_power_levels {
|
||||
if room_version.authorization.limit_notifications_power_levels {
|
||||
let old_level = old_state.notifications.room;
|
||||
let new_level = new_state.notifications.room;
|
||||
if old_level != new_level {
|
||||
@@ -1451,7 +1465,7 @@ fn get_deserialize_levels(
|
||||
/// Does the event redacting come from a user with enough power to redact the
|
||||
/// given event.
|
||||
fn check_redaction(
|
||||
_room_version: &RoomVersion,
|
||||
_room_version: &RoomVersionRules,
|
||||
redaction_event: &impl Event,
|
||||
user_level: Int,
|
||||
redact_level: Int,
|
||||
@@ -1501,79 +1515,34 @@ fn get_send_level(
|
||||
}
|
||||
|
||||
fn verify_third_party_invite(
|
||||
target_user: Option<&UserId>,
|
||||
sender: &UserId,
|
||||
tp_id: &ThirdPartyInvite,
|
||||
current_third_party_invite: Option<&impl Event>,
|
||||
_target_user: Option<&UserId>,
|
||||
_sender: &UserId,
|
||||
_tp_id: &ThirdPartyInvite,
|
||||
_current_third_party_invite: Option<&impl Event>,
|
||||
) -> bool {
|
||||
// 1. Check for user being banned happens before this is called
|
||||
// checking for mxid and token keys is done by ruma when deserializing
|
||||
|
||||
// The state key must match the invitee
|
||||
if target_user != Some(&tp_id.signed.mxid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no m.room.third_party_invite event in the current room state with
|
||||
// state_key matching token, reject
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let current_tpid = match current_third_party_invite {
|
||||
| Some(id) => id,
|
||||
| None => return false,
|
||||
};
|
||||
|
||||
if current_tpid.state_key() != Some(&tp_id.signed.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if sender != current_tpid.sender() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If any signature in signed matches any public key in the
|
||||
// m.room.third_party_invite event, allow
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let tpid_ev =
|
||||
match from_json_str::<RoomThirdPartyInviteEventContent>(current_tpid.content().get()) {
|
||||
| Ok(ev) => ev,
|
||||
| Err(_) => return false,
|
||||
};
|
||||
|
||||
#[allow(clippy::manual_let_else)]
|
||||
let decoded_invite_token = match Base64::parse(&tp_id.signed.token) {
|
||||
| Ok(tok) => tok,
|
||||
// FIXME: Log a warning?
|
||||
| Err(_) => return false,
|
||||
};
|
||||
|
||||
// A list of public keys in the public_keys field
|
||||
for key in tpid_ev.public_keys.unwrap_or_default() {
|
||||
if key.public_key == decoded_invite_token {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// A single public key in the public_key field
|
||||
tpid_ev.public_key == decoded_invite_token
|
||||
// TODO: implement proper verification here
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership,
|
||||
use ruma::{
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
join_rules::{AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
room::RoomMembership,
|
||||
room_version_rules::RoomVersionRules,
|
||||
};
|
||||
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||
|
||||
use crate::{
|
||||
matrix::{Event, EventTypeExt, Pdu as PduEvent},
|
||||
state_res::{
|
||||
RoomVersion, StateMap,
|
||||
StateMap,
|
||||
event_auth::valid_membership_change,
|
||||
test_utils::{
|
||||
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
|
||||
@@ -1610,7 +1579,7 @@ fn test_ban_pass() {
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1655,7 +1624,7 @@ fn test_join_non_creator() {
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1700,7 +1669,7 @@ fn test_join_creator() {
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1745,7 +1714,7 @@ fn test_ban_fail() {
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1807,7 +1776,7 @@ fn test_restricted_join_rule() {
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
&RoomVersionRules::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1825,7 +1794,7 @@ fn test_restricted_join_rule() {
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
&RoomVersionRules::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
@@ -1879,7 +1848,7 @@ fn test_knock() {
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V7,
|
||||
&RoomVersionRules::V7,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
pub(crate) mod error;
|
||||
pub mod event_auth;
|
||||
mod power_levels;
|
||||
mod room_version;
|
||||
mod serde_backports;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
@@ -20,25 +20,22 @@
|
||||
|
||||
use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future};
|
||||
use ruma::{
|
||||
EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
|
||||
EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
int,
|
||||
room_version_rules::{RoomIdFormatVersion, RoomVersionRules, StateResolutionVersion},
|
||||
};
|
||||
use serde_json::from_str as from_json_str;
|
||||
|
||||
pub(crate) use self::error::Error;
|
||||
pub use self::event_auth::{auth_check, auth_types_for_event};
|
||||
use self::power_levels::PowerLevelsContentFields;
|
||||
pub use self::{
|
||||
event_auth::{auth_check, auth_types_for_event},
|
||||
room_version::RoomVersion,
|
||||
};
|
||||
use crate::{
|
||||
debug, debug_error, err,
|
||||
matrix::{Event, StateKey},
|
||||
state_res::room_version::StateResolutionVersion,
|
||||
trace,
|
||||
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
|
||||
warn,
|
||||
@@ -77,7 +74,7 @@
|
||||
//#[tracing::instrument(level event_fetch))]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
|
||||
room_version: &RoomVersionId,
|
||||
room_version: &RoomVersionRules,
|
||||
state_sets: Sets,
|
||||
auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>],
|
||||
event_fetch: &Fetch,
|
||||
@@ -94,11 +91,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
|
||||
Pdu: Event + Clone + Send + Sync,
|
||||
for<'b> &'b Pdu: Event + Send,
|
||||
{
|
||||
use RoomVersionId::*;
|
||||
let stateres_version = match room_version {
|
||||
| V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 => StateResolutionVersion::V2,
|
||||
| _ => StateResolutionVersion::V2_1,
|
||||
};
|
||||
let stateres_version = room_version.state_res;
|
||||
debug!(version = ?stateres_version, "State resolution starting");
|
||||
|
||||
// Split non-conflicting and conflicting state
|
||||
@@ -114,19 +107,21 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
|
||||
|
||||
debug!(count = conflicting.len(), "conflicting events");
|
||||
trace!(map = ?conflicting, "conflicting events");
|
||||
let (conflicted_state_subgraph, initial_state) =
|
||||
if stateres_version == StateResolutionVersion::V2_1 {
|
||||
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
|
||||
})?;
|
||||
debug!(count = csg.len(), "conflicted subgraph");
|
||||
trace!(set = ?csg, "conflicted subgraph");
|
||||
(csg, HashMap::new())
|
||||
} else {
|
||||
(HashSet::new(), unconflicted.clone())
|
||||
};
|
||||
let (conflicted_state_subgraph, initial_state) = if let StateResolutionVersion::V2(v2_rules) =
|
||||
stateres_version
|
||||
&& v2_rules.consider_conflicted_state_subgraph
|
||||
{
|
||||
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
|
||||
})?;
|
||||
debug!(count = csg.len(), "conflicted subgraph");
|
||||
trace!(set = ?csg, "conflicted subgraph");
|
||||
(csg, HashMap::new())
|
||||
} else {
|
||||
(HashSet::new(), unconflicted.clone())
|
||||
};
|
||||
|
||||
// `all_conflicted` contains unique items
|
||||
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in
|
||||
@@ -166,7 +161,6 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
|
||||
debug!(count = sorted_control_levels.len(), "power events");
|
||||
trace!(list = ?sorted_control_levels, "sorted power events");
|
||||
|
||||
let room_version = RoomVersion::new(room_version)?;
|
||||
// Sequentially auth check each control event.
|
||||
let resolved_control = iterative_auth_check(
|
||||
&room_version,
|
||||
@@ -596,7 +590,7 @@ async fn get_power_level_for_sender<E, F, Fut>(
|
||||
/// `event_auth::auth_check` function.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
events_to_check: S,
|
||||
unconflicted_state: StateMap<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
@@ -666,7 +660,7 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
trace!(list = ?auth_types, event_id = event.event_id().as_str(), "auth types for event");
|
||||
|
||||
let mut auth_state = StateMap::new();
|
||||
if room_version.room_ids_as_hashes {
|
||||
if room_version.room_id_format == RoomIdFormatVersion::V2 {
|
||||
trace!("room version uses hashed IDs, manually fetching create event");
|
||||
let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$");
|
||||
let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| {
|
||||
@@ -963,7 +957,7 @@ fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, Stat
|
||||
|
||||
impl EventTypeExt for TimelineEventType {
|
||||
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
|
||||
(self.into(), state_key.into())
|
||||
(self.to_string().into(), state_key.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,13 +982,14 @@ mod tests {
|
||||
StateEventType, TimelineEventType,
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
},
|
||||
int, uint,
|
||||
int,
|
||||
room_version_rules::RoomVersionRules,
|
||||
uint,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value as to_raw_json_value};
|
||||
|
||||
use super::{
|
||||
StateMap, is_power_event,
|
||||
room_version::RoomVersion,
|
||||
test_utils::{
|
||||
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
|
||||
member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event,
|
||||
@@ -1004,7 +999,6 @@ mod tests {
|
||||
use crate::{
|
||||
debug,
|
||||
matrix::{Event, EventTypeExt, Pdu as PduEvent},
|
||||
state_res::room_version::StateResolutionVersion,
|
||||
utils::stream::IterStream,
|
||||
};
|
||||
|
||||
@@ -1036,7 +1030,7 @@ async fn test_event_sort() {
|
||||
.unwrap();
|
||||
|
||||
let resolved_power = super::iterative_auth_check(
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
sorted_power_events.iter().map(AsRef::as_ref).stream(),
|
||||
HashMap::new(), // unconflicted events
|
||||
&fetcher,
|
||||
@@ -1436,13 +1430,18 @@ async fn test_event_map_none() {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let resolved =
|
||||
match super::resolve(&RoomVersionId::V2, &state_sets, &auth_chain, &fetcher, &exists)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(e) => panic!("{e}"),
|
||||
};
|
||||
let resolved = match super::resolve(
|
||||
&RoomVersionRules::V2,
|
||||
&state_sets,
|
||||
&auth_chain,
|
||||
&fetcher,
|
||||
&exists,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(e) => panic!("{e}"),
|
||||
};
|
||||
|
||||
assert_eq!(expected, resolved);
|
||||
}
|
||||
@@ -1549,13 +1548,18 @@ async fn ban_with_auth_chains2() {
|
||||
|
||||
let fetcher = |id: OwnedEventId| ready(ev_map.get(&id).cloned());
|
||||
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
|
||||
let resolved =
|
||||
match super::resolve(&RoomVersionId::V6, &state_sets, &auth_chain, &fetcher, &exists)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(e) => panic!("{e}"),
|
||||
};
|
||||
let resolved = match super::resolve(
|
||||
&RoomVersionRules::V6,
|
||||
&state_sets,
|
||||
&auth_chain,
|
||||
&fetcher,
|
||||
&exists,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(e) => panic!("{e}"),
|
||||
};
|
||||
|
||||
debug!(
|
||||
resolved = ?resolved
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
Int, OwnedUserId, UserId,
|
||||
events::{TimelineEventType, room::power_levels::RoomPowerLevelsEventContent},
|
||||
power_levels::{NotificationPowerLevels, default_power_level},
|
||||
serde::{
|
||||
deserialize_v1_powerlevel, vec_deserialize_int_powerlevel_values,
|
||||
vec_deserialize_v1_powerlevel_values,
|
||||
},
|
||||
room_version_rules::{AuthorizationRules, RoomVersionRules},
|
||||
serde::deserialize_v1_powerlevel,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Error, from_str as from_json_str};
|
||||
|
||||
use super::{Result, RoomVersion};
|
||||
use super::{Result, serde_backports::*};
|
||||
use crate::error;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -48,8 +46,11 @@ struct IntRoomPowerLevelsEventContent {
|
||||
notifications: IntNotificationPowerLevels,
|
||||
}
|
||||
|
||||
impl From<IntRoomPowerLevelsEventContent> for RoomPowerLevelsEventContent {
|
||||
fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self {
|
||||
impl IntRoomPowerLevelsEventContent {
|
||||
fn to_room_power_levels_content(
|
||||
self,
|
||||
auth_rules: &AuthorizationRules,
|
||||
) -> RoomPowerLevelsEventContent {
|
||||
let IntRoomPowerLevelsEventContent {
|
||||
ban,
|
||||
events,
|
||||
@@ -61,9 +62,9 @@ fn from(int_pl: IntRoomPowerLevelsEventContent) -> Self {
|
||||
users,
|
||||
users_default,
|
||||
notifications,
|
||||
} = int_pl;
|
||||
} = self;
|
||||
|
||||
let mut pl = Self::new();
|
||||
let mut pl = RoomPowerLevelsEventContent::new(auth_rules);
|
||||
pl.ban = ban;
|
||||
pl.events = events;
|
||||
pl.events_default = events_default;
|
||||
@@ -101,18 +102,21 @@ fn from(int_notif: IntNotificationPowerLevels) -> Self {
|
||||
#[inline]
|
||||
pub(crate) fn deserialize_power_levels(
|
||||
content: &str,
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
) -> Option<RoomPowerLevelsEventContent> {
|
||||
if room_version.integer_power_levels {
|
||||
deserialize_integer_power_levels(content)
|
||||
if room_version.authorization.integer_power_levels {
|
||||
deserialize_integer_power_levels(content, &room_version.authorization)
|
||||
} else {
|
||||
deserialize_legacy_power_levels(content)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_integer_power_levels(content: &str) -> Option<RoomPowerLevelsEventContent> {
|
||||
fn deserialize_integer_power_levels(
|
||||
content: &str,
|
||||
auth_rules: &AuthorizationRules,
|
||||
) -> Option<RoomPowerLevelsEventContent> {
|
||||
match from_json_str::<IntRoomPowerLevelsEventContent>(content) {
|
||||
| Ok(content) => Some(content.into()),
|
||||
| Ok(content) => Some(content.to_room_power_levels_content(auth_rules)),
|
||||
| Err(_) => {
|
||||
error!("m.room.power_levels event is not valid with integer values");
|
||||
None
|
||||
@@ -174,9 +178,9 @@ fn from(pl: IntPowerLevelsContentFields) -> Self {
|
||||
#[inline]
|
||||
pub(crate) fn deserialize_power_levels_content_fields(
|
||||
content: &str,
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
) -> Result<PowerLevelsContentFields, Error> {
|
||||
if room_version.integer_power_levels {
|
||||
if room_version.authorization.integer_power_levels {
|
||||
deserialize_integer_power_levels_content_fields(content)
|
||||
} else {
|
||||
deserialize_legacy_power_levels_content_fields(content)
|
||||
@@ -216,9 +220,9 @@ fn from(pl: IntPowerLevelsContentInvite) -> Self {
|
||||
|
||||
pub(crate) fn deserialize_power_levels_content_invite(
|
||||
content: &str,
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
) -> Result<PowerLevelsContentInvite, Error> {
|
||||
if room_version.integer_power_levels {
|
||||
if room_version.authorization.integer_power_levels {
|
||||
from_json_str::<IntPowerLevelsContentInvite>(content).map(Into::into)
|
||||
} else {
|
||||
from_json_str(content)
|
||||
@@ -246,9 +250,9 @@ fn from(pl: IntPowerLevelsContentRedact) -> Self {
|
||||
|
||||
pub(crate) fn deserialize_power_levels_content_redact(
|
||||
content: &str,
|
||||
room_version: &RoomVersion,
|
||||
room_version: &RoomVersionRules,
|
||||
) -> Result<PowerLevelsContentRedact, Error> {
|
||||
if room_version.integer_power_levels {
|
||||
if room_version.authorization.integer_power_levels {
|
||||
from_json_str::<IntPowerLevelsContentRedact>(content).map(Into::into)
|
||||
} else {
|
||||
from_json_str(content)
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
use ruma::RoomVersionId;
|
||||
|
||||
use super::{Error, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::exhaustive_enums)]
|
||||
pub enum RoomDisposition {
|
||||
/// A room version that has a stable specification.
|
||||
Stable,
|
||||
/// A room version that is not yet fully specified.
|
||||
Unstable,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum EventFormatVersion {
|
||||
/// $id:server event id format
|
||||
V1,
|
||||
/// MSC1659-style $hash event id format: introduced for room v3
|
||||
V2,
|
||||
/// MSC1884-style $hash format: introduced for room v4
|
||||
V3,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum StateResolutionVersion {
|
||||
/// State resolution for rooms at version 1.
|
||||
V1,
|
||||
/// State resolution for room at version 2 or later.
|
||||
V2,
|
||||
/// State resolution for room at version 12 or later.
|
||||
V2_1,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct RoomVersion {
|
||||
/// The stability of this room.
|
||||
pub disposition: RoomDisposition,
|
||||
/// The format of the EventId.
|
||||
pub event_format: EventFormatVersion,
|
||||
/// Which state resolution algorithm is used.
|
||||
pub state_res: StateResolutionVersion,
|
||||
// FIXME: not sure what this one means?
|
||||
pub enforce_key_validity: bool,
|
||||
|
||||
/// `m.room.aliases` had special auth rules and redaction rules
|
||||
/// before room version 6.
|
||||
///
|
||||
/// before MSC2261/MSC2432,
|
||||
pub special_case_aliases_auth: bool,
|
||||
/// Strictly enforce canonical json, do not allow:
|
||||
/// * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1]
|
||||
/// * Floats
|
||||
/// * NaN, Infinity, -Infinity
|
||||
pub strict_canonicaljson: bool,
|
||||
/// Verify notifications key while checking m.room.power_levels.
|
||||
///
|
||||
/// bool: MSC2209: Check 'notifications'
|
||||
pub limit_notifications_power_levels: bool,
|
||||
/// Extra rules when verifying redaction events.
|
||||
pub extra_redaction_checks: bool,
|
||||
/// Allow knocking in event authentication.
|
||||
///
|
||||
/// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/)
|
||||
pub allow_knocking: bool,
|
||||
/// Adds support for the restricted join rule.
|
||||
///
|
||||
/// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289)
|
||||
pub restricted_join_rules: bool,
|
||||
/// Adds support for the knock_restricted join rule.
|
||||
///
|
||||
/// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787)
|
||||
pub knock_restricted_join_rule: bool,
|
||||
/// Enforces integer power levels.
|
||||
///
|
||||
/// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667)
|
||||
pub integer_power_levels: bool,
|
||||
/// Determine the room creator using the `m.room.create` event's `sender`,
|
||||
/// instead of the event content's `creator` field.
|
||||
///
|
||||
/// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175)
|
||||
pub use_room_create_sender: bool,
|
||||
/// Whether the room creators are considered superusers.
|
||||
/// A superuser will always have infinite power levels in the room.
|
||||
///
|
||||
/// See: [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289)
|
||||
pub explicitly_privilege_room_creators: bool,
|
||||
/// Whether the room's m.room.create event ID is itself the room ID.
|
||||
///
|
||||
/// See: [MSC4291](https://github.com/matrix-org/matrix-spec-proposals/pull/4291)
|
||||
pub room_ids_as_hashes: bool,
|
||||
}
|
||||
|
||||
impl RoomVersion {
|
||||
pub const V1: Self = Self {
|
||||
disposition: RoomDisposition::Stable,
|
||||
event_format: EventFormatVersion::V1,
|
||||
state_res: StateResolutionVersion::V1,
|
||||
enforce_key_validity: false,
|
||||
special_case_aliases_auth: true,
|
||||
strict_canonicaljson: false,
|
||||
limit_notifications_power_levels: false,
|
||||
extra_redaction_checks: true,
|
||||
allow_knocking: false,
|
||||
restricted_join_rules: false,
|
||||
knock_restricted_join_rule: false,
|
||||
integer_power_levels: false,
|
||||
use_room_create_sender: false,
|
||||
explicitly_privilege_room_creators: false,
|
||||
room_ids_as_hashes: false,
|
||||
};
|
||||
pub const V10: Self = Self {
|
||||
knock_restricted_join_rule: true,
|
||||
integer_power_levels: true,
|
||||
..Self::V9
|
||||
};
|
||||
pub const V11: Self = Self {
|
||||
use_room_create_sender: true,
|
||||
..Self::V10
|
||||
};
|
||||
pub const V12: Self = Self {
|
||||
explicitly_privilege_room_creators: true,
|
||||
room_ids_as_hashes: true,
|
||||
..Self::V11
|
||||
};
|
||||
pub const V2: Self = Self {
|
||||
state_res: StateResolutionVersion::V2,
|
||||
..Self::V1
|
||||
};
|
||||
pub const V3: Self = Self {
|
||||
event_format: EventFormatVersion::V2,
|
||||
extra_redaction_checks: false,
|
||||
..Self::V2
|
||||
};
|
||||
pub const V4: Self = Self {
|
||||
event_format: EventFormatVersion::V3,
|
||||
..Self::V3
|
||||
};
|
||||
pub const V5: Self = Self { enforce_key_validity: true, ..Self::V4 };
|
||||
pub const V6: Self = Self {
|
||||
special_case_aliases_auth: false,
|
||||
strict_canonicaljson: true,
|
||||
limit_notifications_power_levels: true,
|
||||
..Self::V5
|
||||
};
|
||||
pub const V7: Self = Self { allow_knocking: true, ..Self::V6 };
|
||||
pub const V8: Self = Self { restricted_join_rules: true, ..Self::V7 };
|
||||
pub const V9: Self = Self::V8;
|
||||
|
||||
pub fn new(version: &RoomVersionId) -> Result<Self> {
|
||||
Ok(match version {
|
||||
| RoomVersionId::V1 => Self::V1,
|
||||
| RoomVersionId::V2 => Self::V2,
|
||||
| RoomVersionId::V3 => Self::V3,
|
||||
| RoomVersionId::V4 => Self::V4,
|
||||
| RoomVersionId::V5 => Self::V5,
|
||||
| RoomVersionId::V6 => Self::V6,
|
||||
| RoomVersionId::V7 => Self::V7,
|
||||
| RoomVersionId::V8 => Self::V8,
|
||||
| RoomVersionId::V9 => Self::V9,
|
||||
| RoomVersionId::V10 => Self::V10,
|
||||
| RoomVersionId::V11 => Self::V11,
|
||||
| RoomVersionId::V12 => Self::V12,
|
||||
| ver => return Err(Error::Unsupported(format!("found version `{ver}`"))),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! These functions are copied from an old version of Ruma. power_levels.rs uses
|
||||
//! them to lazily deserialize power level events. Upstream Ruma uses a much
|
||||
//! more elegant approach in its state resolution code, which we may want
|
||||
//! to look into at some point.
|
||||
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
use ruma::{Int, serde::deserialize_v1_powerlevel};
|
||||
use serde::{
|
||||
Deserialize, Deserializer,
|
||||
de::{MapAccess, Visitor},
|
||||
};
|
||||
|
||||
/// Take a Map with values of either an integer number or a string and
|
||||
/// deserialize those to integer numbers in a Vec of sorted pairs.
|
||||
///
|
||||
/// To be used like this:
|
||||
/// `#[serde(deserialize_with = "vec_deserialize_v1_powerlevel_values")]`
|
||||
pub fn vec_deserialize_v1_powerlevel_values<'de, D, T>(de: D) -> Result<Vec<(T, Int)>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de> + Ord,
|
||||
{
|
||||
#[repr(transparent)]
|
||||
struct IntWrap(Int);
|
||||
|
||||
impl<'de> Deserialize<'de> for IntWrap {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserialize_v1_powerlevel(deserializer).map(IntWrap)
|
||||
}
|
||||
}
|
||||
|
||||
struct IntMapVisitor<T> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> IntMapVisitor<T> {
|
||||
fn new() -> Self { Self { _phantom: PhantomData } }
|
||||
}
|
||||
|
||||
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
|
||||
where
|
||||
T: Deserialize<'de> + Ord,
|
||||
{
|
||||
type Value = Vec<(T, Int)>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a map with integers or strings as values")
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
|
||||
let mut res = Vec::new();
|
||||
if let Some(hint) = map.size_hint() {
|
||||
res.reserve(hint);
|
||||
}
|
||||
|
||||
while let Some((k, IntWrap(v))) = map.next_entry()? {
|
||||
res.push((k, v));
|
||||
}
|
||||
|
||||
res.sort_unstable();
|
||||
res.dedup_by(|a, b| a.0 == b.0);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
de.deserialize_map(IntMapVisitor::new())
|
||||
}
|
||||
|
||||
/// Take a Map with integer values and deserialize those to a Vec of sorted
|
||||
/// pairs
|
||||
///
|
||||
/// To be used like this:
|
||||
/// `#[serde(deserialize_with = "vec_deserialize_int_powerlevel_values")]`
|
||||
pub fn vec_deserialize_int_powerlevel_values<'de, D, T>(de: D) -> Result<Vec<(T, Int)>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de> + Ord,
|
||||
{
|
||||
struct IntMapVisitor<T> {
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> IntMapVisitor<T> {
|
||||
fn new() -> Self { Self { _phantom: PhantomData } }
|
||||
}
|
||||
|
||||
impl<'de, T> Visitor<'de> for IntMapVisitor<T>
|
||||
where
|
||||
T: Deserialize<'de> + Ord,
|
||||
{
|
||||
type Value = Vec<(T, Int)>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a map with integers as values")
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
|
||||
let mut res = Vec::new();
|
||||
if let Some(hint) = map.size_hint() {
|
||||
res.reserve(hint);
|
||||
}
|
||||
|
||||
while let Some(item) = map.next_entry()? {
|
||||
res.push(item);
|
||||
}
|
||||
|
||||
res.sort_unstable();
|
||||
res.dedup_by(|a, b| a.0 == b.0);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
de.deserialize_map(IntMapVisitor::new())
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
},
|
||||
int, room_id, uint, user_id,
|
||||
int, room_id,
|
||||
room_version_rules::RoomVersionRules,
|
||||
uint, user_id,
|
||||
};
|
||||
use serde_json::{
|
||||
json,
|
||||
@@ -24,7 +26,7 @@
|
||||
|
||||
use super::auth_types_for_event;
|
||||
use crate::{
|
||||
Result, RoomVersion, info,
|
||||
Result, info,
|
||||
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
|
||||
};
|
||||
|
||||
@@ -133,9 +135,14 @@ pub(crate) async fn do_check(
|
||||
let event_map = &event_map;
|
||||
let fetch = |id: OwnedEventId| ready(event_map.get(&id).cloned());
|
||||
let exists = |id: OwnedEventId| ready(event_map.get(&id).is_some());
|
||||
let resolved =
|
||||
super::resolve(&RoomVersionId::V6, state_sets, &auth_chain_sets, &fetch, &exists)
|
||||
.await;
|
||||
let resolved = super::resolve(
|
||||
&RoomVersionRules::V6,
|
||||
state_sets,
|
||||
&auth_chain_sets,
|
||||
&fetch,
|
||||
&exists,
|
||||
)
|
||||
.await;
|
||||
|
||||
match resolved {
|
||||
| Ok(state) => state,
|
||||
@@ -154,7 +161,7 @@ pub(crate) async fn do_check(
|
||||
fake_event.sender(),
|
||||
fake_event.state_key(),
|
||||
fake_event.content(),
|
||||
&RoomVersion::V6,
|
||||
&RoomVersionRules::V6,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn versions() -> Vec<String> {
|
||||
vec![
|
||||
"r0.0.1".to_owned(),
|
||||
"r0.1.0".to_owned(),
|
||||
"r0.2.0".to_owned(),
|
||||
"r0.3.0".to_owned(),
|
||||
"r0.4.0".to_owned(),
|
||||
"r0.5.0".to_owned(),
|
||||
"r0.6.0".to_owned(),
|
||||
"r0.6.1".to_owned(),
|
||||
"v1.1".to_owned(),
|
||||
"v1.2".to_owned(),
|
||||
"v1.3".to_owned(),
|
||||
"v1.4".to_owned(),
|
||||
"v1.5".to_owned(),
|
||||
"v1.8".to_owned(),
|
||||
"v1.11".to_owned(),
|
||||
"v1.12".to_owned(),
|
||||
"v1.13".to_owned(),
|
||||
"v1.14".to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn unstable_features() -> BTreeMap<String, bool> {
|
||||
BTreeMap::from_iter([
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
|
||||
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
|
||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
||||
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
|
||||
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
|
||||
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
|
||||
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
|
||||
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
|
||||
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
|
||||
])
|
||||
}
|
||||
+1
-3
@@ -25,9 +25,7 @@
|
||||
version,
|
||||
version::{name, version},
|
||||
};
|
||||
pub use matrix::{
|
||||
Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res,
|
||||
};
|
||||
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res};
|
||||
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
|
||||
pub use server::Server;
|
||||
pub use utils::{implement, result, result::Result};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{fmt, marker::PhantomData, str::FromStr};
|
||||
|
||||
use ruma::{CanonicalJsonError, CanonicalJsonObject, canonical_json::try_from_json_map};
|
||||
use ruma::{CanonicalJsonError, CanonicalJsonObject, canonical_json::to_canonical_value};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
pub fn to_canonical_object<T: serde::Serialize>(
|
||||
value: T,
|
||||
) -> Result<CanonicalJsonObject, CanonicalJsonError> {
|
||||
use CanonicalJsonError::SerDe;
|
||||
use serde::ser::Error;
|
||||
use ruma::CanonicalJsonValue;
|
||||
|
||||
match serde_json::to_value(value).map_err(SerDe)? {
|
||||
| serde_json::Value::Object(map) => try_from_json_map(map),
|
||||
| _ => Err(SerDe(serde_json::Error::custom("Value must be an object"))),
|
||||
match to_canonical_value(value)? {
|
||||
| CanonicalJsonValue::Object(map) => Ok(map),
|
||||
| _ => Err(to_canonical_value(1.0_f32).unwrap_err()), /* Hack to return a
|
||||
* CanonicalJsonError */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -415,13 +415,6 @@ fn deserialize_ignored_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Valu
|
||||
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
|
||||
)]
|
||||
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||
debug_assert_eq!(
|
||||
conduwuit::debug::type_name::<V>(),
|
||||
"serde_json::value::de::<impl serde_core::de::Deserialize for \
|
||||
serde_json::value::Value>::deserialize::ValueVisitor",
|
||||
"deserialize_any: type not expected"
|
||||
);
|
||||
|
||||
match self.record_peek_byte() {
|
||||
| Some(b'{') => self.deserialize_map(visitor),
|
||||
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
|
||||
|
||||
@@ -53,6 +53,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "disabledroomids",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "email_localpart",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "eventid_outlierpdu",
|
||||
cache_disp: CacheDisp::SharedWith("pduid_pdu"),
|
||||
@@ -100,6 +104,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "lazyloadedids",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "localpart_email",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "mediaid_file",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user