Compare commits
3 commits
main
...
jade/commu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abe9944082 | ||
|
|
c0a9bde8e0 | ||
|
|
7a36830346 |
75 changed files with 361 additions and 5398 deletions
|
|
@ -62,6 +62,10 @@ sync:
|
|||
target: registry.gitlab.com/continuwuity/continuwuity
|
||||
type: repository
|
||||
<<: *tags-main
|
||||
- source: *source
|
||||
target: git.nexy7574.co.uk/mirrored/continuwuity
|
||||
type: repository
|
||||
<<: *tags-releases
|
||||
- source: *source
|
||||
target: ghcr.io/continuwuity/continuwuity
|
||||
type: repository
|
||||
|
|
|
|||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
github: [JadedBlueEyes, nexy7574, gingershaped]
|
||||
custom:
|
||||
- https://timedout.uk/donate.html
|
||||
- https://jade.ellis.link/sponsors
|
||||
- https://ko-fi.com/nexy7574
|
||||
- https://ko-fi.com/JadedBlueEyes
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
371
Cargo.lock
generated
371
Cargo.lock
generated
|
|
@ -94,9 +94,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
|
|
@ -221,7 +221,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_derive",
|
||||
"unicode-ident",
|
||||
"winnow 0.7.15",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -461,7 +461,6 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"headers",
|
||||
|
|
@ -751,9 +750,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.57"
|
||||
version = "1.2.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
|
|
@ -824,9 +823,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -834,9 +833,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
|
|
@ -844,9 +843,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -856,9 +855,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
|
|
@ -906,7 +905,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_admin",
|
||||
|
|
@ -938,7 +937,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
|
|
@ -959,7 +958,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
|
@ -991,14 +990,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_build_metadata"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"built",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_core"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
|
|
@ -1060,7 +1059,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_database"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"conduwuit_core",
|
||||
|
|
@ -1078,7 +1077,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_macros"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
|
|
@ -1088,7 +1087,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
|
|
@ -1122,7 +1121,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_service"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"async-trait",
|
||||
|
|
@ -1164,26 +1163,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_web"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"base64 0.22.1",
|
||||
"conduwuit_build_metadata",
|
||||
"conduwuit_core",
|
||||
"conduwuit_service",
|
||||
"futures",
|
||||
"memory-serve",
|
||||
"rand 0.10.0",
|
||||
"ruma",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"tower-http",
|
||||
"tower-sec-fetch",
|
||||
"tracing",
|
||||
"validator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1266,17 +1255,6 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coolor"
|
||||
version = "1.1.0"
|
||||
|
|
@ -1529,41 +1507,6 @@ version = "2.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "817fa642fb0ee7fe42e95783e00e0969927b96091bdd4b9b1af082acd943913b"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
|
|
@ -2595,12 +2538,6 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
|
|
@ -2624,9 +2561,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
version = "0.25.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
|
|
@ -2642,8 +2579,8 @@ dependencies = [
|
|||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
"zune-core 0.5.1",
|
||||
"zune-jpeg 0.5.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2929,9 +2866,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.25"
|
||||
version = "1.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1"
|
||||
checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
|
@ -3090,22 +3027,6 @@ version = "2.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "memory-serve"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81b5bbad2035f57b1e95f66da606832edd935b47d82312e38e1ccffbcfb8a427"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"brotli",
|
||||
"flate2",
|
||||
"mime_guess",
|
||||
"sha256",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meowlnir-antispam"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3122,16 +3043,6 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minicbor"
|
||||
version = "2.2.1"
|
||||
|
|
@ -3204,9 +3115,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
|
|
@ -3567,9 +3478,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
|
|
@ -3917,29 +3828,7 @@ version = "3.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.5+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"toml_edit 0.25.4+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4077,9 +3966,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -4232,9 +4121,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.13.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
|
||||
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
|
|
@ -4267,9 +4156,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "recaptcha-verify"
|
||||
version = "0.2.0"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "409bf11a93fe93093f3c0254aab67576524f1e0524692615b5b63091dbc88a79"
|
||||
checksum = "0d694033c2b0abdbb8893edfb367f16270e790be4a67e618206d811dbe4efee4"
|
||||
dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
|
@ -4743,15 +4632,6 @@ version = "1.0.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sanitize-filename"
|
||||
version = "0.6.0"
|
||||
|
|
@ -4774,9 +4654,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
|
@ -5113,19 +4993,6 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha256"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"hex",
|
||||
"sha2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
|
|
@ -5294,12 +5161,6 @@ dependencies = [
|
|||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subslice"
|
||||
version = "0.2.3"
|
||||
|
|
@ -5440,16 +5301,16 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.11.3"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
||||
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
|
||||
dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5523,9 +5384,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.11.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
|
@ -5634,7 +5495,7 @@ dependencies = [
|
|||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.15",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5657,9 +5518,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -5675,28 +5536,28 @@ dependencies = [
|
|||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.15",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.5+spec-1.1.0"
|
||||
version = "0.25.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.0.1+spec-1.1.0",
|
||||
"toml_datetime 1.0.0+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.0",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.10+spec-1.1.0"
|
||||
version = "1.0.9+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
dependencies = [
|
||||
"winnow 1.0.0",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5707,9 +5568,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.7+spec-1.1.0"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
|
||||
[[package]]
|
||||
name = "tonic"
|
||||
|
|
@ -5800,18 +5661,6 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-sec-fetch"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff1e78d241de2527d3ef67e49d65d8cb08468c644c3aafac7a988c4accd76547"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"http",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
|
|
@ -5824,7 +5673,6 @@ version = "0.1.44"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
|
|
@ -5902,9 +5750,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
|
|
@ -6046,12 +5894,6 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
|
@ -6087,36 +5929,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
|
@ -6135,16 +5947,6 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
|
|
@ -6359,15 +6161,6 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
@ -6611,15 +6404,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
|
@ -6754,7 +6538,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"cargo_metadata",
|
||||
|
|
@ -6800,18 +6584,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.42"
|
||||
version = "0.8.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
||||
checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.42"
|
||||
version = "0.8.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
||||
checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -6912,6 +6696,12 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
|
|
@ -6929,9 +6719,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.13"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c"
|
||||
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
"zune-core 0.4.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
|
||||
dependencies = [
|
||||
"zune-core 0.5.1",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ license = "Apache-2.0"
|
|||
# See also `rust-toolchain.toml`
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
version = "0.5.7-alpha.1"
|
||||
version = "0.5.6"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
|
|
@ -99,7 +99,7 @@ features = [
|
|||
[workspace.dependencies.axum-extra]
|
||||
version = "0.12.0"
|
||||
default-features = false
|
||||
features = ["typed-header", "tracing", "cookie"]
|
||||
features = ["typed-header", "tracing"]
|
||||
|
||||
[workspace.dependencies.axum-server]
|
||||
version = "0.7.2"
|
||||
|
|
@ -969,6 +969,3 @@ needless_raw_string_hashes = "allow"
|
|||
|
||||
# TODO: Enable this lint & fix all instances
|
||||
collapsible_if = "allow"
|
||||
|
||||
# TODO: break these apart
|
||||
cognitive_complexity = "allow"
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Added support for using an admin command to issue self-service password reset links.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add Space permission cascading: power levels cascade from Spaces to child rooms, role-based room access with custom roles, continuous enforcement (auto-join/kick), and admin commands for role management. Server-wide default controlled by `space_permission_cascading` config flag (off by default), with per-Space overrides via `!admin space roles enable/disable <space>`.
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add new config option to allow or disallow search engine indexing through a `<meta ../>` tag. Defaults to blocking indexing (`content="noindex"`). Contributed by @s1lv3r and @ginger.
|
||||
|
|
@ -25,10 +25,6 @@
|
|||
#
|
||||
# Also see the `[global.well_known]` config section at the very bottom.
|
||||
#
|
||||
# If `client` is not set under `[global.well_known]`, the server name will
|
||||
# be used as the base domain for user-facing links (such as password
|
||||
# reset links) created by Continuwuity.
|
||||
#
|
||||
# Examples of delegation:
|
||||
# - https://continuwuity.org/.well-known/matrix/server
|
||||
# - https://continuwuity.org/.well-known/matrix/client
|
||||
|
|
@ -474,18 +470,6 @@
|
|||
#
|
||||
#suspend_on_register = false
|
||||
|
||||
# Server-wide default for space permission cascading (power levels and
|
||||
# role-based access). Individual Spaces can override this via the
|
||||
# `com.continuwuity.space.cascading` state event or the admin command
|
||||
# `!admin space roles enable/disable <space>`.
|
||||
#
|
||||
#space_permission_cascading = false
|
||||
|
||||
# Maximum number of spaces to cache role data for. When exceeded the
|
||||
# cache is cleared and repopulated on demand.
|
||||
#
|
||||
#space_roles_cache_flush_threshold = 1000
|
||||
|
||||
# Enabling this setting opens registration to anyone without restrictions.
|
||||
# This makes your server vulnerable to abuse
|
||||
#
|
||||
|
|
@ -1811,11 +1795,6 @@
|
|||
#
|
||||
#config_reload_signal = true
|
||||
|
||||
# Allow search engines and crawlers to index Continuwuity's built-in
|
||||
# webpages served under the `/_continuwuity/` prefix.
|
||||
#
|
||||
#allow_web_indexing = false
|
||||
|
||||
[global.tls]
|
||||
|
||||
# Path to a valid TLS certificate file.
|
||||
|
|
|
|||
|
|
@ -11,3 +11,7 @@ For either one to work correctly, you have to do some additional setup.
|
|||
|
||||
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
|
||||
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability, so you might want to configure your TURN server first. [Read the LiveKit guide](./calls/livekit.mdx)
|
||||
|
||||
:::info
|
||||
Our [`#matrixrtc:continuwuity.org`](https://matrix.to/#/#matrixrtc:continuwuity.org) room is all about calling on matrix. Join there if you have any questions!
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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 everyone: 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 @@ all members to:
|
|||
|
||||
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.
|
||||
Don't expect others to give up their time and labour for you - instead, 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 @@ within the Continuwuity community:
|
|||
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 @@ experience unacceptable behaviour, or have any other concerns, please report it.
|
|||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ services:
|
|||
### then you are ready to go.
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
restart: unless-stopped
|
||||
command: /sbin/conduwuit
|
||||
volumes:
|
||||
- db:/var/lib/continuwuity
|
||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ services:
|
|||
### then you are ready to go.
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
restart: unless-stopped
|
||||
command: /sbin/conduwuit
|
||||
volumes:
|
||||
- db:/var/lib/continuwuity
|
||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ services:
|
|||
### then you are ready to go.
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
restart: unless-stopped
|
||||
command: /sbin/conduwuit
|
||||
volumes:
|
||||
- db:/var/lib/continuwuity
|
||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ services:
|
|||
### then you are ready to go.
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
restart: unless-stopped
|
||||
command: /sbin/conduwuit
|
||||
ports:
|
||||
- 8448:6167
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ docker run -d \
|
|||
-e CONTINUWUITY_ALLOW_REGISTRATION="false" \
|
||||
--name continuwuity \
|
||||
forgejo.ellis.link/continuwuation/continuwuity:latest \
|
||||
/sbin/conduwuit --execute "users create-user admin"
|
||||
--execute "users create-user admin"
|
||||
```
|
||||
|
||||
Replace `matrix.example.com` with your actual server name and `admin` with
|
||||
|
|
@ -141,7 +141,7 @@ compose file, add under the `continuwuity` service:
|
|||
services:
|
||||
continuwuity:
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
command: /sbin/conduwuit --execute "users create-user admin"
|
||||
command: --execute "users create-user admin"
|
||||
# ... rest of configuration
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ spec:
|
|||
- name: continuwuity
|
||||
# use a sha hash <3
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
command: ["/sbin/conduwuit"]
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
|
|
|||
|
|
@ -1,226 +0,0 @@
|
|||
# Space Permission Cascading — Design Document
|
||||
|
||||
**Date:** 2026-03-17
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Server-side feature that allows user rights in a Space to cascade down to its
|
||||
direct child rooms. Includes power level cascading and role-based room access
|
||||
control. Enabled via a server-wide configuration flag, disabled by default.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Power levels defined in a Space cascade to all direct child rooms (Space
|
||||
always wins over per-room overrides).
|
||||
2. Admins can define custom roles in a Space and assign them to users.
|
||||
3. Child rooms can require one or more roles for access.
|
||||
4. Enforcement is continuous — role revocation auto-kicks users from rooms they
|
||||
no longer qualify for.
|
||||
5. Users are auto-joined to all qualifying child rooms when they join a Space or
|
||||
receive a new role.
|
||||
6. Cascading applies to direct parent Space only; no nested cascade through
|
||||
sub-spaces.
|
||||
7. Feature is toggled by a single server-wide config flag
|
||||
(`space_permission_cascading`), off by default.
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml
|
||||
# conduwuit-example.toml
|
||||
|
||||
# Enable space permission cascading (power levels and role-based access).
|
||||
# When enabled, power levels cascade from Spaces to child rooms and rooms
|
||||
# can require roles for access. Applies to all Spaces on this server.
|
||||
# Default: false
|
||||
space_permission_cascading = false
|
||||
```
|
||||
|
||||
## Custom State Events
|
||||
|
||||
All events live in the Space room.
|
||||
|
||||
### `m.space.roles` (state key: `""`)
|
||||
|
||||
Defines the available roles for the Space. Two default roles (`admin` and `mod`)
|
||||
are created automatically when a Space is first encountered with the feature
|
||||
enabled.
|
||||
|
||||
```json
|
||||
{
|
||||
"roles": {
|
||||
"admin": {
|
||||
"description": "Space administrator",
|
||||
"power_level": 100
|
||||
},
|
||||
"mod": {
|
||||
"description": "Space moderator",
|
||||
"power_level": 50
|
||||
},
|
||||
"nsfw": {
|
||||
"description": "Access to NSFW content"
|
||||
},
|
||||
"vip": {
|
||||
"description": "VIP member"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `description` (string, required): Human-readable description.
|
||||
- `power_level` (integer, optional): If present, users with this role receive
|
||||
this power level in all child rooms. When a user holds multiple roles with
|
||||
power levels, the highest value wins.
|
||||
|
||||
### `m.space.role.member` (state key: user ID)
|
||||
|
||||
Assigns roles to a user within the Space.
|
||||
|
||||
```json
|
||||
{
|
||||
"roles": ["nsfw", "vip"]
|
||||
}
|
||||
```
|
||||
|
||||
### `m.space.role.room` (state key: room ID)
|
||||
|
||||
Declares which roles a child room requires. A user must hold **all** listed
|
||||
roles to access the room.
|
||||
|
||||
```json
|
||||
{
|
||||
"required_roles": ["nsfw"]
|
||||
}
|
||||
```
|
||||
|
||||
## Enforcement Rules
|
||||
|
||||
All enforcement is skipped when `space_permission_cascading = false`.
|
||||
|
||||
### 1. Join gating
|
||||
|
||||
When a user attempts to join a room that is a direct child of a Space:
|
||||
|
||||
- Look up the room's `m.space.role.room` event in the parent Space.
|
||||
- If the room has `required_roles`, check the user's `m.space.role.member`.
|
||||
- Reject the join if the user is missing any required role.
|
||||
|
||||
### 2. Power level override
|
||||
|
||||
For every user in a child room of a Space:
|
||||
|
||||
- Look up their roles via `m.space.role.member` in the parent Space.
|
||||
- For each role that has a `power_level`, take the highest value.
|
||||
- Override the user's power level in the child room's `m.room.power_levels`.
|
||||
- Reject attempts to manually set per-room power levels that conflict with
|
||||
Space-granted levels.
|
||||
|
||||
### 3. Role revocation
|
||||
|
||||
When an `m.space.role.member` event is updated and a role is removed:
|
||||
|
||||
- Identify all child rooms that require the removed role.
|
||||
- Auto-kick the user from rooms they no longer qualify for.
|
||||
- Recalculate and update the user's power level in all child rooms.
|
||||
|
||||
### 4. Room requirement change
|
||||
|
||||
When an `m.space.role.room` event is updated with new requirements:
|
||||
|
||||
- Check all current members of the room.
|
||||
- Auto-kick members who do not hold all newly required roles.
|
||||
|
||||
### 5. Auto-join on role grant
|
||||
|
||||
When an `m.space.role.member` event is updated and a role is added:
|
||||
|
||||
- Find all child rooms where the user now meets all required roles.
|
||||
- Auto-join the user to qualifying rooms they are not already in.
|
||||
|
||||
This also applies when a user first joins the Space — they are auto-joined to
|
||||
all child rooms they qualify for. Rooms with no role requirements auto-join all
|
||||
Space members.
|
||||
|
||||
### 6. New child room
|
||||
|
||||
When a new `m.space.child` event is added to a Space:
|
||||
|
||||
- Auto-join all qualifying Space members to the new child room.
|
||||
|
||||
## Caching & Indexing
|
||||
|
||||
The source of truth is always the state events. The server maintains an
|
||||
in-memory index for fast enforcement lookups, following the same patterns as the
|
||||
existing `roomid_spacehierarchy_cache`.
|
||||
|
||||
### Index structures
|
||||
|
||||
| Index | Source event |
|
||||
|------------------------------|------------------------|
|
||||
| Space → roles defined | `m.space.roles` |
|
||||
| Space → user → roles | `m.space.role.member` |
|
||||
| Space → room → required roles| `m.space.role.room` |
|
||||
| Room → parent Space | `m.space.child` (reverse lookup) |
|
||||
|
||||
The Space → child rooms mapping already exists.
|
||||
|
||||
### Cache invalidation triggers
|
||||
|
||||
| Event changed | Action |
|
||||
|----------------------------|-----------------------------------------------------|
|
||||
| `m.space.roles` | Refresh role definitions, revalidate all members |
|
||||
| `m.space.role.member` | Refresh user's roles, trigger auto-join/kick |
|
||||
| `m.space.role.room` | Refresh room requirements, trigger auto-join/kick |
|
||||
| `m.space.child` added | Index new child, auto-join qualifying members |
|
||||
| `m.space.child` removed | Remove from index (no auto-kick) |
|
||||
| Server startup | Full rebuild from state events |
|
||||
|
||||
## Admin Room Commands
|
||||
|
||||
Roles are managed via the existing admin room interface, which sends the
|
||||
appropriate state events under the hood and triggers enforcement.
|
||||
|
||||
```
|
||||
!admin space roles list <space>
|
||||
!admin space roles add <space> <role_name> [description] [power_level]
|
||||
!admin space roles remove <space> <role_name>
|
||||
!admin space roles assign <space> <user_id> <role_name>
|
||||
!admin space roles revoke <space> <user_id> <role_name>
|
||||
!admin space roles require <space> <room_id> <role_name>
|
||||
!admin space roles unrequire <space> <room_id> <role_name>
|
||||
!admin space roles user <space> <user_id>
|
||||
!admin space roles room <space> <room_id>
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
**Approach:** Hybrid — state events for definition, database cache for
|
||||
enforcement.
|
||||
|
||||
- State events are the source of truth and federate normally.
|
||||
- The server maintains an in-memory cache/index for fast enforcement.
|
||||
- Cache is invalidated on relevant state event changes and fully rebuilt on
|
||||
startup.
|
||||
- All enforcement hooks (join gating, PL override, auto-join, auto-kick) check
|
||||
the feature flag first and no-op when disabled.
|
||||
- Existing clients can manage roles via Developer Tools (custom state events).
|
||||
The admin room commands provide a user-friendly interface.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Server-wide feature flag
|
||||
- Custom state events for role definition, assignment, and room requirements
|
||||
- Power level cascading (Space always wins)
|
||||
- Continuous enforcement (auto-join, auto-kick)
|
||||
- Admin room commands
|
||||
- In-memory caching with invalidation
|
||||
- Default `admin` (PL 100) and `mod` (PL 50) roles
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Client-side UI for role management
|
||||
- Nested cascade through sub-spaces
|
||||
- Per-space opt-in/opt-out (it is server-wide)
|
||||
- Federation-specific logic beyond normal state event replication
|
||||
File diff suppressed because it is too large
Load diff
36
flake.lock
generated
36
flake.lock
generated
|
|
@ -3,11 +3,11 @@
|
|||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1773786698,
|
||||
"narHash": "sha256-o/J7ZculgwSs1L4H4UFlFZENOXTJzq1X0n71x6oNNvY=",
|
||||
"lastModified": 1772776993,
|
||||
"narHash": "sha256-CpBa+UpogN0Xn1gMmgqQrzKGee+E8TCkgHar8/w6CRk=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "99e9de91bb8b61f06ef234ff84e11f758ecd5384",
|
||||
"rev": "b3472341e37cbd4b8c27b052b2abb34792f4d3c4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1773189535,
|
||||
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
|
||||
"lastModified": 1772560058,
|
||||
"narHash": "sha256-NuVKdMBJldwUXgghYpzIWJdfeB7ccsu1CC7B+NfSoZ8=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
|
||||
"rev": "db590d9286ed5ce22017541e36132eab4e8b3045",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -39,11 +39,11 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773732206,
|
||||
"narHash": "sha256-HKibxaUXyWd4Hs+ZUnwo6XslvaFqFqJh66uL9tphU4Q=",
|
||||
"lastModified": 1772953398,
|
||||
"narHash": "sha256-fTTHCaEvPLzWyZFxPud/G9HM3pNYmW/64Kj58hdH4+k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "0aa13c1b54063a8d8679b28a5cd357ba98f4a56b",
|
||||
"rev": "fc4863887d98fd879cf5f11af1d23d44d9bdd8ae",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -89,11 +89,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1773734432,
|
||||
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
|
||||
"lastModified": 1772773019,
|
||||
"narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
|
||||
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -132,11 +132,11 @@
|
|||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1773697963,
|
||||
"narHash": "sha256-xdKI77It9PM6eNrCcDZsnP4SKulZwk8VkDgBRVMnCb8=",
|
||||
"lastModified": 1772877513,
|
||||
"narHash": "sha256-RcRGv2Bng5I9y75XwFX7oK2l6mLH1dtbTTG9U8qun0c=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "2993637174252ff60a582fd1f55b9ab52c39db6d",
|
||||
"rev": "a1b86d600f88be98643e5dd61d6ed26eda17c09e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -153,11 +153,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773297127,
|
||||
"narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
|
||||
"lastModified": 1772660329,
|
||||
"narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
|
||||
"rev": "3710e0e1218041bbad640352a0440114b1e10428",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
61
package-lock.json
generated
61
package-lock.json
generated
|
|
@ -16,21 +16,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
|
||||
"integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.0",
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -39,9 +39,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
|
||||
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -144,22 +144,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rsbuild/plugin-react": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.6.tgz",
|
||||
"integrity": "sha512-LAT6xHlEyZKA0VjF/ph5d50iyG+WSmBx+7g98HNZUwb94VeeTMZFB8qVptTkbIRMss3BNKOXmHOu71Lhsh9oEw==",
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.5.tgz",
|
||||
"integrity": "sha512-eS2sXCedgGA/7bLu8yVtn48eE/GyPbXx4Q7OcutB01IQ1D2y8WSMBys4nwfrecy19utvw4NPn4gYDy52316+vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/plugin-react-refresh": "^1.6.1",
|
||||
"@rspack/plugin-react-refresh": "^1.6.0",
|
||||
"react-refresh": "^0.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rsbuild/core": "^1.0.0 || ^2.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rsbuild/core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding": {
|
||||
|
|
@ -700,13 +695,13 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@unhead/react": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.12.tgz",
|
||||
"integrity": "sha512-1xXFrxyw29f+kScXfEb0GxjlgtnHxoYau0qpW9k8sgWhQUNnE5gNaH3u+rNhd5IqhyvbdDRJpQ25zoz0HIyGaw==",
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.10.tgz",
|
||||
"integrity": "sha512-z9IzzkaCI1GyiBwVRMt4dGc2mOvsj9drbAdXGMy6DWpu9FwTR37ZTmAi7UeCVyIkpVdIaNalz7vkbvGG8afFng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unhead": "2.1.12"
|
||||
"unhead": "2.1.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/harlan-zw"
|
||||
|
|
@ -1578,9 +1573,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
|
||||
"integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz",
|
||||
"integrity": "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -2983,14 +2978,14 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oniguruma-to-es": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
|
||||
"integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz",
|
||||
"integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"oniguruma-parser": "^0.12.1",
|
||||
"regex": "^6.1.0",
|
||||
"regex": "^6.0.1",
|
||||
"regex-recursion": "^6.0.2"
|
||||
}
|
||||
},
|
||||
|
|
@ -3725,9 +3720,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/unhead": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz",
|
||||
"integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==",
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.10.tgz",
|
||||
"integrity": "sha512-We8l9uNF8zz6U8lfQaVG70+R/QBfQx1oPIgXin4BtZnK2IQpz6yazQ0qjMNVBDw2ADgF2ea58BtvSK+XX5AS7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use crate::{
|
|||
query::{self, QueryCommand},
|
||||
room::{self, RoomCommand},
|
||||
server::{self, ServerCommand},
|
||||
space::{self, SpaceCommand},
|
||||
token::{self, TokenCommand},
|
||||
user::{self, UserCommand},
|
||||
};
|
||||
|
|
@ -35,10 +34,6 @@ pub enum AdminCommand {
|
|||
/// Commands for managing rooms
|
||||
Rooms(RoomCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing space permissions
|
||||
Spaces(SpaceCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing federation
|
||||
Federation(FederationCommand),
|
||||
|
|
@ -86,10 +81,6 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
|||
token::process(command, context).await
|
||||
},
|
||||
| Rooms(command) => room::process(command, context).await,
|
||||
| Spaces(command) => {
|
||||
context.bail_restricted()?;
|
||||
space::process(command, context).await
|
||||
},
|
||||
| Federation(command) => federation::process(command, context).await,
|
||||
| Server(command) => server::process(command, context).await,
|
||||
| Debug(command) => debug::process(command, context).await,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ pub(crate) mod media;
|
|||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod space;
|
||||
pub(crate) mod token;
|
||||
pub(crate) mod user;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
pub(super) mod roles;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduwuit::Result;
|
||||
|
||||
use self::roles::SpaceRolesCommand;
|
||||
use crate::admin_command_dispatch;
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SpaceCommand {
|
||||
#[command(subcommand)]
|
||||
/// Manage space roles and permissions
|
||||
Roles(SpaceRolesCommand),
|
||||
}
|
||||
|
|
@ -1,632 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduwuit::{Err, Event, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit_core::matrix::space_roles::{
|
||||
RoleDefinition, SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE, SpaceCascadingEventContent,
|
||||
SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, SpaceRolesEventContent,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, events::StateEventType};
|
||||
use serde_json::value::to_raw_value;
|
||||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
fn roles_event_type() -> StateEventType {
|
||||
StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned())
|
||||
}
|
||||
|
||||
fn member_event_type() -> StateEventType {
|
||||
StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned())
|
||||
}
|
||||
|
||||
fn room_event_type() -> StateEventType {
|
||||
StateEventType::from(SPACE_ROLE_ROOM_EVENT_TYPE.to_owned())
|
||||
}
|
||||
|
||||
fn cascading_event_type() -> StateEventType {
|
||||
StateEventType::from(SPACE_CASCADING_EVENT_TYPE.to_owned())
|
||||
}
|
||||
|
||||
macro_rules! resolve_room_as_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
let space_id = $self.services.rooms.alias.resolve(&$space).await?;
|
||||
if !matches!(
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
space_id
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! resolve_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
let space_id = resolve_room_as_space!($self, $space);
|
||||
if !$self
|
||||
.services
|
||||
.rooms
|
||||
.roles
|
||||
.is_enabled_for_space(&space_id)
|
||||
.await
|
||||
{
|
||||
return $self
|
||||
.write_str(
|
||||
"Space permission cascading is disabled for this Space. Enable it \
|
||||
server-wide with `space_permission_cascading = true` in your config, or \
|
||||
per-Space with `!admin space roles enable <space>`.",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
space_id
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! custom_state_pdu {
|
||||
($event_type:expr, $state_key:expr, $content:expr) => {
|
||||
PduBuilder {
|
||||
event_type: $event_type.to_owned().into(),
|
||||
content: to_raw_value($content)
|
||||
.map_err(|e| conduwuit::err!("Failed to serialize state event content: {e}"))?,
|
||||
state_key: Some($state_key.to_owned().into()),
|
||||
..PduBuilder::default()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Cascade-remove a role name from all state events of a given type. For each
|
||||
/// event that contains the role, the `$field` is filtered and the updated
|
||||
/// content is sent back as a new state event.
|
||||
macro_rules! cascade_remove_role {
|
||||
(
|
||||
$self:expr,
|
||||
$shortstatehash:expr,
|
||||
$event_type_fn:expr,
|
||||
$event_type_const:expr,
|
||||
$content_ty:ty,
|
||||
$field:ident,
|
||||
$role_name:expr,
|
||||
$space_id:expr,
|
||||
$state_lock:expr,
|
||||
$server_user:expr
|
||||
) => {{
|
||||
let ev_type = $event_type_fn;
|
||||
let entries: Vec<(_, ruma::OwnedEventId)> = $self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_keys_with_ids($shortstatehash, &ev_type)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (state_key, event_id) in entries {
|
||||
if let Ok(pdu) = $self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||
if let Ok(mut content) = pdu.get_content::<$content_ty>() {
|
||||
if content.$field.contains($role_name) {
|
||||
content.$field.retain(|r| r != $role_name);
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!($event_type_const, &state_key, &content),
|
||||
$server_user,
|
||||
Some(&$space_id),
|
||||
&$state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! send_space_state {
|
||||
($self:expr, $space_id:expr, $event_type:expr, $state_key:expr, $content:expr) => {{
|
||||
let state_lock = $self.services.rooms.state.mutex.lock(&$space_id).await;
|
||||
let server_user = &$self.services.globals.server_user;
|
||||
$self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!($event_type, $state_key, $content),
|
||||
server_user,
|
||||
Some(&$space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?
|
||||
}};
|
||||
}
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SpaceRolesCommand {
|
||||
/// List all roles defined in a space
|
||||
List {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Add a new role to a space
|
||||
Add {
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
#[arg(long)]
|
||||
description: Option<String>,
|
||||
#[arg(long)]
|
||||
power_level: Option<i64>,
|
||||
},
|
||||
/// Remove a role from a space
|
||||
Remove {
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Assign a role to a user
|
||||
Assign {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Revoke a role from a user
|
||||
Revoke {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Require a role for a room
|
||||
Require {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Remove a role requirement from a room
|
||||
Unrequire {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
},
|
||||
/// Show a user's roles in a space
|
||||
User {
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
/// Show a room's role requirements in a space
|
||||
Room {
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
},
|
||||
/// Enable space permission cascading for a specific space (overrides
|
||||
/// server config)
|
||||
Enable {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Disable space permission cascading for a specific space (overrides
|
||||
/// server config)
|
||||
Disable {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Show whether cascading is enabled for a space and the source (server
|
||||
/// default or per-space override)
|
||||
Status {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn list(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
let roles_event_type = roles_event_type();
|
||||
|
||||
let content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.is_empty() {
|
||||
return self.write_str("No roles defined in this space.").await;
|
||||
}
|
||||
|
||||
let mut msg = format!("Roles in {space_id}:\n```\n");
|
||||
for (name, def) in &content.roles {
|
||||
let pl = def
|
||||
.power_level
|
||||
.map(|p| format!(" (power_level: {p})"))
|
||||
.unwrap_or_default();
|
||||
let _ = writeln!(msg, "- {name}: {}{pl}", def.description);
|
||||
}
|
||||
msg.push_str("```");
|
||||
|
||||
self.write_str(&msg).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn add(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
role_name: String,
|
||||
description: Option<String>,
|
||||
power_level: Option<i64>,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
if let Some(pl) = power_level {
|
||||
if pl > i64::from(ruma::Int::MAX) || pl < i64::from(ruma::Int::MIN) {
|
||||
return Err!(
|
||||
"Power level must be between {} and {}.",
|
||||
ruma::Int::MIN,
|
||||
ruma::Int::MAX
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let roles_event_type = roles_event_type();
|
||||
|
||||
let mut content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.contains_key(&role_name) {
|
||||
return Err!("Role '{role_name}' already exists in this space.");
|
||||
}
|
||||
|
||||
content.roles.insert(role_name.clone(), RoleDefinition {
|
||||
description: description.unwrap_or_else(|| role_name.clone()),
|
||||
power_level,
|
||||
});
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||
|
||||
self.write_str(&format!("Added role '{role_name}' to space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
let roles_event_type = roles_event_type();
|
||||
|
||||
let mut content: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.remove(&role_name).is_none() {
|
||||
return Err!("Role '{role_name}' does not exist in this space.");
|
||||
}
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||
|
||||
// Cascade: remove the deleted role from all member and room events
|
||||
let server_user = &self.services.globals.server_user;
|
||||
if let Ok(shortstatehash) = self
|
||||
.services
|
||||
.rooms
|
||||
.state
|
||||
.get_room_shortstatehash(&space_id)
|
||||
.await
|
||||
{
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
|
||||
cascade_remove_role!(
|
||||
self,
|
||||
shortstatehash,
|
||||
member_event_type(),
|
||||
SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SpaceRoleMemberEventContent,
|
||||
roles,
|
||||
&role_name,
|
||||
space_id,
|
||||
state_lock,
|
||||
server_user
|
||||
);
|
||||
|
||||
cascade_remove_role!(
|
||||
self,
|
||||
shortstatehash,
|
||||
room_event_type(),
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
SpaceRoleRoomEventContent,
|
||||
required_roles,
|
||||
&role_name,
|
||||
space_id,
|
||||
state_lock,
|
||||
server_user
|
||||
);
|
||||
}
|
||||
|
||||
self.write_str(&format!("Removed role '{role_name}' from space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn assign(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
let roles_event_type = roles_event_type();
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !role_defs.roles.contains_key(&role_name) {
|
||||
return Err!("Role '{role_name}' does not exist in this space.");
|
||||
}
|
||||
|
||||
let member_event_type = member_event_type();
|
||||
|
||||
let mut content: SpaceRoleMemberEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.roles.contains(&role_name) {
|
||||
return Err!("User {user_id} already has role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
content.roles.push(role_name.clone());
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!("Assigned role '{role_name}' to {user_id} in space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn revoke(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
user_id: OwnedUserId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
let member_event_type = member_event_type();
|
||||
|
||||
let mut content: SpaceRoleMemberEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let original_len = content.roles.len();
|
||||
content.roles.retain(|r| r != &role_name);
|
||||
|
||||
if content.roles.len() == original_len {
|
||||
return Err!("User {user_id} does not have role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!("Revoked role '{role_name}' from {user_id} in space {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn require(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
let child_rooms = self.services.rooms.roles.get_child_rooms(&space_id).await;
|
||||
if !child_rooms.contains(&room_id) {
|
||||
return Err!("Room {room_id} is not a child of space {space_id}.");
|
||||
}
|
||||
|
||||
let roles_event_type = roles_event_type();
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !role_defs.roles.contains_key(&role_name) {
|
||||
return Err!("Role '{role_name}' does not exist in this space.");
|
||||
}
|
||||
|
||||
let room_event_type = room_event_type();
|
||||
|
||||
let mut content: SpaceRoleRoomEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if content.required_roles.contains(&role_name) {
|
||||
return Err!("Room {room_id} already requires role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
content.required_roles.push(role_name.clone());
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!(
|
||||
"Room {room_id} now requires role '{role_name}' in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn unrequire(
|
||||
&self,
|
||||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
role_name: String,
|
||||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
let room_event_type = room_event_type();
|
||||
|
||||
let mut content: SpaceRoleRoomEventContent = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let original_len = content.required_roles.len();
|
||||
content.required_roles.retain(|r| r != &role_name);
|
||||
|
||||
if content.required_roles.len() == original_len {
|
||||
return Err!("Room {room_id} does not require role '{role_name}' in this space.");
|
||||
}
|
||||
|
||||
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||
|
||||
self.write_str(&format!(
|
||||
"Removed role requirement '{role_name}' from room {room_id} in space {space_id}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn user(&self, space: OwnedRoomOrAliasId, user_id: OwnedUserId) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
let roles = self
|
||||
.services
|
||||
.rooms
|
||||
.roles
|
||||
.get_user_roles_in_space(&space_id, &user_id)
|
||||
.await;
|
||||
|
||||
match roles {
|
||||
| Some(roles) if !roles.is_empty() => {
|
||||
let list: String = roles
|
||||
.iter()
|
||||
.map(|r| format!("- {r}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.write_str(&format!("Roles for {user_id} in space {space_id}:\n```\n{list}\n```"))
|
||||
.await
|
||||
},
|
||||
| _ =>
|
||||
self.write_str(&format!("User {user_id} has no roles in space {space_id}."))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
let reqs = self
|
||||
.services
|
||||
.rooms
|
||||
.roles
|
||||
.get_room_requirements_in_space(&space_id, &room_id)
|
||||
.await;
|
||||
|
||||
match reqs {
|
||||
| Some(reqs) if !reqs.is_empty() => {
|
||||
let list: String = reqs
|
||||
.iter()
|
||||
.map(|r| format!("- {r}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
self.write_str(&format!(
|
||||
"Required roles for room {room_id} in space {space_id}:\n```\n{list}\n```"
|
||||
))
|
||||
.await
|
||||
},
|
||||
| _ =>
|
||||
self.write_str(&format!(
|
||||
"Room {room_id} has no role requirements in space {space_id}."
|
||||
))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn enable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.roles
|
||||
.ensure_default_roles(&space_id)
|
||||
.await?;
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: true };
|
||||
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||
|
||||
self.write_str(&format!("Space permission cascading enabled for {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn disable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: false };
|
||||
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||
|
||||
self.write_str(&format!("Space permission cascading disabled for {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn status(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = resolve_room_as_space!(self, space);
|
||||
|
||||
let global_default = self.services.rooms.roles.is_enabled();
|
||||
let cascading_event_type = cascading_event_type();
|
||||
let per_space_override: Option<bool> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<SpaceCascadingEventContent>(
|
||||
&space_id,
|
||||
&cascading_event_type,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|c| c.enabled);
|
||||
|
||||
let effective = per_space_override.unwrap_or(global_default);
|
||||
let source = match per_space_override {
|
||||
| Some(v) => format!("per-Space override (enabled: {v})"),
|
||||
| None => format!("server default (space_permission_cascading: {global_default})"),
|
||||
};
|
||||
|
||||
self.write_str(&format!(
|
||||
"Cascading status for {space_id}:\n- Effective: **{effective}**\n- Source: {source}"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
|
@ -296,31 +296,6 @@ pub(super) async fn reset_password(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
|
||||
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
|
||||
|
||||
self.bail_restricted()?;
|
||||
|
||||
let mut reset_url = self
|
||||
.services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(PASSWORD_RESET_PATH)
|
||||
.unwrap();
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
let token = self.services.password_reset.issue_token(user_id).await?;
|
||||
reset_url
|
||||
.query_pairs_mut()
|
||||
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
|
||||
|
||||
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
||||
if self.body.len() < 2
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ pub enum UserCommand {
|
|||
password: Option<String>,
|
||||
},
|
||||
|
||||
/// Issue a self-service password reset link for a user.
|
||||
IssuePasswordResetLink {
|
||||
/// Username of the user who may use the link
|
||||
username: String,
|
||||
},
|
||||
|
||||
/// Deactivate a user
|
||||
///
|
||||
/// User will be removed from all rooms by default.
|
||||
|
|
|
|||
|
|
@ -347,12 +347,6 @@ pub async fn join_room_by_id_helper(
|
|||
}
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
.roles
|
||||
.check_join_allowed(room_id, sender_user)
|
||||
.await?;
|
||||
|
||||
if server_in_room {
|
||||
join_room_by_id_helper_local(services, sender_user, room_id, reason, servers, state_lock)
|
||||
.boxed()
|
||||
|
|
|
|||
|
|
@ -68,10 +68,6 @@ pub struct Config {
|
|||
///
|
||||
/// Also see the `[global.well_known]` config section at the very bottom.
|
||||
///
|
||||
/// If `client` is not set under `[global.well_known]`, the server name will
|
||||
/// be used as the base domain for user-facing links (such as password
|
||||
/// reset links) created by Continuwuity.
|
||||
///
|
||||
/// Examples of delegation:
|
||||
/// - https://continuwuity.org/.well-known/matrix/server
|
||||
/// - https://continuwuity.org/.well-known/matrix/client
|
||||
|
|
@ -607,22 +603,6 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub suspend_on_register: bool,
|
||||
|
||||
/// Server-wide default for space permission cascading (power levels and
|
||||
/// role-based access). Individual Spaces can override this via the
|
||||
/// `com.continuwuity.space.cascading` state event or the admin command
|
||||
/// `!admin space roles enable/disable <space>`.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub space_permission_cascading: bool,
|
||||
|
||||
/// Maximum number of spaces to cache role data for. When exceeded the
|
||||
/// cache is cleared and repopulated on demand.
|
||||
///
|
||||
/// default: 1000
|
||||
#[serde(default = "default_space_roles_cache_flush_threshold")]
|
||||
pub space_roles_cache_flush_threshold: u32,
|
||||
|
||||
/// Enabling this setting opens registration to anyone without restrictions.
|
||||
/// This makes your server vulnerable to abuse
|
||||
#[serde(default)]
|
||||
|
|
@ -2109,13 +2089,6 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub force_disable_first_run_mode: bool,
|
||||
|
||||
/// Allow search engines and crawlers to index Continuwuity's built-in
|
||||
/// webpages served under the `/_continuwuity/` prefix.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub allow_web_indexing: bool,
|
||||
|
||||
/// display: nested
|
||||
#[serde(default)]
|
||||
pub ldap: LdapConfig,
|
||||
|
|
@ -2853,5 +2826,3 @@ fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() }
|
|||
fn default_ldap_uid_attribute() -> String { String::from("uid") }
|
||||
|
||||
fn default_ldap_name_attribute() -> String { String::from("givenName") }
|
||||
|
||||
fn default_space_roles_cache_flush_threshold() -> u32 { 1000 }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
pub mod event;
|
||||
pub mod pdu;
|
||||
pub mod space_roles;
|
||||
pub mod state_key;
|
||||
pub mod state_res;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const SPACE_ROLES_EVENT_TYPE: &str = "com.continuwuity.space.roles";
|
||||
pub const SPACE_ROLE_MEMBER_EVENT_TYPE: &str = "com.continuwuity.space.role.member";
|
||||
pub const SPACE_ROLE_ROOM_EVENT_TYPE: &str = "com.continuwuity.space.role.room";
|
||||
pub const SPACE_CASCADING_EVENT_TYPE: &str = "com.continuwuity.space.cascading";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRolesEventContent {
|
||||
pub roles: BTreeMap<String, RoleDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RoleDefinition {
|
||||
pub description: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub power_level: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRoleMemberEventContent {
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRoleRoomEventContent {
|
||||
pub required_roles: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceCascadingEventContent {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn space_roles_roundtrip() {
|
||||
let mut roles = BTreeMap::new();
|
||||
roles.insert("admin".to_owned(), RoleDefinition {
|
||||
description: "Space administrator".to_owned(),
|
||||
power_level: Some(100),
|
||||
});
|
||||
roles.insert("nsfw".to_owned(), RoleDefinition {
|
||||
description: "NSFW access".to_owned(),
|
||||
power_level: None,
|
||||
});
|
||||
let content = SpaceRolesEventContent { roles };
|
||||
let json = serde_json::to_string(&content).unwrap();
|
||||
let deserialized: SpaceRolesEventContent = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.roles["admin"].power_level, Some(100));
|
||||
assert!(deserialized.roles["nsfw"].power_level.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_omitted_in_serialization_when_none() {
|
||||
let role = RoleDefinition {
|
||||
description: "Test".to_owned(),
|
||||
power_level: None,
|
||||
};
|
||||
let json = serde_json::to_string(&role).unwrap();
|
||||
assert!(!json.contains("power_level"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_power_level() {
|
||||
let json = r#"{"description":"Restricted","power_level":-10}"#;
|
||||
let role: RoleDefinition = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(role.power_level, Some(-10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_description_fails() {
|
||||
let json = r#"{"power_level":100}"#;
|
||||
serde_json::from_str::<RoleDefinition>(json).unwrap_err();
|
||||
}
|
||||
}
|
||||
|
|
@ -1224,7 +1224,6 @@ 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,
|
||||
power_event: &impl Event,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ type Result<T, E = Error> = crate::Result<T, E>;
|
|||
/// event is part of the same room.
|
||||
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
|
||||
//#[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,
|
||||
state_sets: Sets,
|
||||
|
|
|
|||
|
|
@ -112,10 +112,6 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||
name: "onetimekeyid_onetimekeys",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "passwordresettoken_info",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "pduid_pdu",
|
||||
cache_disp: CacheDisp::SharedWith("eventid_outlierpdu"),
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
|||
}
|
||||
|
||||
async fn not_found(_uri: Uri) -> impl IntoResponse {
|
||||
Error::Request(ErrorKind::Unrecognized, "not found :(".into(), StatusCode::NOT_FOUND)
|
||||
Error::Request(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ webpage.workspace = true
|
|||
webpage.optional = true
|
||||
blurhash.workspace = true
|
||||
blurhash.optional = true
|
||||
recaptcha-verify = { version = "0.2.0", default-features = false }
|
||||
recaptcha-verify = { version = "0.1.5", default-features = false }
|
||||
yansi.workspace = true
|
||||
|
||||
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use conduwuit::{
|
|||
config::{Config, check},
|
||||
error, implement,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::registration_tokens::{ValidToken, ValidTokenSource};
|
||||
|
||||
|
|
@ -24,18 +23,6 @@ impl Service {
|
|||
.clone()
|
||||
.map(|token| ValidToken { token, source: ValidTokenSource::Config })
|
||||
}
|
||||
|
||||
/// Get the base domain to use for user-facing URLs.
|
||||
#[must_use]
|
||||
pub fn get_client_domain(&self) -> Url {
|
||||
self.well_known.client.clone().unwrap_or_else(|| {
|
||||
let host = self.server_name.host();
|
||||
format!("https://{host}")
|
||||
.as_str()
|
||||
.try_into()
|
||||
.expect("server name should be a valid host")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ pub mod globals;
|
|||
pub mod key_backups;
|
||||
pub mod media;
|
||||
pub mod moderation;
|
||||
pub mod password_reset;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
pub mod registration_tokens;
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use conduwuit::utils::{ReadyExt, stream::TryExpect};
|
||||
use database::{Database, Deserialized, Json, Map};
|
||||
use ruma::{OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub(super) struct Data {
|
||||
passwordresettoken_info: Arc<Map>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResetTokenInfo {
|
||||
pub user: OwnedUserId,
|
||||
pub issued_at: SystemTime,
|
||||
}
|
||||
|
||||
impl ResetTokenInfo {
|
||||
// one hour
|
||||
const MAX_TOKEN_AGE: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
let now = SystemTime::now();
|
||||
|
||||
now.duration_since(self.issued_at)
|
||||
.is_ok_and(|duration| duration < Self::MAX_TOKEN_AGE)
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub(super) fn new(db: &Arc<Database>) -> Self {
|
||||
Self {
|
||||
passwordresettoken_info: db["passwordresettoken_info"].clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Associate a reset token with its info in the database.
|
||||
pub(super) fn save_token(&self, token: &str, info: &ResetTokenInfo) {
|
||||
self.passwordresettoken_info.raw_put(token, Json(info));
|
||||
}
|
||||
|
||||
/// Lookup the info for a reset token.
|
||||
pub(super) async fn lookup_token_info(&self, token: &str) -> Option<ResetTokenInfo> {
|
||||
self.passwordresettoken_info
|
||||
.get(token)
|
||||
.await
|
||||
.deserialized()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Find a user's existing reset token, if any.
|
||||
pub(super) async fn find_token_for_user(
|
||||
&self,
|
||||
user: &UserId,
|
||||
) -> Option<(String, ResetTokenInfo)> {
|
||||
self.passwordresettoken_info
|
||||
.stream::<'_, String, ResetTokenInfo>()
|
||||
.expect_ok()
|
||||
.ready_find(|(_, info)| info.user == user)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Remove a reset token.
|
||||
pub(super) fn remove_token(&self, token: &str) { self.passwordresettoken_info.remove(token); }
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
mod data;
|
||||
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use data::{Data, ResetTokenInfo};
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use crate::{Dep, globals, users};
|
||||
|
||||
pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/account/reset_password";
|
||||
pub const RESET_TOKEN_QUERY_PARAM: &str = "token";
|
||||
const RESET_TOKEN_LENGTH: usize = 32;
|
||||
|
||||
pub struct Service {
|
||||
db: Data,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
struct Services {
|
||||
users: Dep<users::Service>,
|
||||
globals: Dep<globals::Service>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ValidResetToken {
|
||||
pub token: String,
|
||||
pub info: ResetTokenInfo,
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
db: Data::new(args.db),
|
||||
services: Services {
|
||||
users: args.depend::<users::Service>("users"),
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Generate a random string suitable to be used as a password reset token.
|
||||
#[must_use]
|
||||
pub fn generate_token_string() -> String { utils::random_string(RESET_TOKEN_LENGTH) }
|
||||
|
||||
/// Issue a password reset token for `user`, who must be a local user with
|
||||
/// the `password` origin.
|
||||
pub async fn issue_token(&self, user_id: OwnedUserId) -> Result<ValidResetToken> {
|
||||
if !self.services.globals.user_is_local(&user_id) {
|
||||
return Err!("Cannot issue a password reset token for remote user {user_id}");
|
||||
}
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Cannot issue a password reset token for the server user");
|
||||
}
|
||||
|
||||
if self
|
||||
.services
|
||||
.users
|
||||
.origin(&user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| "password".to_owned())
|
||||
!= "password"
|
||||
{
|
||||
return Err!("Cannot issue a password reset token for non-internal user {user_id}");
|
||||
}
|
||||
|
||||
if self.services.users.is_deactivated(&user_id).await? {
|
||||
return Err!("Cannot issue a password reset token for deactivated user {user_id}");
|
||||
}
|
||||
|
||||
if let Some((existing_token, _)) = self.db.find_token_for_user(&user_id).await {
|
||||
self.db.remove_token(&existing_token);
|
||||
}
|
||||
|
||||
let token = Self::generate_token_string();
|
||||
let info = ResetTokenInfo {
|
||||
user: user_id,
|
||||
issued_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.db.save_token(&token, &info);
|
||||
|
||||
Ok(ValidResetToken { token, info })
|
||||
}
|
||||
|
||||
/// Check if `token` represents a valid, non-expired password reset token.
|
||||
pub async fn check_token(&self, token: &str) -> Option<ValidResetToken> {
|
||||
self.db.lookup_token_info(token).await.and_then(|info| {
|
||||
if info.is_valid() {
|
||||
Some(ValidResetToken { token: token.to_owned(), info })
|
||||
} else {
|
||||
self.db.remove_token(token);
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Consume the supplied valid token, using it to change its user's password
|
||||
/// to `new_password`.
|
||||
pub async fn consume_token(
|
||||
&self,
|
||||
ValidResetToken { token, info }: ValidResetToken,
|
||||
new_password: &str,
|
||||
) -> Result<()> {
|
||||
if info.is_valid() {
|
||||
self.db.remove_token(&token);
|
||||
self.services
|
||||
.users
|
||||
.set_password(&info.user, Some(new_password))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ pub mod metadata;
|
|||
pub mod outlier;
|
||||
pub mod pdu_metadata;
|
||||
pub mod read_receipt;
|
||||
pub mod roles;
|
||||
pub mod search;
|
||||
pub mod short;
|
||||
pub mod spaces;
|
||||
|
|
@ -32,7 +31,6 @@ pub struct Service {
|
|||
pub outlier: Arc<outlier::Service>,
|
||||
pub pdu_metadata: Arc<pdu_metadata::Service>,
|
||||
pub read_receipt: Arc<read_receipt::Service>,
|
||||
pub roles: Arc<roles::Service>,
|
||||
pub search: Arc<search::Service>,
|
||||
pub short: Arc<short::Service>,
|
||||
pub spaces: Arc<spaces::Service>,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,204 +0,0 @@
|
|||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use conduwuit_core::matrix::space_roles::RoleDefinition;
|
||||
|
||||
use super::{compute_user_power_level, roles_satisfy_requirements};
|
||||
|
||||
pub(super) fn make_roles(entries: &[(&str, Option<i64>)]) -> BTreeMap<String, RoleDefinition> {
|
||||
entries
|
||||
.iter()
|
||||
.map(|(name, pl)| {
|
||||
((*name).to_owned(), RoleDefinition {
|
||||
description: format!("{name} role"),
|
||||
power_level: *pl,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn make_set(items: &[&str]) -> HashSet<String> {
|
||||
items.iter().map(|s| (*s).to_owned()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_single_role() {
|
||||
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50))]);
|
||||
assert_eq!(compute_user_power_level(&roles, &make_set(&["admin"])), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_multiple_roles_takes_highest() {
|
||||
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50)), ("helper", Some(25))]);
|
||||
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "helper"])), Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_no_power_roles() {
|
||||
let roles = make_roles(&[("nsfw", None), ("vip", None)]);
|
||||
assert_eq!(compute_user_power_level(&roles, &make_set(&["nsfw", "vip"])), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_mixed_roles() {
|
||||
let roles = make_roles(&[("mod", Some(50)), ("nsfw", None)]);
|
||||
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "nsfw"])), Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_no_roles_assigned() {
|
||||
let roles = make_roles(&[("admin", Some(100))]);
|
||||
assert_eq!(compute_user_power_level(&roles, &HashSet::new()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_level_unknown_role_ignored() {
|
||||
let roles = make_roles(&[("admin", Some(100))]);
|
||||
assert_eq!(compute_user_power_level(&roles, &make_set(&["nonexistent"])), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualifies_with_all_required_roles() {
|
||||
assert!(roles_satisfy_requirements(
|
||||
&make_set(&["nsfw", "vip"]),
|
||||
&make_set(&["nsfw", "vip", "extra"]),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_qualify_missing_one_role() {
|
||||
assert!(!roles_satisfy_requirements(&make_set(&["nsfw", "vip"]), &make_set(&["nsfw"]),));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualifies_with_no_requirements() {
|
||||
assert!(roles_satisfy_requirements(&HashSet::new(), &make_set(&["nsfw"])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_qualify_with_no_roles() {
|
||||
assert!(!roles_satisfy_requirements(&make_set(&["nsfw"]), &HashSet::new()));
|
||||
}
|
||||
|
||||
// Multi-space scenarios
|
||||
|
||||
#[test]
|
||||
fn multi_space_highest_pl_wins() {
|
||||
let space_a_roles = make_roles(&[("mod", Some(50))]);
|
||||
let space_b_roles = make_roles(&[("admin", Some(100))]);
|
||||
|
||||
let user_roles_a = make_set(&["mod"]);
|
||||
let user_roles_b = make_set(&["admin"]);
|
||||
|
||||
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||
|
||||
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||
assert_eq!(effective, Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_one_space_has_no_pl() {
|
||||
let space_a_roles = make_roles(&[("nsfw", None)]);
|
||||
let space_b_roles = make_roles(&[("mod", Some(50))]);
|
||||
|
||||
let user_roles_a = make_set(&["nsfw"]);
|
||||
let user_roles_b = make_set(&["mod"]);
|
||||
|
||||
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||
|
||||
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||
assert_eq!(effective, Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_neither_has_pl() {
|
||||
let space_a_roles = make_roles(&[("nsfw", None)]);
|
||||
let space_b_roles = make_roles(&[("vip", None)]);
|
||||
|
||||
let user_roles_a = make_set(&["nsfw"]);
|
||||
let user_roles_b = make_set(&["vip"]);
|
||||
|
||||
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||
|
||||
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||
assert_eq!(effective, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_user_only_in_one_space() {
|
||||
let space_a_roles = make_roles(&[("admin", Some(100))]);
|
||||
let space_b_roles = make_roles(&[("mod", Some(50))]);
|
||||
|
||||
let user_roles_a = make_set(&["admin"]);
|
||||
let user_roles_b: HashSet<String> = HashSet::new();
|
||||
|
||||
let pl_a = compute_user_power_level(&space_a_roles, &user_roles_a);
|
||||
let pl_b = compute_user_power_level(&space_b_roles, &user_roles_b);
|
||||
|
||||
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||
assert_eq!(effective, Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_qualifies_in_one_not_other() {
|
||||
let space_a_reqs = make_set(&["staff"]);
|
||||
let space_b_reqs = make_set(&["nsfw"]);
|
||||
|
||||
let user_roles = make_set(&["nsfw"]);
|
||||
|
||||
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles));
|
||||
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_qualifies_after_role_revoke_via_other_space() {
|
||||
let space_a_reqs = make_set(&["nsfw"]);
|
||||
let space_b_reqs = make_set(&["vip"]);
|
||||
|
||||
let user_roles_after_revoke = make_set(&["vip"]);
|
||||
|
||||
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles_after_revoke));
|
||||
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles_after_revoke));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_room_has_reqs_in_one_space_only() {
|
||||
let space_a_reqs = make_set(&["admin"]);
|
||||
let space_b_reqs: HashSet<String> = HashSet::new();
|
||||
|
||||
let user_roles = make_set(&["nsfw"]);
|
||||
|
||||
assert!(!roles_satisfy_requirements(&space_a_reqs, &user_roles));
|
||||
assert!(roles_satisfy_requirements(&space_b_reqs, &user_roles));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_no_qualification_anywhere() {
|
||||
let space_a_reqs = make_set(&["staff"]);
|
||||
let space_b_reqs = make_set(&["admin"]);
|
||||
|
||||
let user_roles = make_set(&["nsfw"]);
|
||||
|
||||
let qualifies_a = roles_satisfy_requirements(&space_a_reqs, &user_roles);
|
||||
let qualifies_b = roles_satisfy_requirements(&space_b_reqs, &user_roles);
|
||||
|
||||
assert!(!qualifies_a);
|
||||
assert!(!qualifies_b);
|
||||
assert!(!(qualifies_a || qualifies_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_space_same_role_different_pl() {
|
||||
let space_a_roles = make_roles(&[("mod", Some(50))]);
|
||||
let space_b_roles = make_roles(&[("mod", Some(75))]);
|
||||
|
||||
let user_roles = make_set(&["mod"]);
|
||||
|
||||
let pl_a = compute_user_power_level(&space_a_roles, &user_roles);
|
||||
let pl_b = compute_user_power_level(&space_b_roles, &user_roles);
|
||||
|
||||
let effective = [pl_a, pl_b].into_iter().flatten().max();
|
||||
assert_eq!(effective, Some(75));
|
||||
}
|
||||
|
|
@ -327,7 +327,7 @@ where
|
|||
}
|
||||
},
|
||||
| TimelineEventType::SpaceChild =>
|
||||
if pdu.state_key().is_some() {
|
||||
if let Some(_state_key) = pdu.state_key() {
|
||||
self.services
|
||||
.spaces
|
||||
.roomid_spacehierarchy_cache
|
||||
|
|
@ -359,8 +359,6 @@ where
|
|||
| _ => {},
|
||||
}
|
||||
|
||||
self.services.roles.on_pdu_appended(room_id, &pdu);
|
||||
|
||||
// CONCERN: If we receive events with a relation out-of-order, we never write
|
||||
// their relation / thread. We need some kind of way to trigger when we receive
|
||||
// this event, and potentially a way to rebuild the table entirely.
|
||||
|
|
|
|||
|
|
@ -97,17 +97,6 @@ pub async fn build_and_append_pdu(
|
|||
)));
|
||||
}
|
||||
}
|
||||
if *pdu.kind() == TimelineEventType::RoomPowerLevels {
|
||||
if let Ok(proposed) =
|
||||
pdu.get_content::<ruma::events::room::power_levels::RoomPowerLevelsEventContent>()
|
||||
{
|
||||
self.services
|
||||
.roles
|
||||
.validate_pl_change(&room_id, pdu.sender(), &proposed)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if *pdu.kind() == TimelineEventType::RoomCreate {
|
||||
trace!("Creating shortroomid for {room_id}");
|
||||
self.services
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ struct Services {
|
|||
threads: Dep<rooms::threads::Service>,
|
||||
search: Dep<rooms::search::Service>,
|
||||
spaces: Dep<rooms::spaces::Service>,
|
||||
roles: Dep<rooms::roles::Service>,
|
||||
event_handler: Dep<rooms::event_handler::Service>,
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +112,6 @@ impl crate::Service for Service {
|
|||
threads: args.depend::<rooms::threads::Service>("rooms::threads"),
|
||||
search: args.depend::<rooms::search::Service>("rooms::search"),
|
||||
spaces: args.depend::<rooms::spaces::Service>("rooms::spaces"),
|
||||
roles: args.depend::<rooms::roles::Service>("rooms::roles"),
|
||||
event_handler: args
|
||||
.depend::<rooms::event_handler::Service>("rooms::event_handler"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ async fn acquire_notary_result(&self, missing: &mut Batch, server_keys: ServerSi
|
|||
self.add_signing_keys(server_keys.clone()).await;
|
||||
|
||||
if let Some(key_ids) = missing.get_mut(server) {
|
||||
key_ids.retain(|key_id| !key_exists(&server_keys, key_id));
|
||||
key_ids.retain(|key_id| key_exists(&server_keys, key_id));
|
||||
if key_ids.is_empty() {
|
||||
missing.remove(server);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ use crate::{
|
|||
account_data, admin, announcements, antispam, appservice, client, config, emergency,
|
||||
federation, firstrun, globals, key_backups,
|
||||
manager::Manager,
|
||||
media, moderation, password_reset, presence, pusher, registration_tokens, resolver, rooms,
|
||||
sending, server_keys,
|
||||
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||
server_keys,
|
||||
service::{self, Args, Map, Service},
|
||||
sync, transactions, uiaa, users,
|
||||
};
|
||||
|
|
@ -27,7 +27,6 @@ pub struct Services {
|
|||
pub globals: Arc<globals::Service>,
|
||||
pub key_backups: Arc<key_backups::Service>,
|
||||
pub media: Arc<media::Service>,
|
||||
pub password_reset: Arc<password_reset::Service>,
|
||||
pub presence: Arc<presence::Service>,
|
||||
pub pusher: Arc<pusher::Service>,
|
||||
pub registration_tokens: Arc<registration_tokens::Service>,
|
||||
|
|
@ -82,7 +81,6 @@ impl Services {
|
|||
globals: build!(globals::Service),
|
||||
key_backups: build!(key_backups::Service),
|
||||
media: build!(media::Service),
|
||||
password_reset: build!(password_reset::Service),
|
||||
presence: build!(presence::Service),
|
||||
pusher: build!(pusher::Service),
|
||||
registration_tokens: build!(registration_tokens::Service),
|
||||
|
|
@ -96,7 +94,6 @@ impl Services {
|
|||
outlier: build!(rooms::outlier::Service),
|
||||
pdu_metadata: build!(rooms::pdu_metadata::Service),
|
||||
read_receipt: build!(rooms::read_receipt::Service),
|
||||
roles: build!(rooms::roles::Service),
|
||||
search: build!(rooms::search::Service),
|
||||
short: build!(rooms::short::Service),
|
||||
spaces: build!(rooms::spaces::Service),
|
||||
|
|
|
|||
|
|
@ -181,11 +181,20 @@ pub async fn try_auth(
|
|||
uiaainfo.completed.push(AuthType::Password);
|
||||
},
|
||||
| AuthData::ReCaptcha(r) => {
|
||||
let Some(ref private_site_key) = self.services.config.recaptcha_private_site_key
|
||||
else {
|
||||
if self.services.config.recaptcha_private_site_key.is_none() {
|
||||
return Err!(Request(Forbidden("ReCaptcha is not configured.")));
|
||||
};
|
||||
match recaptcha_verify::verify_v3(private_site_key, r.response.as_str(), None).await {
|
||||
}
|
||||
match recaptcha_verify::verify(
|
||||
self.services
|
||||
.config
|
||||
.recaptcha_private_site_key
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
r.response.as_str(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(()) => {
|
||||
uiaainfo.completed.push(AuthType::ReCaptcha);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,25 +20,12 @@ crate-type = [
|
|||
[dependencies]
|
||||
conduwuit-build-metadata.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
conduwuit-core.workspace = true
|
||||
async-trait.workspace = true
|
||||
askama.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
base64.workspace = true
|
||||
futures.workspace = true
|
||||
tracing.workspace = true
|
||||
rand.workspace = true
|
||||
ruma.workspace = true
|
||||
thiserror.workspace = true
|
||||
tower-http.workspace = true
|
||||
serde.workspace = true
|
||||
memory-serve = "2.1.0"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
tower-sec-fetch = { version = "0.1.2", features = ["tracing"] }
|
||||
|
||||
[build-dependencies]
|
||||
memory-serve = "2.1.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
[general]
|
||||
dirs = ["pages/templates"]
|
||||
|
|
@ -1 +0,0 @@
|
|||
fn main() { memory_serve::load_directory("./pages/resources"); }
|
||||
94
src/web/css/index.css
Normal file
94
src/web/css/index.css
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
:root {
|
||||
color-scheme: light;
|
||||
--font-stack: sans-serif;
|
||||
|
||||
--background-color: #fff;
|
||||
--text-color: #000;
|
||||
|
||||
--bg: oklch(0.76 0.0854 317.27);
|
||||
--panel-bg: oklch(0.91 0.042 317.27);
|
||||
|
||||
--name-lightness: 0.45;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
--text-color: #fff;
|
||||
--bg: oklch(0.15 0.042 317.27);
|
||||
--panel-bg: oklch(0.24 0.03 317.27);
|
||||
|
||||
--name-lightness: 0.8;
|
||||
}
|
||||
|
||||
--c1: oklch(0.44 0.177 353.06);
|
||||
--c2: oklch(0.59 0.158 150.88);
|
||||
|
||||
--normal-font-size: 1rem;
|
||||
--small-font-size: 0.8rem;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-stack);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--bg);
|
||||
background-image: linear-gradient(
|
||||
70deg,
|
||||
oklch(from var(--bg) l + 0.2 c h),
|
||||
oklch(from var(--bg) l - 0.2 c h)
|
||||
);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: min(clamp(24rem, 12rem + 40vw, 48rem), calc(100vw - 3rem));
|
||||
border-radius: 15px;
|
||||
background-color: var(--panel-bg);
|
||||
padding-inline: 1.5rem;
|
||||
padding-block: 1rem;
|
||||
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 24rem) {
|
||||
.panel {
|
||||
padding-inline: 0.25rem;
|
||||
width: calc(100vw - 0.5rem);
|
||||
border-radius: 0;
|
||||
margin-block-start: 0.2rem;
|
||||
}
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-inline: 0.25rem;
|
||||
height: max(fit-content, 2rem);
|
||||
}
|
||||
|
||||
.project-name {
|
||||
text-decoration: none;
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
oklch(from var(--c1) var(--name-lightness) c h),
|
||||
oklch(from var(--c2) var(--name-lightness) c h)
|
||||
);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
b {
|
||||
color: oklch(from var(--c2) var(--name-lightness) c h);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
}
|
||||
141
src/web/mod.rs
141
src/web/mod.rs
|
|
@ -1,113 +1,86 @@
|
|||
use std::any::Any;
|
||||
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::rejection::{FormRejection, QueryRejection},
|
||||
http::{HeaderValue, StatusCode, header},
|
||||
extract::State,
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag};
|
||||
use conduwuit_service::state;
|
||||
use tower_http::{catch_panic::CatchPanicLayer, set_header::SetResponseHeaderLayer};
|
||||
use tower_sec_fetch::SecFetchLayer;
|
||||
|
||||
use crate::pages::TemplateContext;
|
||||
pub fn build() -> Router<state::State> {
|
||||
Router::<state::State>::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/_continuwuity/logo.svg", get(logo_handler))
|
||||
}
|
||||
|
||||
mod pages;
|
||||
async fn index_handler(
|
||||
State(services): State<state::State>,
|
||||
) -> Result<impl IntoResponse, WebError> {
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "index.html.j2")]
|
||||
struct Index<'a> {
|
||||
nonce: &'a str,
|
||||
server_name: &'a str,
|
||||
first_run: bool,
|
||||
}
|
||||
let nonce = rand::random::<u64>().to_string();
|
||||
|
||||
type State = state::State;
|
||||
let template = Index {
|
||||
nonce: &nonce,
|
||||
server_name: services.config.server_name.as_str(),
|
||||
first_run: services.firstrun.is_first_run(),
|
||||
};
|
||||
Ok((
|
||||
[(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
format!("default-src 'nonce-{nonce}'; img-src 'self';"),
|
||||
)],
|
||||
Html(template.render()?),
|
||||
))
|
||||
}
|
||||
|
||||
const CATASTROPHIC_FAILURE: &str = "cat-astrophic failure! we couldn't even render the error template. \
|
||||
please contact the team @ https://continuwuity.org";
|
||||
async fn logo_handler() -> impl IntoResponse {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "image/svg+xml")],
|
||||
include_str!("templates/logo.svg").to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum WebError {
|
||||
#[error("Failed to validate form body: {0}")]
|
||||
ValidationError(#[from] validator::ValidationErrors),
|
||||
#[error("{0}")]
|
||||
QueryRejection(#[from] QueryRejection),
|
||||
#[error("{0}")]
|
||||
FormRejection(#[from] FormRejection),
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
|
||||
#[error("This page does not exist.")]
|
||||
NotFound,
|
||||
|
||||
#[error("Failed to render template: {0}")]
|
||||
Render(#[from] askama::Error),
|
||||
#[error("{0}")]
|
||||
InternalError(#[from] conduwuit_core::Error),
|
||||
#[error("Request handler panicked! {0}")]
|
||||
Panic(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for WebError {
|
||||
fn into_response(self) -> Response {
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "error.html.j2")]
|
||||
struct Error {
|
||||
error: WebError,
|
||||
status: StatusCode,
|
||||
context: TemplateContext,
|
||||
struct Error<'a> {
|
||||
nonce: &'a str,
|
||||
err: WebError,
|
||||
}
|
||||
|
||||
let nonce = rand::random::<u64>().to_string();
|
||||
|
||||
let status = match &self {
|
||||
| Self::ValidationError(_)
|
||||
| Self::BadRequest(_)
|
||||
| Self::QueryRejection(_)
|
||||
| Self::FormRejection(_) => StatusCode::BAD_REQUEST,
|
||||
| Self::NotFound => StatusCode::NOT_FOUND,
|
||||
| _ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
| Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let template = Error {
|
||||
error: self,
|
||||
status,
|
||||
context: TemplateContext {
|
||||
// Statically set false to prevent error pages from being indexed.
|
||||
allow_indexing: false,
|
||||
},
|
||||
};
|
||||
|
||||
if let Ok(body) = template.render() {
|
||||
(status, Html(body)).into_response()
|
||||
let tmpl = Error { nonce: &nonce, err: self };
|
||||
if let Ok(body) = tmpl.render() {
|
||||
(
|
||||
status,
|
||||
[(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
format!("default-src 'none' 'nonce-{nonce}';"),
|
||||
)],
|
||||
Html(body),
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
(status, CATASTROPHIC_FAILURE).into_response()
|
||||
(status, "Something went wrong").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build() -> Router<state::State> {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use pages::*;
|
||||
|
||||
Router::new()
|
||||
.merge(index::build())
|
||||
.nest(
|
||||
"/_continuwuity/",
|
||||
Router::new()
|
||||
.merge(resources::build())
|
||||
.merge(password_reset::build())
|
||||
.merge(debug::build())
|
||||
.fallback(async || WebError::NotFound),
|
||||
)
|
||||
.layer(CatchPanicLayer::custom(|panic: Box<dyn Any + Send + 'static>| {
|
||||
let details = if let Some(s) = panic.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else if let Some(s) = panic.downcast_ref::<&str>() {
|
||||
(*s).to_owned()
|
||||
} else {
|
||||
"(opaque panic payload)".to_owned()
|
||||
};
|
||||
|
||||
WebError::Panic(details).into_response()
|
||||
}))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
HeaderValue::from_static("default-src 'self'; img-src 'self' data:;"),
|
||||
))
|
||||
.layer(SecFetchLayer::new(|policy| {
|
||||
policy.allow_safe_methods().reject_missing_metadata();
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
use askama::{Template, filters::HtmlSafe};
|
||||
use validator::ValidationErrors;
|
||||
|
||||
/// A reusable form component with field validation.
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/form.html.j2", print = "code")]
|
||||
pub(crate) struct Form<'a> {
|
||||
pub inputs: Vec<FormInput<'a>>,
|
||||
pub validation_errors: Option<ValidationErrors>,
|
||||
pub submit_label: &'a str,
|
||||
}
|
||||
|
||||
impl HtmlSafe for Form<'_> {}
|
||||
|
||||
/// An input element in a form component.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct FormInput<'a> {
|
||||
/// The field name of the input.
|
||||
pub id: &'static str,
|
||||
/// The `type` property of the input.
|
||||
pub input_type: &'a str,
|
||||
/// The contents of the input's label.
|
||||
pub label: &'a str,
|
||||
/// Whether the input is required. Defaults to `true`.
|
||||
pub required: bool,
|
||||
/// The autocomplete mode for the input. Defaults to `on`.
|
||||
pub autocomplete: &'a str,
|
||||
|
||||
// This is a hack to make the form! macro's support for client-only fields
|
||||
// work properly. Client-only fields are specified in the macro without a type and aren't
|
||||
// included in the POST body or as a field in the generated struct.
|
||||
// To keep the field from being included in the POST body, its `name` property needs not to
|
||||
// be set in the template. Because of limitations of macro_rules!'s repetition feature, this
|
||||
// field needs to exist to allow the template to check if the field is client-only.
|
||||
#[doc(hidden)]
|
||||
pub type_name: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Default for FormInput<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: "",
|
||||
input_type: "text",
|
||||
label: "",
|
||||
required: true,
|
||||
autocomplete: "",
|
||||
|
||||
type_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a deserializable struct which may be turned into a [`Form`]
|
||||
/// for inclusion in another template.
|
||||
#[macro_export]
|
||||
macro_rules! form {
|
||||
(
|
||||
$(#[$struct_meta:meta])*
|
||||
struct $struct_name:ident {
|
||||
$(
|
||||
$(#[$field_meta:meta])*
|
||||
$name:ident$(: $type:ty)? where { $($prop:ident: $value:expr),* }
|
||||
),*
|
||||
|
||||
submit: $submit_label:expr
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, serde::Deserialize, validator::Validate)]
|
||||
$(#[$struct_meta])*
|
||||
struct $struct_name {
|
||||
$(
|
||||
$(#[$field_meta])*
|
||||
$(pub $name: $type,)?
|
||||
)*
|
||||
}
|
||||
|
||||
impl $struct_name {
|
||||
/// Generate a [`Form`] which matches the shape of this struct.
|
||||
#[allow(clippy::needless_update)]
|
||||
fn build(validation_errors: Option<validator::ValidationErrors>) -> $crate::pages::components::form::Form<'static> {
|
||||
$crate::pages::components::form::Form {
|
||||
inputs: vec![
|
||||
$(
|
||||
$crate::pages::components::form::FormInput {
|
||||
id: stringify!($name),
|
||||
$(type_name: Some(stringify!($type)),)?
|
||||
$($prop: $value),*,
|
||||
..Default::default()
|
||||
},
|
||||
)*
|
||||
],
|
||||
validation_errors,
|
||||
submit_label: $submit_label,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
use askama::{Template, filters::HtmlSafe};
|
||||
use base64::Engine;
|
||||
use conduwuit_core::result::FlatOk;
|
||||
use conduwuit_service::Services;
|
||||
use ruma::UserId;
|
||||
|
||||
pub(super) mod form;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum AvatarType<'a> {
|
||||
Initial(char),
|
||||
Image(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/avatar.html.j2")]
|
||||
pub(super) struct Avatar<'a> {
|
||||
pub(super) avatar_type: AvatarType<'a>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for Avatar<'_> {}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/user_card.html.j2")]
|
||||
pub(super) struct UserCard<'a> {
|
||||
pub user_id: &'a UserId,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_src: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for UserCard<'_> {}
|
||||
|
||||
impl<'a> UserCard<'a> {
|
||||
pub(super) async fn for_local_user(services: &Services, user_id: &'a UserId) -> Self {
|
||||
let display_name = services.users.displayname(user_id).await.ok();
|
||||
|
||||
let avatar_src = async {
|
||||
let avatar_url = services.users.avatar_url(user_id).await.ok()?;
|
||||
let avatar_mxc = avatar_url.parts().ok()?;
|
||||
let file = services.media.get(&avatar_mxc).await.flat_ok()?;
|
||||
|
||||
Some(format!(
|
||||
"data:{};base64,{}",
|
||||
file.content_type
|
||||
.unwrap_or_else(|| "application/octet-stream".to_owned()),
|
||||
file.content
|
||||
.map(|content| base64::prelude::BASE64_STANDARD.encode(content))
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
.await;
|
||||
|
||||
Self { user_id, display_name, avatar_src }
|
||||
}
|
||||
|
||||
fn avatar(&'a self) -> Avatar<'a> {
|
||||
let avatar_type = if let Some(ref avatar_src) = self.avatar_src {
|
||||
AvatarType::Image(avatar_src)
|
||||
} else if let Some(initial) = self
|
||||
.display_name
|
||||
.as_ref()
|
||||
.and_then(|display_name| display_name.chars().next())
|
||||
{
|
||||
AvatarType::Initial(initial)
|
||||
} else {
|
||||
AvatarType::Initial(self.user_id.localpart().chars().next().unwrap())
|
||||
};
|
||||
|
||||
Avatar { avatar_type }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
use conduwuit_core::Error;
|
||||
|
||||
use crate::WebError;
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route("/_debug/panic", get(async || -> Infallible { panic!("Guru meditation error") }))
|
||||
.route(
|
||||
"/_debug/error",
|
||||
get(async || -> WebError {
|
||||
Error::Err(std::borrow::Cow::Borrowed("Guru meditation error")).into()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
use askama::Template;
|
||||
use axum::{Router, extract::State, response::IntoResponse, routing::get};
|
||||
|
||||
use crate::{WebError, template};
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/_continuwuity/", get(index_handler))
|
||||
}
|
||||
|
||||
async fn index_handler(
|
||||
State(services): State<crate::State>,
|
||||
) -> Result<impl IntoResponse, WebError> {
|
||||
template! {
|
||||
struct Index<'a> use "index.html.j2" {
|
||||
server_name: &'a str,
|
||||
first_run: bool
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Index::new(
|
||||
&services,
|
||||
services.globals.server_name().as_str(),
|
||||
services.firstrun.is_first_run(),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
mod components;
|
||||
pub(super) mod debug;
|
||||
pub(super) mod index;
|
||||
pub(super) mod password_reset;
|
||||
pub(super) mod resources;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TemplateContext {
|
||||
pub allow_indexing: bool,
|
||||
}
|
||||
|
||||
impl From<&crate::State> for TemplateContext {
|
||||
fn from(state: &crate::State) -> Self {
|
||||
Self {
|
||||
allow_indexing: state.config.allow_web_indexing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! template {
|
||||
(
|
||||
struct $name:ident $(<$lifetime:lifetime>)? use $path:literal {
|
||||
$($field_name:ident: $field_type:ty),*
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, askama::Template)]
|
||||
#[template(path = $path)]
|
||||
struct $name$(<$lifetime>)? {
|
||||
context: $crate::pages::TemplateContext,
|
||||
$($field_name: $field_type,)*
|
||||
}
|
||||
|
||||
impl$(<$lifetime>)? $name$(<$lifetime>)? {
|
||||
fn new(state: &$crate::State, $($field_name: $field_type,)*) -> Self {
|
||||
Self {
|
||||
context: state.into(),
|
||||
$($field_name,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(single_use_lifetimes)]
|
||||
impl$(<$lifetime>)? axum::response::IntoResponse for $name$(<$lifetime>)? {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self.render() {
|
||||
Ok(rendered) => axum::response::Html(rendered).into_response(),
|
||||
Err(err) => $crate::WebError::from(err).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use askama::Template;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{
|
||||
Query, State,
|
||||
rejection::{FormRejection, QueryRejection},
|
||||
},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
WebError, form,
|
||||
pages::components::{UserCard, form::Form},
|
||||
template,
|
||||
};
|
||||
|
||||
const INVALID_TOKEN_ERROR: &str = "Invalid reset token. Your reset link may have expired.";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PasswordResetQuery {
|
||||
token: String,
|
||||
}
|
||||
|
||||
template! {
|
||||
struct PasswordReset<'a> use "password_reset.html.j2" {
|
||||
user_card: UserCard<'a>,
|
||||
body: PasswordResetBody
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PasswordResetBody {
|
||||
Form(Form<'static>),
|
||||
Success,
|
||||
}
|
||||
|
||||
form! {
|
||||
struct PasswordResetForm {
|
||||
#[validate(length(min = 1, message = "Password cannot be empty"))]
|
||||
new_password: String where {
|
||||
input_type: "password",
|
||||
label: "New password",
|
||||
autocomplete: "new-password"
|
||||
},
|
||||
|
||||
#[validate(must_match(other = "new_password", message = "Passwords must match"))]
|
||||
confirm_new_password: String where {
|
||||
input_type: "password",
|
||||
label: "Confirm new password",
|
||||
autocomplete: "new-password"
|
||||
}
|
||||
|
||||
submit: "Reset Password"
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route("/account/reset_password", get(get_password_reset).post(post_password_reset))
|
||||
}
|
||||
|
||||
async fn password_reset_form(
|
||||
services: crate::State,
|
||||
query: PasswordResetQuery,
|
||||
reset_form: Form<'static>,
|
||||
) -> Result<impl IntoResponse, WebError> {
|
||||
let Some(token) = services.password_reset.check_token(&query.token).await else {
|
||||
return Err(WebError::BadRequest(INVALID_TOKEN_ERROR.to_owned()));
|
||||
};
|
||||
|
||||
let user_card = UserCard::for_local_user(&services, &token.info.user).await;
|
||||
|
||||
Ok(PasswordReset::new(&services, user_card, PasswordResetBody::Form(reset_form))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn get_password_reset(
|
||||
State(services): State<crate::State>,
|
||||
query: Result<Query<PasswordResetQuery>, QueryRejection>,
|
||||
) -> Result<impl IntoResponse, WebError> {
|
||||
let Query(query) = query?;
|
||||
|
||||
password_reset_form(services, query, PasswordResetForm::build(None)).await
|
||||
}
|
||||
|
||||
async fn post_password_reset(
|
||||
State(services): State<crate::State>,
|
||||
query: Result<Query<PasswordResetQuery>, QueryRejection>,
|
||||
form: Result<axum::Form<PasswordResetForm>, FormRejection>,
|
||||
) -> Result<Response, WebError> {
|
||||
let Query(query) = query?;
|
||||
let axum::Form(form) = form?;
|
||||
|
||||
match form.validate() {
|
||||
| Ok(()) => {
|
||||
let Some(token) = services.password_reset.check_token(&query.token).await else {
|
||||
return Err(WebError::BadRequest(INVALID_TOKEN_ERROR.to_owned()));
|
||||
};
|
||||
let user_id = token.info.user.clone();
|
||||
|
||||
services
|
||||
.password_reset
|
||||
.consume_token(token, &form.new_password)
|
||||
.await?;
|
||||
|
||||
let user_card = UserCard::for_local_user(&services, &user_id).await;
|
||||
Ok(PasswordReset::new(&services, user_card, PasswordResetBody::Success)
|
||||
.into_response())
|
||||
},
|
||||
| Err(err) => Ok((
|
||||
StatusCode::BAD_REQUEST,
|
||||
password_reset_form(services, query, PasswordResetForm::build(Some(err))).await,
|
||||
)
|
||||
.into_response()),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
use axum::Router;
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new().nest(
|
||||
"/resources/",
|
||||
#[allow(unused_qualifications)]
|
||||
memory_serve::load!().index_file(None).into_router(),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
:root {
|
||||
color-scheme: light;
|
||||
--font-stack: sans-serif;
|
||||
|
||||
--background-color: #fff;
|
||||
--text-color: #000;
|
||||
--secondary: #666;
|
||||
--bg: oklch(0.76 0.0854 317.27);
|
||||
--panel-bg: oklch(0.91 0.042 317.27);
|
||||
--c1: oklch(0.44 0.177 353.06);
|
||||
--c2: oklch(0.59 0.158 150.88);
|
||||
|
||||
--name-lightness: 0.45;
|
||||
--background-lightness: 0.9;
|
||||
|
||||
--background-gradient:
|
||||
radial-gradient(42.12% 56.13% at 100% 0%, oklch(from var(--c2) var(--background-lightness) c h) 0%, #fff0 100%),
|
||||
radial-gradient(42.01% 79.63% at 52.86% 0%, #d9ff5333 0%, #fff0 100%),
|
||||
radial-gradient(79.67% 58.09% at 0% 0%, oklch(from var(--c1) var(--background-lightness) c h) 0%, #fff0 100%);
|
||||
|
||||
--normal-font-size: 1rem;
|
||||
--small-font-size: 0.8rem;
|
||||
|
||||
--border-radius-sm: 5px;
|
||||
--border-radius-lg: 15px;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
--text-color: #fff;
|
||||
--secondary: #888;
|
||||
--bg: oklch(0.15 0.042 317.27);
|
||||
--panel-bg: oklch(0.24 0.03 317.27);
|
||||
|
||||
--name-lightness: 0.8;
|
||||
--background-lightness: 0.2;
|
||||
|
||||
--background-gradient:
|
||||
radial-gradient(
|
||||
42.12% 56.13% at 100% 0%,
|
||||
oklch(from var(--c2) var(--background-lightness) c h) 0%,
|
||||
#12121200 100%
|
||||
),
|
||||
radial-gradient(55.81% 87.78% at 48.37% 0%, #000 0%, #12121200 89.55%),
|
||||
radial-gradient(
|
||||
122.65% 88.24% at 0% 0%,
|
||||
oklch(from var(--c1) var(--background-lightness) c h) 0%,
|
||||
#12121200 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
place-items: center;
|
||||
min-height: 100vh;
|
||||
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-stack);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--bg);
|
||||
background-image: var(--background-gradient);
|
||||
font-size: var(--normal-font-size);
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-inline: 0.25rem;
|
||||
height: max(fit-content, 2rem);
|
||||
|
||||
.logo {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
em {
|
||||
color: oklch(from var(--c2) var(--name-lightness) c h);
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
small.error {
|
||||
display: block;
|
||||
color: red;
|
||||
font-size: small;
|
||||
font-style: italic;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
--preferred-width: 12rem + 40dvw;
|
||||
--maximum-width: 48rem;
|
||||
|
||||
width: min(clamp(24rem, var(--preferred-width), var(--maximum-width)), calc(100dvw - 3rem));
|
||||
border-radius: var(--border-radius-lg);
|
||||
background-color: var(--panel-bg);
|
||||
padding-inline: 1.5rem;
|
||||
padding-block: 1rem;
|
||||
box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
|
||||
|
||||
&.narrow {
|
||||
--preferred-width: 12rem + 20dvw;
|
||||
--maximum-width: 36rem;
|
||||
|
||||
input, button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input, button {
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
|
||||
border: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
input {
|
||||
border: 2px solid var(--secondary);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--c1);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--c1);
|
||||
transition: opacity .2s;
|
||||
|
||||
&:enabled:hover {
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.67em;
|
||||
}
|
||||
|
||||
@media (max-width: 425px) {
|
||||
main {
|
||||
padding-block-start: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
input, button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
.avatar {
|
||||
--avatar-size: 56px;
|
||||
|
||||
display: inline-block;
|
||||
aspect-ratio: 1 / 1;
|
||||
inline-size: var(--avatar-size);
|
||||
border-radius: 50%;
|
||||
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: calc(var(--avatar-size) * 0.5);
|
||||
font-weight: 700;
|
||||
line-height: calc(var(--avatar-size) - 2px);
|
||||
|
||||
color: oklch(from var(--c1) calc(l + 0.2) c h);
|
||||
background-color: var(--c1);
|
||||
}
|
||||
|
||||
.user-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
background-color: oklch(from var(--panel-bg) calc(l - 0.05) c h);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 16px;
|
||||
|
||||
.info {
|
||||
flex: 1 1;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
&.display-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.k10y {
|
||||
font-family: monospace;
|
||||
font-size: x-small;
|
||||
font-weight: 700;
|
||||
transform: translate(1rem, 1.6rem);
|
||||
color: var(--secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.project-name {
|
||||
text-decoration: none;
|
||||
background: linear-gradient(
|
||||
130deg,
|
||||
oklch(from var(--c1) var(--name-lightness) c h),
|
||||
oklch(from var(--c2) var(--name-lightness) c h)
|
||||
);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="447.99823"
|
||||
height="447.99823"
|
||||
viewBox="0 0 447.99823 447.99823"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer1"
|
||||
transform="translate(-32.000893,-32.000893)"><circle
|
||||
style="fill:#9b4bd4;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path1"
|
||||
cy="256"
|
||||
cx="256"
|
||||
r="176" /><path
|
||||
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 41,174 69,36 C 135,126 175,102 226,94 l -12,31 62,-44 -69,-44 15,30 C 128,69 84,109 41,172 Z"
|
||||
id="path7" /><path
|
||||
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 338,41 -36,69 c 84,25 108,65 116,116 l -31,-12 44,62 44,-69 -30,15 C 443,128 403,84 340,41 Z"
|
||||
id="path6" /><path
|
||||
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 471,338 -69,-36 c -25,84 -65,108 -116,116 l 12,-31 -62,44 69,44 -15,-30 c 94,-2 138,-42 181,-105 z"
|
||||
id="path8" /><path
|
||||
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 174,471 36,-69 C 126,377 102,337 94,286 l 31,12 -44,-62 -44,69 30,-15 c 2,94 42,138 105,181 z"
|
||||
id="path9" /><g
|
||||
id="g15"
|
||||
transform="translate(-5.4157688e-4)"><path
|
||||
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 155.45977,224.65379 c -7.25909,13.49567 -7.25909,26.09161 -6.35171,39.58729 0.90737,11.69626 12.7034,24.29222 24.49943,26.09164 21.77727,3.59884 28.12898,-20.69338 28.12898,-20.69338 0,0 4.53693,-15.29508 5.4443,-40.48699"
|
||||
id="path11" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 218.96706,278.05399 c 3.00446,17.12023 7.52704,24.88918 19.22704,28.48918 9,2.7 22.5,-4.5 22.5,-16.2 0.9,21.6 17.1,17.1 19.8,17.1 11.7,-1.8 18.9,-14.4 16.2,-30.6"
|
||||
id="path12" /><path
|
||||
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 305.6941,230.94317 c 1.8,27 6.3,40.5 6.3,40.5 8.1,27 28.8,19.8 28.8,19.8 18.9,-7.2 22.5,-24.3 22.5,-30.6 0,-25.2 -6.3,-35.1 -6.3,-35.1"
|
||||
id="path13" /></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
{% match avatar_type %}
|
||||
{% when AvatarType::Initial with (initial) %}
|
||||
<span class="avatar" role="img">{{ initial }}</span>
|
||||
{% when AvatarType::Image with (src) %}
|
||||
<img class="avatar" src="{{ src }}">
|
||||
{% endmatch %}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<form method="post">
|
||||
{% let validation_errors = validation_errors.clone().unwrap_or_default() %}
|
||||
{% let field_errors = validation_errors.field_errors() %}
|
||||
{% for input in inputs %}
|
||||
<p>
|
||||
<label for="{{ input.id }}">{{ input.label }}</label>
|
||||
{% let name = std::borrow::Cow::from(*input.id) %}
|
||||
{% if let Some(errors) = field_errors.get(name) %}
|
||||
{% for error in errors %}
|
||||
<small class="error">
|
||||
{% if let Some(message) = error.message %}
|
||||
{{ message }}
|
||||
{% else %}
|
||||
Mysterious validation error <code>{{ error.code }}</code>!
|
||||
{% endif %}
|
||||
</small>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<input
|
||||
type="{{ input.input_type }}"
|
||||
id="{{ input.id }}"
|
||||
autocomplete="{{ input.autocomplete }}"
|
||||
{% if input.type_name.is_some() %}name="{{ input.id }}"{% endif %}
|
||||
{% if input.required %}required{% endif %}
|
||||
>
|
||||
</p>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<div class="user-card">
|
||||
{{ avatar() }}
|
||||
<div class="info">
|
||||
{% if let Some(display_name) = display_name %}
|
||||
<p class="display-name">{{ display_name }}</p>
|
||||
{% endif %}
|
||||
<p class="user_id">{{ user_id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block head -%}
|
||||
<link rel="stylesheet" href="/_continuwuity/resources/error.css">
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block title -%}
|
||||
🐈 Request Error
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<pre class="k10y" aria-hidden>
|
||||
/> フ
|
||||
| _ _|
|
||||
/` ミ_xノ
|
||||
/ |
|
||||
/ ヽ ノ
|
||||
│ | | |
|
||||
/ ̄| | | |
|
||||
| ( ̄ヽ__ヽ_)__)
|
||||
\二つ
|
||||
</pre>
|
||||
<div class="panel">
|
||||
<h1>
|
||||
{% if status == StatusCode::NOT_FOUND %}
|
||||
Not found
|
||||
{% else if status == StatusCode::INTERNAL_SERVER_ERROR %}
|
||||
Internal server error
|
||||
{% else %}
|
||||
Bad request
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
{% if status == StatusCode::INTERNAL_SERVER_ERROR %}
|
||||
<p>Please <a href="https://forgejo.ellis.link/continuwuation/continuwuity/issues/new">submit a bug report</a> 🥺</p>
|
||||
{% endif %}
|
||||
|
||||
<pre><code>{{ error }}</code></pre>
|
||||
</div>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block title -%}
|
||||
Reset Password
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<div class="panel narrow">
|
||||
<h1>Reset Password</h1>
|
||||
{{ user_card }}
|
||||
{% match body %}
|
||||
{% when PasswordResetBody::Form(reset_form) %}
|
||||
{{ reset_form }}
|
||||
{% when PasswordResetBody::Success %}
|
||||
<p>Your password has been reset successfully.</p>
|
||||
{% endmatch %}
|
||||
</div>
|
||||
{%- endblock -%}
|
||||
|
|
@ -5,24 +5,23 @@
|
|||
<meta charset="UTF-8" />
|
||||
<title>{% block title %}Continuwuity{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{%- if !context.allow_indexing %}
|
||||
<meta name="robots" content="noindex" />
|
||||
{%- endif %}
|
||||
|
||||
<link rel="icon" href="/_continuwuity/resources/logo.svg">
|
||||
<link rel="stylesheet" href="/_continuwuity/resources/common.css">
|
||||
<link rel="stylesheet" href="/_continuwuity/resources/components.css">
|
||||
{% block head %}{% endblock %}
|
||||
<link rel="icon" href="/_continuwuity/logo.svg">
|
||||
<style type="text/css" nonce="{{ nonce }}">
|
||||
/*<![CDATA[*/
|
||||
{{ include_str !("css/index.css") | safe }}
|
||||
/*]]>*/
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>{%~ block content %}{% endblock ~%}</main>
|
||||
{%~ block footer ~%}
|
||||
<footer>
|
||||
<img class="logo" src="/_continuwuity/resources/logo.svg">
|
||||
<img class="logo" src="/_continuwuity/logo.svg">
|
||||
<p>Powered by <a href="https://continuwuity.org">Continuwuity</a> {{ env!("CARGO_PKG_VERSION") }}
|
||||
{%~ if let Some(version_info) = conduwuit_build_metadata::version_tag() ~%}
|
||||
{%~ if let Some(url) = conduwuit_build_metadata::GIT_REMOTE_COMMIT_URL.or(conduwuit_build_metadata::GIT_REMOTE_WEB_URL) ~%}
|
||||
{%~ if let Some(version_info) = self::version_tag() ~%}
|
||||
{%~ if let Some(url) = GIT_REMOTE_COMMIT_URL.or(GIT_REMOTE_WEB_URL) ~%}
|
||||
(<a href="{{ url }}">{{ version_info }}</a>)
|
||||
{%~ else ~%}
|
||||
({{ version_info }})
|
||||
20
src/web/templates/error.html.j2
Normal file
20
src/web/templates/error.html.j2
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block title -%}
|
||||
Server Error
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<h1>
|
||||
{%- match err -%}
|
||||
{% else -%} 500: Internal Server Error
|
||||
{%- endmatch -%}
|
||||
</h1>
|
||||
|
||||
{%- match err -%}
|
||||
{% when WebError::Render(err) -%}
|
||||
<pre>{{ err }}</pre>
|
||||
{% else -%} <p>An error occurred</p>
|
||||
{%- endmatch -%}
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block head -%}
|
||||
<link rel="stylesheet" href="/_continuwuity/resources/index.css">
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<div class="panel">
|
||||
<h1>
|
||||
|
|
@ -11,7 +6,7 @@
|
|||
</h1>
|
||||
<p>Continuwuity is successfully installed and working.</p>
|
||||
{%- if first_run %}
|
||||
<p>To get started, <em>check the server logs</em> for instructions on how to create the first account.</p>
|
||||
<p>To get started, <b>check the server logs</b> for instructions on how to create the first account.</p>
|
||||
<p>For support, take a look at the <a href="https://continuwuity.org/introduction">documentation</a> or join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a>.</p>
|
||||
{%- else %}
|
||||
<p>To get started, <a href="https://matrix.org/ecosystem/clients">choose a client</a> and connect to <code>{{ server_name }}</code>.</p>
|
||||
1
src/web/templates/logo.svg
Symbolic link
1
src/web/templates/logo.svg
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../docs/public/assets/logo.svg
|
||||
Loading…
Add table
Reference in a new issue