mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49ce6b4072 | |||
| f6ad1787a0 | |||
| 81ff8f1bd3 | |||
| 04980b3ee7 |
@@ -64,7 +64,6 @@ runs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=${{ inputs.tag_suffix }},onlatest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}},prefix=v
|
||||
@@ -73,6 +72,7 @@ runs:
|
||||
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},
|
||||
type=ref,event=pr
|
||||
type=sha,format=short
|
||||
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }},priority=1100
|
||||
images: ${{ inputs.images }}
|
||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||
env:
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
name: 'New pull request'
|
||||
about: 'Open a new pull request to contribute to continuwuity'
|
||||
ref: 'main'
|
||||
---
|
||||
|
||||
<!--
|
||||
In order to help reviewers know what your pull request does at a glance, you should ensure that
|
||||
|
||||
1. Your PR title is a short, single sentence describing what you changed
|
||||
2. You have described in more detail what you have changed, why you have changed it, what the
|
||||
intended effect is, and why you think this will be beneficial to the project.
|
||||
|
||||
If you have made any potentially strange/questionable design choices, but didn't feel they'd benefit
|
||||
from code comments, please don't mention them here - after opening your pull request,
|
||||
go to "files changed", and click on the "+" symbol in the line number gutter,
|
||||
and attach comments to the lines that you think would benefit from some clarification.
|
||||
-->
|
||||
|
||||
This pull request...
|
||||
|
||||
<!-- Example:
|
||||
This pull request allows us to warp through time and space ten times faster than before by
|
||||
double-inverting the warp drive with hyperheated jump fluid, both making the drive faster and more
|
||||
efficient. This resolves the common issue where we have to wait more than 10 milliseconds to
|
||||
engage, use, and disengage the warp drive when travelling between galaxies.
|
||||
-->
|
||||
|
||||
<!-- Closes: #... -->
|
||||
<!-- Fixes: #... -->
|
||||
<!-- Uncomment the above line(s) if your pull request fixes an issue or closes another pull request
|
||||
by superseding it. Replace `#...` with the issue/pr number, such as `#123`. -->
|
||||
|
||||
**Pull request checklist:**
|
||||
|
||||
<!-- You need to complete these before your PR can be considered.
|
||||
If you aren't sure about some, feel free to ask for clarification in #dev:continuwuity.org. -->
|
||||
- [ ] This pull request targets the `main` branch, and the branch is named something other than
|
||||
`main`.
|
||||
- [ ] I have written an appropriate pull request title and my description is clear.
|
||||
- [ ] I understand I am responsible for the contents of this pull request.
|
||||
- I have followed the [contributing guidelines][c1]:
|
||||
- [ ] My contribution follows the [code style][c2], if applicable.
|
||||
- [ ] I ran [pre-commit checks][c1pc] before opening/drafting this pull request.
|
||||
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
|
||||
myself, if applicable. This includes ensuring code compiles.
|
||||
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
|
||||
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
|
||||
|
||||
<!--
|
||||
Notes on these requirements:
|
||||
|
||||
- While not required, we encourage you to sign your commits with GPG or SSH to attest the
|
||||
authenticity of your changes.
|
||||
- While we allow LLM-assisted contributions, we do not appreciate contributions that are
|
||||
low quality, which is typical of machine-generated contributions that have not had a lot of love
|
||||
and care from a human. Please do not open a PR if all you have done is asked ChatGPT to tidy up
|
||||
the codebase with a +-100,000 diff.
|
||||
- In the case of code style violations, reviewers may leave review comments/change requests
|
||||
indicating what the ideal change would look like. For example, a reviewer may suggest you lower
|
||||
a log level, or use `match` instead of `if/else` etc.
|
||||
- In the case of code style violations, pre-commit check failures, minor things like typos/spelling
|
||||
errors, and in some cases commit format violations, reviewers may modify your branch directly,
|
||||
typically by making changes and adding a commit. Particularly in the latter case, a reviewer may
|
||||
rebase your commits to squash "spammy" ones (like "fix", "fix", "actually fix"), and reword
|
||||
commit messages that don't satisfy the format.
|
||||
- Pull requests MUST pass the `Checks` CI workflows to be capable of being merged. This can only be
|
||||
bypassed in exceptional circumstances.
|
||||
If your CI flakes, let us know in matrix:r/dev:continuwuity.org.
|
||||
- Pull requests have to be based on the latest `main` commit before being merged. If the main branch
|
||||
changes while you're making your changes, you should make sure you rebase on main before
|
||||
opening a PR. Your branch will be rebased on main before it is merged if it has fallen behind.
|
||||
- We typically only do fast-forward merges, so your entire commit log will be included. Once in
|
||||
main, it's difficult to get out cleanly, so put on your best dress, smile for the cameras!
|
||||
-->
|
||||
|
||||
[c1]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md
|
||||
[c2]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/docs/development/code_style.mdx
|
||||
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
|
||||
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
|
||||
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
|
||||
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
@@ -59,9 +59,10 @@ jobs:
|
||||
# Aggressive GC since cache restores don't increment counter
|
||||
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Rust
|
||||
- name: Setup Rust nightly
|
||||
uses: ./.forgejo/actions/setup-rust
|
||||
with:
|
||||
rust-version: nightly
|
||||
github-token: ${{ secrets.GH_PUBLIC_RO }}
|
||||
|
||||
- name: Get package version and component
|
||||
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.41.0
|
||||
rev: v1.40.0
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: https://github.com/crate-ci/committed
|
||||
rev: v1.1.9
|
||||
rev: v1.1.8
|
||||
hooks:
|
||||
- id: committed
|
||||
|
||||
|
||||
@@ -24,4 +24,3 @@ extend-ignore-re = [
|
||||
"continuwuity" = "continuwuity"
|
||||
"continuwity" = "continuwuity"
|
||||
"execuse" = "execuse"
|
||||
"oltp" = "OTLP"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Continuwuity 0.5.0 (2025-12-30)
|
||||
|
||||
**This release contains a CRITICAL vulnerability patch, and you must update as soon as possible**
|
||||
|
||||
## Features
|
||||
|
||||
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). (#1251)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Don't allow admin room upgrades, as this can break the admin room (@timedout) (#1245)
|
||||
- Fix invalid creators in power levels during upgrade to v12 (@timedout) (#1245)
|
||||
Generated
+23
-25
@@ -940,7 +940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_admin",
|
||||
@@ -972,7 +972,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
@@ -994,7 +994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.7.9",
|
||||
@@ -1027,14 +1027,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_build_metadata"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"built",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_core"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
@@ -1095,7 +1095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_database"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"conduwuit_core",
|
||||
@@ -1114,7 +1114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_macros"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
@@ -1124,7 +1124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"axum-client-ip",
|
||||
@@ -1159,7 +1159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_service"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -1200,7 +1200,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_web"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"axum 0.7.9",
|
||||
@@ -3305,8 +3305,6 @@ dependencies = [
|
||||
"prost",
|
||||
"reqwest",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -4065,7 +4063,7 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
@@ -4085,7 +4083,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.10.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4097,7 +4095,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.18.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4120,7 +4118,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
@@ -4152,7 +4150,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.28.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
@@ -4177,7 +4175,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4199,7 +4197,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4208,7 +4206,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4218,7 +4216,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
@@ -4233,7 +4231,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4245,7 +4243,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=3a6f29fda2c3ccf07282c746dc0e2021defc50bb#3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
@@ -6206,7 +6204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
@@ -6215,7 +6213,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask-generate-commands"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"clap-markdown",
|
||||
"clap_builder",
|
||||
|
||||
+64
-64
@@ -21,7 +21,7 @@ license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
rust-version = "1.86.0"
|
||||
version = "0.5.1"
|
||||
version = "0.5.0"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
@@ -33,11 +33,11 @@ features = ["serde"]
|
||||
[workspace.dependencies.smallvec]
|
||||
version = "1.14.0"
|
||||
features = [
|
||||
"const_generics",
|
||||
"const_new",
|
||||
"serde",
|
||||
"union",
|
||||
"write",
|
||||
"const_generics",
|
||||
"const_new",
|
||||
"serde",
|
||||
"union",
|
||||
"write",
|
||||
]
|
||||
|
||||
[workspace.dependencies.smallstr]
|
||||
@@ -96,13 +96,13 @@ version = "1.11.1"
|
||||
version = "0.7.9"
|
||||
default-features = false
|
||||
features = [
|
||||
"form",
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"matched-path",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"form",
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"matched-path",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
@@ -149,10 +149,10 @@ features = ["aws_lc_rs"]
|
||||
version = "0.12.15"
|
||||
default-features = false
|
||||
features = [
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"hickory-dns",
|
||||
"http2",
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"hickory-dns",
|
||||
"http2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
@@ -188,18 +188,18 @@ default-features = false
|
||||
version = "0.25.5"
|
||||
default-features = false
|
||||
features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
]
|
||||
|
||||
[workspace.dependencies.blurhash]
|
||||
version = "0.2.3"
|
||||
default-features = false
|
||||
features = [
|
||||
"fast-linear-to-srgb",
|
||||
"image",
|
||||
"fast-linear-to-srgb",
|
||||
"image",
|
||||
]
|
||||
|
||||
# logging
|
||||
@@ -229,13 +229,13 @@ default-features = false
|
||||
version = "4.5.35"
|
||||
default-features = false
|
||||
features = [
|
||||
"derive",
|
||||
"env",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"string",
|
||||
"usage",
|
||||
"derive",
|
||||
"env",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"string",
|
||||
"usage",
|
||||
]
|
||||
|
||||
[workspace.dependencies.futures]
|
||||
@@ -247,15 +247,15 @@ features = ["std", "async-await"]
|
||||
version = "1.44.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"fs",
|
||||
"net",
|
||||
"macros",
|
||||
"sync",
|
||||
"signal",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"tracing",
|
||||
"fs",
|
||||
"net",
|
||||
"macros",
|
||||
"sync",
|
||||
"signal",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[workspace.dependencies.tokio-metrics]
|
||||
@@ -280,18 +280,18 @@ default-features = false
|
||||
version = "1.6.0"
|
||||
default-features = false
|
||||
features = [
|
||||
"server",
|
||||
"http1",
|
||||
"http2",
|
||||
"server",
|
||||
"http1",
|
||||
"http2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.hyper-util]
|
||||
version = "=0.1.17"
|
||||
default-features = false
|
||||
features = [
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
"tokio",
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
# to support multiple variations of setting a config option
|
||||
@@ -310,9 +310,9 @@ features = ["env", "toml"]
|
||||
version = "0.25.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"serde",
|
||||
"system-config",
|
||||
"tokio",
|
||||
"serde",
|
||||
"system-config",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
# Used for conduwuit::Error type
|
||||
@@ -351,7 +351,7 @@ version = "0.1.2"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9"
|
||||
rev = "3a6f29fda2c3ccf07282c746dc0e2021defc50bb"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -381,13 +381,13 @@ features = [
|
||||
"unstable-msc4095",
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4186",
|
||||
"unstable-msc4203", # sending to-device events to appservices
|
||||
"unstable-msc4210", # remove legacy mentions
|
||||
"unstable-extensible-events",
|
||||
"unstable-pdu",
|
||||
"unstable-msc4155"
|
||||
"unstable-msc4155"
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
@@ -395,11 +395,11 @@ git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
|
||||
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
default-features = false
|
||||
features = [
|
||||
"multi-threaded-cf",
|
||||
"mt_static",
|
||||
"lz4",
|
||||
"zstd",
|
||||
"bzip2",
|
||||
"multi-threaded-cf",
|
||||
"mt_static",
|
||||
"lz4",
|
||||
"zstd",
|
||||
"bzip2",
|
||||
]
|
||||
|
||||
[workspace.dependencies.sha2]
|
||||
@@ -426,7 +426,7 @@ features = ["rt-tokio"]
|
||||
|
||||
[workspace.dependencies.opentelemetry-otlp]
|
||||
version = "0.31.0"
|
||||
features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
|
||||
features = ["http", "trace", "logs", "metrics"]
|
||||
|
||||
|
||||
|
||||
@@ -458,16 +458,16 @@ git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
]
|
||||
[workspace.dependencies.tikv-jemallocator]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
"background_threads_runtime_support",
|
||||
"unprefixed_malloc_on_supported_platforms",
|
||||
]
|
||||
[workspace.dependencies.tikv-jemalloc-ctl]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
@@ -491,9 +491,9 @@ default-features = false
|
||||
version = "0.1.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"static",
|
||||
"gcc",
|
||||
"light",
|
||||
"static",
|
||||
"gcc",
|
||||
"light",
|
||||
]
|
||||
|
||||
[workspace.dependencies.rustyline-async]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
The `console` feature is now enabled by default, allowing the server console to be used for running admin commands directly.
|
||||
@@ -1 +0,0 @@
|
||||
Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console.
|
||||
@@ -1 +0,0 @@
|
||||
Implemented a configuration defined admin list independent of the admin room. (@Terryiscool160).
|
||||
@@ -1 +0,0 @@
|
||||
Fixed unreliable room summary fetching and improved error messages. Contributed by @nex.
|
||||
+12
-29
@@ -26,8 +26,8 @@
|
||||
# Also see the `[global.well_known]` config section at the very bottom.
|
||||
#
|
||||
# Examples of delegation:
|
||||
# - https://continuwuity.org/.well-known/matrix/server
|
||||
# - https://continuwuity.org/.well-known/matrix/client
|
||||
# - https://puppygock.gay/.well-known/matrix/server
|
||||
# - https://puppygock.gay/.well-known/matrix/client
|
||||
#
|
||||
# YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
||||
# WIPE.
|
||||
@@ -608,11 +608,6 @@
|
||||
#
|
||||
#otlp_filter = "info"
|
||||
|
||||
# Protocol to use for OTLP tracing export. Options are "http" or "grpc".
|
||||
# The HTTP protocol uses port 4318 by default, while gRPC uses port 4317.
|
||||
#
|
||||
#otlp_protocol = "http"
|
||||
|
||||
# If the 'perf_measurements' compile-time feature is enabled, enables
|
||||
# collecting folded stack trace profile of tracing spans using
|
||||
# tracing_flame. The resulting profile can be visualized with inferno[1],
|
||||
@@ -1520,16 +1515,11 @@
|
||||
#
|
||||
#block_non_admin_invites = false
|
||||
|
||||
# Enable or disable making requests to MSC4284 Policy Servers.
|
||||
# It is recommended you keep this enabled unless you experience frequent
|
||||
# connectivity issues, such as in a restricted networking environment.
|
||||
# This item is undocumented. Please contribute documentation for it.
|
||||
#
|
||||
#enable_msc4284_policy_servers = true
|
||||
|
||||
# Enable running locally generated events through configured MSC4284
|
||||
# policy servers. You may wish to disable this if your server is
|
||||
# single-user for a slight speed benefit in some rooms, but otherwise
|
||||
# should leave it enabled.
|
||||
# This item is undocumented. Please contribute documentation for it.
|
||||
#
|
||||
#policy_server_check_own_events = true
|
||||
|
||||
@@ -1538,7 +1528,7 @@
|
||||
# a normal continuwuity admin command. The reply will be publicly visible
|
||||
# to the room, originating from the sender.
|
||||
#
|
||||
# example: \\!admin debug ping continuwuity.org
|
||||
# example: \\!admin debug ping puppygock.gay
|
||||
#
|
||||
#admin_escape_commands = true
|
||||
|
||||
@@ -1556,8 +1546,7 @@
|
||||
# For example: `./continuwuity --execute "server admin-notice continuwuity
|
||||
# has started up at $(date)"`
|
||||
#
|
||||
# example: admin_execute = ["debug ping continuwuity.org", "debug echo
|
||||
# hi"]`
|
||||
# example: admin_execute = ["debug ping puppygock.gay", "debug echo hi"]`
|
||||
#
|
||||
#admin_execute = []
|
||||
|
||||
@@ -1590,18 +1579,6 @@
|
||||
#
|
||||
#admin_room_tag = "m.server_notice"
|
||||
|
||||
# A list of Matrix IDs that are qualified as server admins.
|
||||
#
|
||||
# Any Matrix IDs within this list are regarded as an admin
|
||||
# regardless of whether they are in the admin room or not
|
||||
#
|
||||
#admins_list = []
|
||||
|
||||
# Defines whether those within the admin room are added to the
|
||||
# admins_list.
|
||||
#
|
||||
#admins_from_room = true
|
||||
|
||||
# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||
# This is NOT enabled by default.
|
||||
#
|
||||
@@ -1757,6 +1734,12 @@
|
||||
#
|
||||
#ldap = false
|
||||
|
||||
# Configuration for protocol experiments that enable experimental
|
||||
# features. Each one is associated with a matrix spec proposal, a list of
|
||||
# which are published at https://spec.matrix.org/proposals/
|
||||
#
|
||||
#experiments = false
|
||||
|
||||
[global.tls]
|
||||
|
||||
# Path to a valid TLS certificate file.
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ EOF
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.16.6
|
||||
ENV BINSTALL_VERSION=1.16.2
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.16.6
|
||||
ENV BINSTALL_VERSION=1.16.2
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -114,10 +114,6 @@ services:
|
||||
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
|
||||
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: "/etc/traefik/acme/acme.json"
|
||||
|
||||
# Since Traefik 3.6.3, paths with certain "encoded characters" are now blocked by default; we need a couple, or else things *will* break
|
||||
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: true
|
||||
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: true
|
||||
|
||||
TRAEFIK_PROVIDERS_DOCKER: true
|
||||
TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: "unix:///var/run/docker.sock"
|
||||
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: false
|
||||
|
||||
@@ -149,12 +149,11 @@ of it, especially when the CI completed successfully and everything so it
|
||||
*looks* done.
|
||||
|
||||
Before submitting a pull request, please ensure:
|
||||
1. Your code passes all CI checks (formatting, linting, typo detection, etc.). Run pre-commit for this.
|
||||
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
|
||||
2. Your code follows the [code style guide](./code_style)
|
||||
3. Your commit messages follow the conventional commits format
|
||||
4. Tests are added for new functionality
|
||||
5. Documentation is updated if needed
|
||||
6. You have written a [news fragment](#writing-news-fragments) for your changes
|
||||
|
||||
Direct all PRs/MRs to the `main` branch.
|
||||
|
||||
@@ -172,32 +171,3 @@ continuwuity Matrix rooms for Code of Conduct violations.
|
||||
[sytest]: https://github.com/matrix-org/sytest/
|
||||
[mdbook]: https://rust-lang.github.io/mdBook/
|
||||
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
||||
|
||||
#### Writing news fragments
|
||||
|
||||
In order to make writing our changelogs easier, we make use of [Towncrier]. Towncrier builds changelogs based on
|
||||
"news fragments", which are little markdown files in the `changelog.d/` directory that describe individual changes.
|
||||
|
||||
When you make a pull request that changes functionality, fixes a bug, or adds documentation, please add a news fragment
|
||||
describing your change. The file name *MUST* be in the format of `{pull_request_number}.{type}`, where `{type}` is one
|
||||
of the following:
|
||||
|
||||
- `feature` - for new features
|
||||
- `bugfix` - for bug fixes
|
||||
- `doc` - for documentation changes
|
||||
- `misc` - for other changes that don't fit the above categories
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ echo "Fixed the quantum flux stabiliser. Contributed by @alice." > changelog.d/42.bugfix
|
||||
```
|
||||
|
||||
(Note: If you want to credit yourself, you should reference your forgejo handle, however links to other platforms are also acceptable.)
|
||||
|
||||
When the next release is made, Towncrier will automatically include your news fragment in the changelog.
|
||||
|
||||
You can read more about writing news fragments in the [Towncrier tutorial][tt].
|
||||
|
||||
[Towncrier]: https://towncrier.readthedocs.io/
|
||||
[tt]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"id": 6,
|
||||
"mention_room": true,
|
||||
"date": "2025-12-30",
|
||||
"message": "Continuwuity v0.5.1 has been released. **The release contains a fix for the critical vulnerability [GHSA-m5p2-vccg-8c9v](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-m5p2-vccg-8c9v) (embargoed) affecting all Conduit-derived servers. Update as soon as possible.**\n\nThis has been *actively exploited* to attempt account takeover and forge events bricking the Continuwuity rooms. The new space is accessible at [Continuwuity (room list)](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=starstruck.systems&via=gingershaped.computer)\n"
|
||||
"date": "2025-12-22",
|
||||
"message": "Continuwuity v0.5.0 has been released. **The release contains a fix for the critical vulnerability [GHSA-22fw-4jq7-g8r8](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-22fw-4jq7-g8r8). Update as soon as possible.**\n\nThis has been *actively exploited* to create fake leave events in the Continuwuity rooms. Please leave and rejoin the rooms to fix any issues this may have caused. \n\n - [Continuwuity (space)](https://matrix.to/#/!PxtzompFuodlyzdCDtV5lzjXs10XIHeOOaq_FYodHyk?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity](https://matrix.to/#/!kn3VQSLcgWGUFm0FFRid4MinJ_aeZPjHQ0irXbHa3bU?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity Announcements](https://matrix.to/#/!d7zDZg1Vu5nhkCi50jNfOIObD5fpfGhfl48SZWZek7k?via=ellis.link)\n - [Continuwuity Offtopic](https://matrix.to/#/!QlOomq-suHC9rJHfDFVdbcGg4HS2ojSQ0bo4W2JOGMM?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity Development](https://matrix.to/#/!aAvealFbgiKTJGzumNbjuwDgt1tOkBKwiyfYqE3ouk0?via=ellis.link&via=explodie.org&via=continuwuity.org)\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
tag-message = "chore: Release v{{version}}"
|
||||
+3
-15
@@ -53,26 +53,14 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
||||
use AdminCommand::*;
|
||||
|
||||
match command {
|
||||
| Appservices(command) => {
|
||||
// appservice commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
appservice::process(command, context).await
|
||||
},
|
||||
| Appservices(command) => appservice::process(command, context).await,
|
||||
| Media(command) => media::process(command, context).await,
|
||||
| Users(command) => {
|
||||
// user commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
user::process(command, context).await
|
||||
},
|
||||
| Users(command) => user::process(command, context).await,
|
||||
| Rooms(command) => room::process(command, context).await,
|
||||
| Federation(command) => federation::process(command, context).await,
|
||||
| Server(command) => server::process(command, context).await,
|
||||
| Debug(command) => debug::process(command, context).await,
|
||||
| Query(command) => {
|
||||
// query commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
query::process(command, context).await
|
||||
},
|
||||
| Query(command) => query::process(command, context).await,
|
||||
| Check(command) => check::process(command, context).await,
|
||||
}
|
||||
}
|
||||
|
||||
+1
-21
@@ -1,6 +1,6 @@
|
||||
use std::{fmt, time::SystemTime};
|
||||
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::Result;
|
||||
use conduwuit_service::Services;
|
||||
use futures::{
|
||||
Future, FutureExt, TryFutureExt,
|
||||
@@ -8,7 +8,6 @@ use futures::{
|
||||
lock::Mutex,
|
||||
};
|
||||
use ruma::{EventId, UserId};
|
||||
use service::admin::InvocationSource;
|
||||
|
||||
pub(crate) struct Context<'a> {
|
||||
pub(crate) services: &'a Services,
|
||||
@@ -17,7 +16,6 @@ pub(crate) struct Context<'a> {
|
||||
pub(crate) reply_id: Option<&'a EventId>,
|
||||
pub(crate) sender: Option<&'a UserId>,
|
||||
pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
|
||||
pub(crate) source: InvocationSource,
|
||||
}
|
||||
|
||||
impl Context<'_> {
|
||||
@@ -45,22 +43,4 @@ impl Context<'_> {
|
||||
self.sender
|
||||
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
|
||||
}
|
||||
|
||||
/// Returns an Err if the [`Self::source`] of this context does not allow
|
||||
/// restricted commands to be executed.
|
||||
///
|
||||
/// This is intended to be placed at the start of restricted commands'
|
||||
/// implementations, like so:
|
||||
///
|
||||
/// ```ignore
|
||||
/// self.bail_restricted()?;
|
||||
/// // actual command impl
|
||||
/// ```
|
||||
pub(crate) fn bail_restricted(&self) -> Result {
|
||||
if self.source.allows_restricted() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err!("This command can only be used in the admin room.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +291,6 @@ pub(super) async fn get_remote_pdu(
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
||||
let room_state: Vec<Raw<AnyStateEvent>> = self
|
||||
.services
|
||||
@@ -419,6 +417,27 @@ pub(super) async fn change_log_level(&self, filter: Option<String>, reset: bool)
|
||||
Err!("No log level was specified.")
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn sign_json(&self) -> Result {
|
||||
if self.body.len() < 2
|
||||
|| !self.body[0].trim().starts_with("```")
|
||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
||||
{
|
||||
return Err!("Expected code block in command body. Add --help for details.");
|
||||
}
|
||||
|
||||
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
||||
match serde_json::from_str(&string) {
|
||||
| Err(e) => return Err!("Invalid json: {e}"),
|
||||
| Ok(mut value) => {
|
||||
self.services.server_keys.sign_json(&mut value)?;
|
||||
let json_text = serde_json::to_string_pretty(&value)?;
|
||||
write!(self, "{json_text}")
|
||||
},
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn verify_json(&self) -> Result {
|
||||
if self.body.len() < 2
|
||||
@@ -458,8 +477,6 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
|
||||
#[admin_command]
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
@@ -485,8 +502,6 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||
#[admin_command]
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
@@ -517,8 +532,6 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
server_name: OwnedServerName,
|
||||
at_event: Option<OwnedEventId>,
|
||||
) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
|
||||
@@ -47,9 +47,9 @@ pub enum DebugCommand {
|
||||
shorteventid: ShortEventId,
|
||||
},
|
||||
|
||||
/// - Attempts to retrieve a PDU from a remote server. **Does not** insert
|
||||
/// it into the database
|
||||
/// or persist it anywhere.
|
||||
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our
|
||||
/// database/timeline if found and we do not have this PDU already
|
||||
/// (following normal event auth rules, handles it as an incoming PDU).
|
||||
GetRemotePdu {
|
||||
/// An event ID (a $ followed by the base64 reference hash)
|
||||
event_id: OwnedEventId,
|
||||
@@ -125,6 +125,12 @@ pub enum DebugCommand {
|
||||
reset: bool,
|
||||
},
|
||||
|
||||
/// - Sign JSON blob
|
||||
///
|
||||
/// This command needs a JSON blob provided in a Markdown code block below
|
||||
/// the command.
|
||||
SignJson,
|
||||
|
||||
/// - Verify JSON signatures
|
||||
///
|
||||
/// This command needs a JSON blob provided in a Markdown code block below
|
||||
|
||||
@@ -8,14 +8,12 @@ use crate::{admin_command, get_room_info};
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||
self.bail_restricted()?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||
self.write_str("Room disabled.").await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||
self.bail_restricted()?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, false);
|
||||
self.write_str("Room enabled.").await
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ pub(super) async fn delete(
|
||||
mxc: Option<OwnedMxcUri>,
|
||||
event_id: Option<OwnedEventId>,
|
||||
) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if event_id.is_some() && mxc.is_some() {
|
||||
return Err!("Please specify either an MXC or an event ID, not both.",);
|
||||
}
|
||||
@@ -178,8 +176,6 @@ pub(super) async fn delete(
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn delete_list(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if self.body.len() < 2
|
||||
|| !self.body[0].trim().starts_with("```")
|
||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
||||
@@ -235,8 +231,6 @@ pub(super) async fn delete_past_remote_media(
|
||||
after: bool,
|
||||
yes_i_want_to_delete_local_media: bool,
|
||||
) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if before && after {
|
||||
return Err!("Please only pick one argument, --before or --after.",);
|
||||
}
|
||||
@@ -279,8 +273,6 @@ pub(super) async fn delete_all_from_server(
|
||||
server_name: OwnedServerName,
|
||||
yes_i_want_to_delete_local_media: bool,
|
||||
) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
|
||||
return Err!("This command only works for remote media by default.",);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
|
||||
reply_id: input.reply_id.as_deref(),
|
||||
sender: input.sender.as_deref(),
|
||||
output: BufWriter::new(Vec::new()).into(),
|
||||
source: input.source,
|
||||
};
|
||||
|
||||
let (result, mut logs) = process(&context, command, &args).await;
|
||||
|
||||
@@ -24,8 +24,6 @@ pub(super) async fn uptime(&self) -> Result {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn show_config(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
self.write_str(&format!("{}", *self.services.server.config))
|
||||
.await
|
||||
}
|
||||
@@ -120,8 +118,6 @@ pub(super) async fn list_backups(&self) -> Result {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn backup_database(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let db = Arc::clone(&self.services.db);
|
||||
let result = self
|
||||
.services
|
||||
@@ -148,8 +144,6 @@ pub(super) async fn admin_notice(&self, message: Vec<String>) -> Result {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn reload_mods(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
self.services.server.reload()?;
|
||||
|
||||
self.write_str("Reloading server...").await
|
||||
@@ -174,8 +168,6 @@ pub(super) async fn restart(&self, force: bool) -> Result {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn shutdown(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
warn!("shutdown command");
|
||||
self.services.server.shutdown()?;
|
||||
|
||||
|
||||
@@ -461,10 +461,8 @@ pub(super) async fn force_join_list_of_local_users(
|
||||
);
|
||||
}
|
||||
|
||||
let server_admins = self.services.admin.get_admins().await;
|
||||
|
||||
if server_admins.is_empty() {
|
||||
return Err!("There are no admins set for this server.");
|
||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
||||
return Err!("There is not an admin room to check for server admins.",);
|
||||
};
|
||||
|
||||
let (room_id, servers) = self
|
||||
@@ -484,6 +482,15 @@ pub(super) async fn force_join_list_of_local_users(
|
||||
return Err!("We are not joined in this room.");
|
||||
}
|
||||
|
||||
let server_admins: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&admin_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
@@ -576,10 +583,8 @@ pub(super) async fn force_join_all_local_users(
|
||||
);
|
||||
}
|
||||
|
||||
let server_admins = self.services.admin.get_admins().await;
|
||||
|
||||
if server_admins.is_empty() {
|
||||
return Err!("There are no admins set for this server.");
|
||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
||||
return Err!("There is not an admin room to check for server admins.",);
|
||||
};
|
||||
|
||||
let (room_id, servers) = self
|
||||
@@ -599,6 +604,15 @@ pub(super) async fn force_join_all_local_users(
|
||||
return Err!("We are not joined in this room.");
|
||||
}
|
||||
|
||||
let server_admins: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&admin_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
|
||||
@@ -13,6 +13,7 @@ use ruma::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use service::Services;
|
||||
|
||||
use super::banned_room_check;
|
||||
@@ -146,7 +147,17 @@ pub(crate) async fn invite_helper(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
|
||||
let invite_room_state = services
|
||||
.rooms
|
||||
.state
|
||||
.summary(&pdu, room_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|evt| RawValue::from_string(evt.json().get().to_owned()))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not clone invite state event: {e}"))))
|
||||
})?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ use service::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::{banned_room_check, validate_remote_member_event_stub};
|
||||
use super::banned_room_check;
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
||||
@@ -837,13 +837,6 @@ async fn join_room_by_id_helper_local(
|
||||
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
|
||||
})?;
|
||||
|
||||
validate_remote_member_event_stub(
|
||||
&MembershipState::Join,
|
||||
sender_user,
|
||||
room_id,
|
||||
&join_event_stub,
|
||||
)?;
|
||||
|
||||
let join_authorized_via_users_server = join_event_stub
|
||||
.get("content")
|
||||
.map(|s| {
|
||||
|
||||
@@ -38,7 +38,7 @@ use service::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::{banned_room_check, join::join_room_by_id_helper, validate_remote_member_event_stub};
|
||||
use super::{banned_room_check, join::join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
|
||||
@@ -408,13 +408,6 @@ async fn knock_room_helper_local(
|
||||
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
||||
})?;
|
||||
|
||||
validate_remote_member_event_stub(
|
||||
&MembershipState::Knock,
|
||||
sender_user,
|
||||
room_id,
|
||||
&knock_event_stub,
|
||||
)?;
|
||||
|
||||
knock_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||
|
||||
@@ -21,7 +21,6 @@ use ruma::{
|
||||
};
|
||||
use service::Services;
|
||||
|
||||
use super::validate_remote_member_event_stub;
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
|
||||
@@ -325,13 +324,6 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
)))
|
||||
})?;
|
||||
|
||||
validate_remote_member_event_stub(
|
||||
&MembershipState::Leave,
|
||||
user_id,
|
||||
room_id,
|
||||
&leave_event_stub,
|
||||
)?;
|
||||
|
||||
// TODO: Is origin needed?
|
||||
leave_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
|
||||
@@ -13,14 +13,7 @@ use std::net::IpAddr;
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, warn};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, OwnedRoomId, RoomId, ServerName, UserId,
|
||||
api::client::membership::joined_rooms,
|
||||
events::{
|
||||
StaticEventContent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
};
|
||||
use ruma::{OwnedRoomId, RoomId, ServerName, UserId, api::client::membership::joined_rooms};
|
||||
use service::Services;
|
||||
|
||||
pub(crate) use self::{
|
||||
@@ -160,80 +153,3 @@ pub(crate) async fn banned_room_check(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates that an event returned from a remote server by `/make_*`
|
||||
/// actually is a membership event with the expected fields.
|
||||
///
|
||||
/// Without checking this, the remote server could use the remote membership
|
||||
/// mechanism to trick our server into signing arbitrary malicious events.
|
||||
pub(crate) fn validate_remote_member_event_stub(
|
||||
membership: &MembershipState,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
event_stub: &CanonicalJsonObject,
|
||||
) -> Result<()> {
|
||||
let Some(event_type) = event_stub.get("type") else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing type field"
|
||||
));
|
||||
};
|
||||
if event_type != &RoomMemberEventContent::TYPE {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with invalid event type"
|
||||
));
|
||||
}
|
||||
|
||||
let Some(sender) = event_stub.get("sender") else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing sender field"
|
||||
));
|
||||
};
|
||||
if sender != &user_id.as_str() {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with incorrect sender"
|
||||
));
|
||||
}
|
||||
|
||||
let Some(state_key) = event_stub.get("state_key") else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing state_key field"
|
||||
));
|
||||
};
|
||||
if state_key != &user_id.as_str() {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with incorrect state_key"
|
||||
));
|
||||
}
|
||||
|
||||
let Some(event_room_id) = event_stub.get("room_id") else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing room_id field"
|
||||
));
|
||||
};
|
||||
if event_room_id != &room_id.as_str() {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with incorrect room_id"
|
||||
));
|
||||
}
|
||||
|
||||
let Some(content) = event_stub
|
||||
.get("content")
|
||||
.and_then(|content| content.as_object())
|
||||
else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing content field"
|
||||
));
|
||||
};
|
||||
let Some(event_membership) = content.get("membership") else {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with missing membership field"
|
||||
));
|
||||
};
|
||||
if event_membership != &membership.as_str() {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with incorrect room_id"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_warn, info, trace,
|
||||
Err, Result, debug_warn, trace,
|
||||
utils::{IterStream, future::TryExtExt},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryFutureExt,
|
||||
FutureExt, StreamExt,
|
||||
future::{OptionFuture, join3},
|
||||
stream::FuturesUnordered,
|
||||
};
|
||||
@@ -79,15 +79,9 @@ async fn room_summary_response(
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
match local_room_summary_response(services, room_id, sender_user)
|
||||
return local_room_summary_response(services, room_id, sender_user)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Ok(response) => return Ok(response),
|
||||
| Err(e) => {
|
||||
debug_warn!("Failed to get local room summary: {e:?}, falling back to remote");
|
||||
},
|
||||
}
|
||||
.await;
|
||||
}
|
||||
|
||||
let room =
|
||||
@@ -117,27 +111,26 @@ async fn local_room_summary_response(
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
|
||||
let (join_rule, world_readable, guest_can_join) = join3(
|
||||
services.rooms.state_accessor.get_join_rules(room_id),
|
||||
services.rooms.state_accessor.is_world_readable(room_id),
|
||||
services.rooms.state_accessor.guest_can_join(room_id),
|
||||
)
|
||||
.await;
|
||||
let join_rule = services.rooms.state_accessor.get_join_rules(room_id);
|
||||
|
||||
// Synapse allows server admins to bypass visibility checks.
|
||||
// That seems neat so we'll copy that behaviour.
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&join_rule.clone().into(),
|
||||
guest_can_join,
|
||||
world_readable,
|
||||
join_rule.allowed_rooms(),
|
||||
sender_user,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let world_readable = services.rooms.state_accessor.is_world_readable(room_id);
|
||||
|
||||
let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id);
|
||||
|
||||
let (join_rule, world_readable, guest_can_join) =
|
||||
join3(join_rule, world_readable, guest_can_join).await;
|
||||
|
||||
trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
|
||||
user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&join_rule.clone().into(),
|
||||
guest_can_join,
|
||||
world_readable,
|
||||
join_rule.allowed_rooms(),
|
||||
sender_user,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let canonical_alias = services
|
||||
.rooms
|
||||
@@ -238,27 +231,15 @@ async fn remote_room_summary_hierarchy_response(
|
||||
"Federaton of room {room_id} is currently disabled on this server."
|
||||
)));
|
||||
}
|
||||
if servers.is_empty() {
|
||||
return Err!(Request(MissingParam(
|
||||
"No servers were provided to fetch the room over federation"
|
||||
)));
|
||||
}
|
||||
|
||||
let request = get_hierarchy::v1::Request::new(room_id.to_owned());
|
||||
|
||||
let mut requests: FuturesUnordered<_> = servers
|
||||
.iter()
|
||||
.map(|server| {
|
||||
info!("Fetching room summary for {room_id} from server {server}");
|
||||
services
|
||||
.sending
|
||||
.send_federation_request(server, request.clone())
|
||||
.inspect_ok(move |v| {
|
||||
debug!("Fetched room summary for {room_id} from server {server}: {v:?}");
|
||||
})
|
||||
.inspect_err(move |e| {
|
||||
info!("Failed to fetch room summary for {room_id} from server {server}: {e}");
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -274,23 +255,23 @@ async fn remote_room_summary_hierarchy_response(
|
||||
continue;
|
||||
}
|
||||
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
return user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&room.join_rule,
|
||||
room.guest_can_join,
|
||||
room.world_readable,
|
||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
||||
sender_user,
|
||||
)
|
||||
.await
|
||||
.map(|()| room);
|
||||
}
|
||||
return Ok(room);
|
||||
return user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&room.join_rule,
|
||||
room.guest_can_join,
|
||||
room.world_readable,
|
||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
||||
sender_user,
|
||||
)
|
||||
.await
|
||||
.map(|()| room);
|
||||
}
|
||||
|
||||
Err!(Request(NotFound("Room not found or is not accessible")))
|
||||
Err!(Request(NotFound(
|
||||
"Room is unknown to this server and was unable to fetch over federation with the \
|
||||
provided servers available"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn user_can_see_summary<'a, I>(
|
||||
@@ -330,14 +311,21 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
Err!(Request(Forbidden(
|
||||
"Room is not world readable, not publicly accessible/joinable, restricted room \
|
||||
conditions not met, and guest access is forbidden. Not allowed to see details \
|
||||
of this room."
|
||||
)))
|
||||
},
|
||||
| None => {
|
||||
if is_public_room || world_readable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
Err!(Request(Forbidden(
|
||||
"Room is not world readable or publicly accessible/joinable, authentication is \
|
||||
required"
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,6 @@ pub(crate) async fn upgrade_room_route(
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
|
||||
// Make sure this isn't the admin room
|
||||
// Admin room upgrades are hacky and should be done manually instead.
|
||||
if services.admin.is_admin_room(&body.room_id).await {
|
||||
return Err!(Request(Forbidden("Upgrading the admin room this way is not allowed.")));
|
||||
}
|
||||
|
||||
// First, check if the user has permission to upgrade the room (send tombstone
|
||||
// event)
|
||||
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
@@ -272,7 +266,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
.room_state_keys(&body.room_id, event_type)
|
||||
.await?;
|
||||
for state_key in state_keys {
|
||||
let mut event_content = match services
|
||||
let event_content = match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, event_type, &state_key)
|
||||
@@ -285,21 +279,6 @@ pub(crate) async fn upgrade_room_route(
|
||||
// If the event content is empty, we skip it
|
||||
continue;
|
||||
}
|
||||
// If this is a power levels event, and the new room version has creators,
|
||||
// we need to make sure they dont appear in the users block of power levels.
|
||||
if *event_type == StateEventType::RoomPowerLevels {
|
||||
// TODO(v12): additional creators
|
||||
let creators = vec![sender_user];
|
||||
let mut power_levels_event_content: RoomPowerLevelsEventContent =
|
||||
serde_json::from_str(event_content.get()).map_err(|_| {
|
||||
err!(Request(BadJson("Power levels event content is not valid")))
|
||||
})?;
|
||||
for creator in creators {
|
||||
power_levels_event_content.users.remove(creator);
|
||||
}
|
||||
event_content = to_raw_value(&power_levels_event_content)
|
||||
.expect("event is valid, we just deserialized and modified it");
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use axum::{Json, extract::State, response::IntoResponse};
|
||||
use conduwuit::{Error, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::{
|
||||
discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||
@@ -70,18 +71,21 @@ pub(crate) async fn well_known_support(
|
||||
|
||||
// Try to add admin users as contacts if no contacts are configured
|
||||
if contacts.is_empty() {
|
||||
let admin_users = services.admin.get_admins().await;
|
||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
||||
let admin_users = services.rooms.state_cache.room_members(&admin_room);
|
||||
let mut stream = admin_users;
|
||||
|
||||
for user_id in admin_users.iter() {
|
||||
if *user_id == services.globals.server_user {
|
||||
continue;
|
||||
while let Some(user_id) = stream.next().await {
|
||||
// Skip server user
|
||||
if *user_id == services.globals.server_user {
|
||||
continue;
|
||||
}
|
||||
contacts.push(Contact {
|
||||
role: role_value.clone(),
|
||||
email_address: None,
|
||||
matrix_id: Some(user_id.to_owned()),
|
||||
});
|
||||
}
|
||||
|
||||
contacts.push(Contact {
|
||||
role: role_value.clone(),
|
||||
email_address: None,
|
||||
matrix_id: Some(user_id.to_owned()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+351
-89
@@ -1,30 +1,327 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use conduwuit::{
|
||||
Err, Error, PduEvent, Result, err,
|
||||
matrix::{Event, event::gen_event_id},
|
||||
Err, Error, EventTypeExt, PduEvent, Result, RoomVersion, debug, debug_warn, err,
|
||||
matrix::{Event, StateKey, event::gen_event_id},
|
||||
trace,
|
||||
utils::{self, hash::sha256},
|
||||
warn,
|
||||
};
|
||||
use ruma::{
|
||||
CanonicalJsonValue, OwnedUserId, UserId,
|
||||
CanonicalJsonValue, OwnedUserId, RoomId, RoomVersionId, ServerName, UserId,
|
||||
api::{client::error::ErrorKind, federation::membership::create_invite},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
serde::JsonObject,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
create::RoomCreateEventContent,
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use service::{Services, rooms::timeline::pdu_fits};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// Ensures that the state received from the invite endpoint is sane, correct,
|
||||
/// and complies with the room version's requirements.
|
||||
async fn check_invite_state(
|
||||
services: &Services,
|
||||
stripped_state: &Vec<Box<RawValue>>,
|
||||
room_id: &RoomId,
|
||||
room_version_id: &RoomVersionId,
|
||||
) -> Result<()> {
|
||||
let room_version = RoomVersion::new(room_version_id).map_err(|e| {
|
||||
err!(Request(UnsupportedRoomVersion("Invalid room version provided: {e}")))
|
||||
})?;
|
||||
let mut room_state: HashMap<(StateEventType, StateKey), PduEvent> = HashMap::new();
|
||||
|
||||
// Build the room state from the provided state events,
|
||||
// ensuring that there's no duplicates. We need to check that m.room.create is
|
||||
// present and lines up with the other things we've been told, and then verify
|
||||
// any signatures present to ensure this isn't forged.
|
||||
for raw_event in stripped_state {
|
||||
trace!("Processing invite state event: {:?}", raw_event);
|
||||
let canonical = utils::to_canonical_object(raw_event)?;
|
||||
let event_id = gen_event_id(&canonical, room_version_id)?;
|
||||
let event = PduEvent::from_id_val(&event_id, canonical.clone())
|
||||
.map_err(|e| err!(Request(InvalidParam("Invite state event is invalid: {e}"))))?;
|
||||
if event.state_key().is_none() {
|
||||
return Err!(Request(InvalidParam("Invite state event missing event type.")));
|
||||
}
|
||||
let key = event
|
||||
.event_type()
|
||||
.with_state_key(event.state_key().unwrap());
|
||||
if room_state.contains_key(&key) {
|
||||
return Err!(Request(InvalidParam("Duplicate state event found for {key:?}")));
|
||||
}
|
||||
|
||||
// verify the event
|
||||
if !pdu_fits(&canonical) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"An invite state event ({event_id}) is too large"
|
||||
)));
|
||||
}
|
||||
services
|
||||
.server_keys
|
||||
.verify_event(&canonical, Some(room_version_id))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam(
|
||||
"Signature failed verification on event {event_id}: {e}"
|
||||
)))
|
||||
})?;
|
||||
|
||||
// Ensure all events are in the same room
|
||||
if event.room_id_or_hash() != room_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"State event room ID for {} does not match the expected room ID {}.",
|
||||
event.event_id,
|
||||
room_id,
|
||||
)));
|
||||
}
|
||||
room_state.insert(key, event);
|
||||
}
|
||||
|
||||
// verify m.room.create is present, has a matching room ID, and a matching room
|
||||
// version.
|
||||
let create_event = room_state
|
||||
.get(&(StateEventType::RoomCreate, "".into()))
|
||||
.ok_or_else(|| err!(Request(MissingParam("Missing m.room.create in stripped state."))))?;
|
||||
let create_event_content: RoomCreateEventContent = create_event
|
||||
.get_content()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid m.room.create content: {e}"))))?;
|
||||
// Room v12 removed room IDs over federation, so we'll need to see if the event
|
||||
// ID matches the room ID instead.
|
||||
if room_version.room_ids_as_hashes {
|
||||
let given_room_id = create_event.event_id().as_str().replace('$', "!");
|
||||
if given_room_id != room_id.as_str() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"m.room.create event ID does not match the room ID."
|
||||
)));
|
||||
}
|
||||
} else if create_event.room_id().unwrap() != room_id {
|
||||
return Err!(Request(InvalidParam("m.room.create room ID does not match the room ID.")));
|
||||
}
|
||||
|
||||
// Make sure the room version matches
|
||||
if &create_event_content.room_version != room_version_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"m.room.create room version does not match the given room version."
|
||||
)));
|
||||
}
|
||||
|
||||
// Looks solid
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensures that the invite event received from the invite endpoint is sane,
|
||||
/// correct, and complies with the room version's requirements.
|
||||
/// Returns the invited user ID on success.
|
||||
async fn check_invite_event(
|
||||
services: &Services,
|
||||
invite_event: &PduEvent,
|
||||
origin: &ServerName,
|
||||
room_id: &RoomId,
|
||||
room_version_id: &RoomVersionId,
|
||||
) -> Result<OwnedUserId> {
|
||||
// Check: The event sender is not a user ID on the origin server.
|
||||
if invite_event.sender.server_name() != origin {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event sender's server does not match the origin server."
|
||||
)));
|
||||
}
|
||||
// Check: The `state_key` is not a user ID on the receiving server.
|
||||
let state_key: &UserId = invite_event
|
||||
.state_key()
|
||||
.ok_or_else(|| err!(Request(MissingParam("Invite event missing state_key."))))?
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
|
||||
if !services.globals.server_is_ours(state_key.server_name()) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event state_key does not belong to this homeserver."
|
||||
)));
|
||||
}
|
||||
|
||||
// Check: The event's room ID matches the expected room ID.
|
||||
if let Some(evt_room_id) = invite_event.room_id() {
|
||||
if evt_room_id != room_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event room ID does not match the expected room ID."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err!(Request(MissingParam("Invite event missing room ID.")));
|
||||
}
|
||||
|
||||
// Check: the membership really is "invite"
|
||||
let content = invite_event.get_content::<RoomMemberEventContent>()?;
|
||||
if content.membership != MembershipState::Invite {
|
||||
return Err!(Request(InvalidParam("Invite event is not a membership invite.")));
|
||||
}
|
||||
|
||||
// Check: signature is valid
|
||||
let as_obj = &mut utils::to_canonical_object(invite_event)?;
|
||||
// remove the event_id before verification
|
||||
as_obj.remove("event_id");
|
||||
services
|
||||
.server_keys
|
||||
.verify_event(as_obj, Some(room_version_id))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam("Invite event signature failed verification: {e}")))
|
||||
})?;
|
||||
|
||||
Ok(state_key.to_owned())
|
||||
}
|
||||
|
||||
/// Performs only legacy checks on the invite, for use when the requesting
|
||||
/// server doesn't support matrix v1.16 invites.
|
||||
/// This is significantly less secure than the full checks and should only be
|
||||
/// used if the user has allowed it.
|
||||
async fn legacy_check_invite(
|
||||
services: &Services,
|
||||
origin: &ServerName,
|
||||
invite_event: &PduEvent,
|
||||
stripped_state: &Vec<Box<RawValue>>,
|
||||
room_id: &RoomId,
|
||||
room_version_id: &RoomVersionId,
|
||||
) -> Result<OwnedUserId> {
|
||||
// Ensure the sender is from origin, the state key is a user ID that points at a
|
||||
// local user, the event type is m.room.member with membership "invite", and
|
||||
// the room ID matches.
|
||||
if invite_event.sender().server_name() != origin {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event sender's server does not match the origin server."
|
||||
)));
|
||||
}
|
||||
let state_key: &UserId = invite_event
|
||||
.state_key()
|
||||
.ok_or_else(|| err!(Request(MissingParam("Invite event missing state_key."))))?
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
|
||||
if !services.globals.server_is_ours(state_key.server_name()) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event state_key does not belong to this homeserver."
|
||||
)));
|
||||
}
|
||||
if let Some(evt_room_id) = invite_event.room_id() {
|
||||
if evt_room_id != room_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Invite event room ID does not match the expected room ID."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err!(Request(MissingParam("Invite event missing room ID.")));
|
||||
}
|
||||
let content = invite_event.get_content::<RoomMemberEventContent>()?;
|
||||
if content.membership != MembershipState::Invite {
|
||||
return Err!(Request(InvalidParam("Invite event is not a membership invite.")));
|
||||
}
|
||||
|
||||
// We can also opportunistically check that the m.room.create event is present
|
||||
// and matches the room version, to avoid accepting invites to rooms that
|
||||
// don't match.
|
||||
let mut has_create = false;
|
||||
for raw_event in stripped_state {
|
||||
let canonical = utils::to_canonical_object(raw_event)?;
|
||||
if canonical.get("room_id").is_none() {
|
||||
// This is a stripped event, skip
|
||||
continue;
|
||||
}
|
||||
if let Some(event_type) = canonical.get("type") {
|
||||
if event_type.as_str().unwrap_or_default() == "m.room.create" {
|
||||
has_create = true;
|
||||
let event_id = gen_event_id(&canonical, room_version_id)?;
|
||||
let event = PduEvent::from_id_val(&event_id, canonical.clone()).map_err(|e| {
|
||||
err!(Request(InvalidParam("Invite state event is invalid: {e}")))
|
||||
})?;
|
||||
|
||||
// We can verify that the room ID is correct now
|
||||
let version = RoomVersion::new(room_version_id)?;
|
||||
if version.room_ids_as_hashes {
|
||||
let given_room_id = event.event_id().as_str().replace('$', "!");
|
||||
if given_room_id != room_id.as_str() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"m.room.create event ID does not match the room ID."
|
||||
)));
|
||||
}
|
||||
} else if event.room_id().unwrap() != room_id {
|
||||
return Err!(Request(InvalidParam(
|
||||
"m.room.create room ID does not match the room ID."
|
||||
)));
|
||||
}
|
||||
// Everything's as fine as we're getting with this event
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !has_create {
|
||||
warn!(
|
||||
"federated invite is missing m.room.create event in stripped state, the remote \
|
||||
server is either outdated or trying something fishy."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(state_key.to_owned())
|
||||
}
|
||||
|
||||
/// Checks the incoming event is allowed and not forged.
|
||||
/// If the MSC4311 enforcement experiment is enabled, performs full checks,
|
||||
/// otherwise performs legacy checks only.
|
||||
async fn check_invite(
|
||||
services: &Services,
|
||||
invite_event: &PduEvent,
|
||||
stripped_state: &Vec<Box<RawValue>>,
|
||||
origin: &ServerName,
|
||||
room_id: &RoomId,
|
||||
room_version_id: &RoomVersionId,
|
||||
) -> Result<OwnedUserId> {
|
||||
if services.config.experiments.enforce_msc4311 {
|
||||
debug!("Checking invite event validity");
|
||||
let user = check_invite_event(services, invite_event, origin, room_id, room_version_id)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
debug_warn!("Invite event validity check failed: {e}");
|
||||
})?;
|
||||
debug!("Checking invite state validity");
|
||||
check_invite_state(services, stripped_state, room_id, room_version_id)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
debug_warn!("Invite state validity check failed: {e}");
|
||||
})?;
|
||||
Ok(user)
|
||||
} else {
|
||||
debug!("Performing legacy invite checks");
|
||||
legacy_check_invite(
|
||||
services,
|
||||
origin,
|
||||
invite_event,
|
||||
stripped_state,
|
||||
room_id,
|
||||
room_version_id,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
debug_warn!("Legacy invite validity check failed: {e}");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
|
||||
///
|
||||
/// Invites a remote user to a room.
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
|
||||
#[tracing::instrument(skip_all, fields(%client, room_id=?body.room_id), name = "invite")]
|
||||
pub(crate) async fn create_invite_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<create_invite::v2::Request>,
|
||||
) -> Result<create_invite::v2::Response> {
|
||||
debug!("Received invite request from {}: {:?}", body.room_id, body.origin());
|
||||
|
||||
// ACL check origin
|
||||
services
|
||||
.rooms
|
||||
@@ -33,6 +330,7 @@ pub(crate) async fn create_invite_route(
|
||||
.await?;
|
||||
|
||||
if !services.server.supported_room_version(&body.room_version) {
|
||||
debug_warn!("Unsupported room version: {}", body.room_version);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::IncompatibleRoomVersion { room_version: body.room_version.clone() },
|
||||
"Server does not support this room version.",
|
||||
@@ -41,6 +339,7 @@ pub(crate) async fn create_invite_route(
|
||||
|
||||
if let Some(server) = body.room_id.server_name() {
|
||||
if services.moderation.is_remote_server_forbidden(server) {
|
||||
warn!("Received invite to room created by a banned server: {}. Rejecting.", server);
|
||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||
}
|
||||
}
|
||||
@@ -50,7 +349,7 @@ pub(crate) async fn create_invite_route(
|
||||
.is_remote_server_forbidden(body.origin())
|
||||
{
|
||||
warn!(
|
||||
"Received federated/remote invite from banned server {} for room ID {}. Rejecting.",
|
||||
"Received invite from banned server {} for room ID {}. Rejecting.",
|
||||
body.origin(),
|
||||
body.room_id
|
||||
);
|
||||
@@ -61,57 +360,42 @@ pub(crate) async fn create_invite_route(
|
||||
let mut signed_event = utils::to_canonical_object(&body.event)
|
||||
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
|
||||
|
||||
// Ensure this is a membership event
|
||||
if signed_event
|
||||
.get("type")
|
||||
.expect("event must have a type")
|
||||
.as_str()
|
||||
.expect("type must be a string")
|
||||
!= "m.room.member"
|
||||
{
|
||||
return Err!(Request(BadJson(
|
||||
"Not allowed to send non-membership event to invite endpoint."
|
||||
)));
|
||||
// We need to hash and sign the event before we can generate the event ID.
|
||||
// It is important that this signed event does not get sent back to the caller
|
||||
// until we've verified this isn't incorrect.
|
||||
trace!(event=?signed_event, "Hashing & signing invite event");
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut signed_event, &body.room_version)
|
||||
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
|
||||
let event_id = gen_event_id(&signed_event.clone(), &body.room_version)?;
|
||||
if event_id != body.event_id {
|
||||
warn!("Event ID mismatch: expected {}, got {}", event_id, body.event_id);
|
||||
return Err!(Request(InvalidParam("Event ID does not match the generated event ID.")));
|
||||
}
|
||||
|
||||
let content: RoomMemberEventContent = serde_json::from_value(
|
||||
signed_event
|
||||
.get("content")
|
||||
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
||||
.clone()
|
||||
.into(),
|
||||
let pdu = PduEvent::from_id_val(&event_id, signed_event.clone())
|
||||
.map_err(|e| err!(Request(InvalidParam("Invite event is invalid: {e}"))))?;
|
||||
|
||||
let recipient_user = check_invite(
|
||||
&services,
|
||||
&pdu,
|
||||
&body.invite_room_state,
|
||||
body.origin(),
|
||||
&body.room_id,
|
||||
&body.room_version,
|
||||
)
|
||||
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
|
||||
.await?;
|
||||
|
||||
// Ensure this is an invite membership event
|
||||
if content.membership != MembershipState::Invite {
|
||||
return Err!(Request(BadJson(
|
||||
"Not allowed to send a non-invite membership event to invite endpoint."
|
||||
)));
|
||||
}
|
||||
|
||||
// Ensure the sending user isn't a lying bozo
|
||||
let sender_server = signed_event
|
||||
.get("sender")
|
||||
.try_into()
|
||||
.map(UserId::server_name)
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
|
||||
if sender_server != body.origin() {
|
||||
return Err!(Request(Forbidden("Sender's server does not match the origin server.",)));
|
||||
}
|
||||
|
||||
// Ensure the target user belongs to this server
|
||||
let recipient_user: OwnedUserId = signed_event
|
||||
.get("state_key")
|
||||
.try_into()
|
||||
.map(UserId::to_owned)
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
|
||||
|
||||
if !services
|
||||
.globals
|
||||
.server_is_ours(recipient_user.server_name())
|
||||
// Make sure the room isn't banned and we allow invites
|
||||
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
|
||||
{
|
||||
return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
|
||||
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
||||
}
|
||||
if services.rooms.metadata.is_banned(&body.room_id).await
|
||||
&& !services.users.is_admin(&recipient_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||
}
|
||||
|
||||
// Make sure we're not ACL'ed from their room.
|
||||
@@ -121,45 +405,9 @@ pub(crate) async fn create_invite_route(
|
||||
.acl_check(recipient_user.server_name(), &body.room_id)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut signed_event, &body.room_version)
|
||||
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&signed_event, &body.room_version)?;
|
||||
|
||||
// Add event_id back
|
||||
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
|
||||
|
||||
let sender_user: &UserId = signed_event
|
||||
.get("sender")
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
|
||||
|
||||
if services.rooms.metadata.is_banned(&body.room_id).await
|
||||
&& !services.users.is_admin(&recipient_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||
}
|
||||
|
||||
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden("This server does not allow room invites.")));
|
||||
}
|
||||
|
||||
let mut invite_state = body.invite_room_state.clone();
|
||||
|
||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
|
||||
event.insert("event_id".to_owned(), "$placeholder".into());
|
||||
|
||||
let pdu: PduEvent = serde_json::from_value(event.into())
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
|
||||
invite_state.push(pdu.to_format());
|
||||
|
||||
// If we are active in the room, the remote server will notify us about the
|
||||
// join/invite through /send. If we are not in the room, we need to manually
|
||||
// record the invited state for client /sync through update_membership(), and
|
||||
@@ -170,6 +418,19 @@ pub(crate) async fn create_invite_route(
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
let mut invite_state: Vec<CanonicalJsonValue> = body
|
||||
.invite_room_state
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|v| utils::to_canonical_object(&v).unwrap().into())
|
||||
.collect();
|
||||
|
||||
invite_state.push(pdu.to_canonical_object().into());
|
||||
let sender_user: &UserId = signed_event
|
||||
.get("sender")
|
||||
.try_into()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
|
||||
debug!("Marking user {} as invited to remote room {}", recipient_user, body.room_id);
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -208,6 +469,7 @@ pub(crate) async fn create_invite_route(
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Invite is valid, returning signed event");
|
||||
Ok(create_invite::v2::Response {
|
||||
event: services
|
||||
.sending
|
||||
|
||||
@@ -175,11 +175,7 @@ pub(crate) async fn create_knock_event_v1_route(
|
||||
.send_pdu_room(&body.room_id, &pdu_id)
|
||||
.await?;
|
||||
|
||||
let knock_room_state = services
|
||||
.rooms
|
||||
.state
|
||||
.summary_stripped(&pdu, &body.room_id)
|
||||
.await;
|
||||
let knock_room_state = services.rooms.state.summary(&pdu, &body.room_id).await;
|
||||
|
||||
Ok(send_knock::v1::Response { knock_room_state })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
mod msc4284_policy_servers;
|
||||
|
||||
use conduwuit_macros::config_example_generator;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.experiments")]
|
||||
pub struct Experiments {
|
||||
/// Enforce MSC4311's updated requirements on all incoming invites.
|
||||
///
|
||||
/// This drastically increases the security and filtering capabilities
|
||||
/// when processing invites over federation, at the cost of compatibility.
|
||||
/// Servers that do not implement MSC4311 will be unable to send invites
|
||||
/// to your server when this is enabled, including continuwuity 0.5.0 and
|
||||
/// below.
|
||||
///
|
||||
/// default: false
|
||||
/// Introduced in: (unreleased)
|
||||
#[serde(default)]
|
||||
pub enforce_msc4311: bool,
|
||||
|
||||
/// MSC4284 Policy Server support configuration.
|
||||
#[serde(default)]
|
||||
pub msc4284: msc4284_policy_servers::MSC4248,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use conduwuit_macros::config_example_generator;
|
||||
use serde::Deserialize;
|
||||
|
||||
fn true_fn() -> bool { true }
|
||||
|
||||
fn default_federation_timeout() -> u64 { 25 }
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.experiments.msc4284"
|
||||
)]
|
||||
pub struct MSC4248 {
|
||||
/// Enable or disable making requests to MSC4284 Policy Servers.
|
||||
/// It is recommended you keep this enabled unless you experience frequent
|
||||
/// connectivity issues, such as in a restricted networking environment.
|
||||
///
|
||||
/// default: true
|
||||
/// Introduced in: 0.5.0
|
||||
#[serde(default = "true_fn")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Enable running locally generated events through configured MSC4284
|
||||
/// policy servers. You may wish to disable this if your server is
|
||||
/// single-user for a slight speed benefit in some rooms, but otherwise
|
||||
/// should leave it enabled.
|
||||
///
|
||||
/// If the room's policy server configuration requires event signatures,
|
||||
/// this option is effectively ignored, as otherwise local events would
|
||||
/// be rejected for missing the policy server's signature.
|
||||
///
|
||||
/// default: true
|
||||
/// Introduced in: 0.5.0
|
||||
#[serde(default = "true_fn")]
|
||||
pub check_own_events: bool,
|
||||
|
||||
/// MSC4284 Policy server request timeout (seconds). Generally policy
|
||||
/// servers should respond near instantly, however may slow down under
|
||||
/// load. If a policy server doesn't respond in a short amount of time, the
|
||||
/// room it is configured in may become unusable if this limit is set too
|
||||
/// high. 25 seconds is a good default, however should be raised if you
|
||||
/// experience too many connection issues.
|
||||
///
|
||||
/// Please be aware that policy requests are *NOT* currently re-tried, so if
|
||||
/// a spam check request fails, the event will be assumed to be not spam,
|
||||
/// which in some cases may result in spam being sent to or received from
|
||||
/// the room that would typically be prevented.
|
||||
///
|
||||
/// If your request timeout is too low, and the policy server requires
|
||||
/// signatures, you may find that you are unable to send events that are
|
||||
/// accepted regardless.
|
||||
///
|
||||
/// About policy servers: https://matrix.org/blog/2025/04/introducing-policy-servers/
|
||||
/// default: 25
|
||||
/// Introduced in: 0.5.0
|
||||
#[serde(default = "default_federation_timeout")]
|
||||
pub request_timeout: u64,
|
||||
}
|
||||
+15
-38
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::doc_link_with_quotes)]
|
||||
pub mod check;
|
||||
pub mod experiments;
|
||||
pub mod manager;
|
||||
pub mod proxy;
|
||||
|
||||
@@ -69,8 +70,8 @@ pub struct Config {
|
||||
/// Also see the `[global.well_known]` config section at the very bottom.
|
||||
///
|
||||
/// Examples of delegation:
|
||||
/// - https://continuwuity.org/.well-known/matrix/server
|
||||
/// - https://continuwuity.org/.well-known/matrix/client
|
||||
/// - https://puppygock.gay/.well-known/matrix/server
|
||||
/// - https://puppygock.gay/.well-known/matrix/client
|
||||
///
|
||||
/// YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
||||
/// WIPE.
|
||||
@@ -737,13 +738,6 @@ pub struct Config {
|
||||
#[serde(default = "default_otlp_filter", alias = "jaeger_filter")]
|
||||
pub otlp_filter: String,
|
||||
|
||||
/// Protocol to use for OTLP tracing export. Options are "http" or "grpc".
|
||||
/// The HTTP protocol uses port 4318 by default, while gRPC uses port 4317.
|
||||
///
|
||||
/// default: "http"
|
||||
#[serde(default = "default_otlp_protocol")]
|
||||
pub otlp_protocol: String,
|
||||
|
||||
/// If the 'perf_measurements' compile-time feature is enabled, enables
|
||||
/// collecting folded stack trace profile of tracing spans using
|
||||
/// tracing_flame. The resulting profile can be visualized with inferno[1],
|
||||
@@ -1741,16 +1735,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub block_non_admin_invites: bool,
|
||||
|
||||
/// Enable or disable making requests to MSC4284 Policy Servers.
|
||||
/// It is recommended you keep this enabled unless you experience frequent
|
||||
/// connectivity issues, such as in a restricted networking environment.
|
||||
#[serde(default = "true_fn")]
|
||||
pub enable_msc4284_policy_servers: bool,
|
||||
|
||||
/// Enable running locally generated events through configured MSC4284
|
||||
/// policy servers. You may wish to disable this if your server is
|
||||
/// single-user for a slight speed benefit in some rooms, but otherwise
|
||||
/// should leave it enabled.
|
||||
#[serde(default = "true_fn")]
|
||||
pub policy_server_check_own_events: bool,
|
||||
|
||||
@@ -1759,7 +1746,7 @@ pub struct Config {
|
||||
/// a normal continuwuity admin command. The reply will be publicly visible
|
||||
/// to the room, originating from the sender.
|
||||
///
|
||||
/// example: \\!admin debug ping continuwuity.org
|
||||
/// example: \\!admin debug ping puppygock.gay
|
||||
#[serde(default = "true_fn")]
|
||||
pub admin_escape_commands: bool,
|
||||
|
||||
@@ -1777,8 +1764,7 @@ pub struct Config {
|
||||
/// For example: `./continuwuity --execute "server admin-notice continuwuity
|
||||
/// has started up at $(date)"`
|
||||
///
|
||||
/// example: admin_execute = ["debug ping continuwuity.org", "debug echo
|
||||
/// hi"]`
|
||||
/// example: admin_execute = ["debug ping puppygock.gay", "debug echo hi"]`
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default)]
|
||||
@@ -1819,22 +1805,6 @@ pub struct Config {
|
||||
#[serde(default = "default_admin_room_tag")]
|
||||
pub admin_room_tag: String,
|
||||
|
||||
/// A list of Matrix IDs that are qualified as server admins.
|
||||
///
|
||||
/// Any Matrix IDs within this list are regarded as an admin
|
||||
/// regardless of whether they are in the admin room or not
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default)]
|
||||
pub admins_list: Vec<OwnedUserId>,
|
||||
|
||||
/// Defines whether those within the admin room are added to the
|
||||
/// admins_list.
|
||||
///
|
||||
/// default: true
|
||||
#[serde(default = "true_fn")]
|
||||
pub admins_from_room: bool,
|
||||
|
||||
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||
/// This is NOT enabled by default.
|
||||
#[serde(default)]
|
||||
@@ -2027,6 +1997,13 @@ pub struct Config {
|
||||
// external structure; separate section
|
||||
#[serde(default)]
|
||||
pub blurhashing: BlurhashConfig,
|
||||
|
||||
/// Configuration for protocol experiments that enable experimental
|
||||
/// features. Each one is associated with a matrix spec proposal, a list of
|
||||
/// which are published at https://spec.matrix.org/proposals/
|
||||
#[serde(default)]
|
||||
pub experiments: experiments::Experiments,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[allow(clippy::zero_sized_map_values)]
|
||||
// this is a catchall, the map shouldn't be zero at runtime
|
||||
@@ -2240,7 +2217,7 @@ struct ListeningAddr {
|
||||
addrs: Either<IpAddr, Vec<IpAddr>>,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str; 9] = &[
|
||||
const DEPRECATED_KEYS: &[&str; 11] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
"max_concurrent_requests",
|
||||
@@ -2250,6 +2227,8 @@ const DEPRECATED_KEYS: &[&str; 9] = &[
|
||||
"well_known_support_role",
|
||||
"well_known_support_email",
|
||||
"well_known_support_mxid",
|
||||
"enable_msc4284_policy_servers",
|
||||
"policy_server_check_own_events",
|
||||
];
|
||||
|
||||
impl Config {
|
||||
@@ -2442,8 +2421,6 @@ fn default_otlp_filter() -> String {
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn default_otlp_protocol() -> String { "http".to_owned() }
|
||||
|
||||
fn default_tracing_flame_output_path() -> String { "./tracing.folded".to_owned() }
|
||||
|
||||
fn default_trusted_servers() -> Vec<OwnedServerName> {
|
||||
|
||||
@@ -558,19 +558,12 @@ where
|
||||
// If type is m.room.power_levels
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
|
||||
debug!("starting m.room.power_levels check");
|
||||
let mut creators = BTreeSet::new();
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
creators.insert(create_event.sender().to_owned());
|
||||
for creator in room_create_content.additional_creators.iter().flatten() {
|
||||
creators.insert(creator.deserialize()?);
|
||||
}
|
||||
}
|
||||
|
||||
match check_power_levels(
|
||||
room_version,
|
||||
incoming_event,
|
||||
power_levels_event.as_ref(),
|
||||
sender_power_level,
|
||||
&creators,
|
||||
) {
|
||||
| Some(required_pwr_lvl) =>
|
||||
if !required_pwr_lvl {
|
||||
@@ -1228,8 +1221,8 @@ fn check_power_levels(
|
||||
power_event: &impl Event,
|
||||
previous_power_event: Option<&impl Event>,
|
||||
user_level: Int,
|
||||
creators: &BTreeSet<OwnedUserId>,
|
||||
) -> Option<bool> {
|
||||
// TODO(hydra): This function does not care about creators!
|
||||
match power_event.state_key() {
|
||||
| Some("") => {},
|
||||
| Some(key) => {
|
||||
@@ -1294,10 +1287,6 @@ fn check_power_levels(
|
||||
for user in user_levels_to_check {
|
||||
let old_level = old_state.users.get(user);
|
||||
let new_level = new_state.users.get(user);
|
||||
if new_level.is_some() && creators.contains(user) {
|
||||
warn!("creators cannot appear in the users list of m.room.power_levels");
|
||||
return Some(false); // cannot alter creator power level
|
||||
}
|
||||
if old_level.is_some() && new_level.is_some() && old_level == new_level {
|
||||
continue;
|
||||
}
|
||||
|
||||
+5
-9
@@ -63,16 +63,15 @@ standard = [
|
||||
"systemd",
|
||||
"url_preview",
|
||||
"zstd_compression",
|
||||
"sentry_telemetry",
|
||||
"otlp_telemetry",
|
||||
"console",
|
||||
"sentry_telemetry"
|
||||
]
|
||||
full = [
|
||||
"standard",
|
||||
# "hardened_malloc", # Conflicts with jemalloc
|
||||
"jemalloc_prof",
|
||||
"perf_measurements",
|
||||
"tokio_console",
|
||||
"tokio_console"
|
||||
# sentry_telemetry
|
||||
]
|
||||
|
||||
blurhashing = [
|
||||
@@ -125,15 +124,12 @@ ldap = [
|
||||
media_thumbnail = [
|
||||
"conduwuit-service/media_thumbnail",
|
||||
]
|
||||
otlp_telemetry = [
|
||||
perf_measurements = [
|
||||
"dep:opentelemetry",
|
||||
"dep:tracing-flame",
|
||||
"dep:tracing-opentelemetry",
|
||||
"dep:opentelemetry_sdk",
|
||||
"dep:opentelemetry-otlp",
|
||||
]
|
||||
perf_measurements = [
|
||||
"dep:tracing-flame",
|
||||
"otlp_telemetry",
|
||||
"conduwuit-core/perf_measurements",
|
||||
"conduwuit-core/sentry_telemetry",
|
||||
]
|
||||
|
||||
+30
-55
@@ -7,10 +7,8 @@ use conduwuit_core::{
|
||||
log::{ConsoleFormat, ConsoleWriter, LogLevelReloadHandles, capture, fmt_span},
|
||||
result::UnwrapOrErr,
|
||||
};
|
||||
#[cfg(feature = "otlp_telemetry")]
|
||||
#[cfg(feature = "perf_measurements")]
|
||||
use opentelemetry::trace::TracerProvider;
|
||||
#[cfg(feature = "otlp_telemetry")]
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt, layer::SubscriberExt, reload};
|
||||
|
||||
#[cfg(feature = "perf_measurements")]
|
||||
@@ -72,57 +70,6 @@ pub(crate) fn init(
|
||||
subscriber.with(sentry_layer.with_filter(sentry_reload_filter))
|
||||
};
|
||||
|
||||
#[cfg(feature = "otlp_telemetry")]
|
||||
let subscriber = {
|
||||
let otlp_filter = EnvFilter::try_new(&config.otlp_filter)
|
||||
.map_err(|e| err!(Config("otlp_filter", "{e}.")))?;
|
||||
|
||||
let otlp_layer = config.allow_otlp.then(|| {
|
||||
opentelemetry::global::set_text_map_propagator(
|
||||
opentelemetry_sdk::propagation::TraceContextPropagator::new(),
|
||||
);
|
||||
|
||||
let exporter = match config.otlp_protocol.as_str() {
|
||||
| "grpc" => opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
|
||||
.build()
|
||||
.expect("Failed to create OTLP gRPC exporter"),
|
||||
| "http" => opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_http()
|
||||
.build()
|
||||
.expect("Failed to create OTLP HTTP exporter"),
|
||||
| protocol => {
|
||||
debug_warn!(
|
||||
"Invalid OTLP protocol '{}', falling back to HTTP. Valid options are \
|
||||
'http' or 'grpc'.",
|
||||
protocol
|
||||
);
|
||||
opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_http()
|
||||
.build()
|
||||
.expect("Failed to create OTLP HTTP exporter")
|
||||
},
|
||||
};
|
||||
|
||||
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||
.with_batch_exporter(exporter)
|
||||
.build();
|
||||
|
||||
let tracer = provider.tracer(conduwuit_core::name());
|
||||
|
||||
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
|
||||
let (otlp_reload_filter, otlp_reload_handle) =
|
||||
reload::Layer::new(otlp_filter.clone());
|
||||
reload_handles.add("otlp", Box::new(otlp_reload_handle));
|
||||
|
||||
Some(telemetry.with_filter(otlp_reload_filter))
|
||||
});
|
||||
|
||||
subscriber.with(otlp_layer)
|
||||
};
|
||||
|
||||
#[cfg(feature = "perf_measurements")]
|
||||
let (subscriber, flame_guard) = {
|
||||
let (flame_layer, flame_guard) = if config.tracing_flame {
|
||||
@@ -142,7 +89,35 @@ pub(crate) fn init(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let subscriber = subscriber.with(flame_layer);
|
||||
let otlp_filter = EnvFilter::try_new(&config.otlp_filter)
|
||||
.map_err(|e| err!(Config("otlp_filter", "{e}.")))?;
|
||||
|
||||
let otlp_layer = config.allow_otlp.then(|| {
|
||||
opentelemetry::global::set_text_map_propagator(
|
||||
opentelemetry_sdk::propagation::TraceContextPropagator::new(),
|
||||
);
|
||||
|
||||
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_http()
|
||||
.build()
|
||||
.expect("Failed to create OTLP exporter");
|
||||
|
||||
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||
.with_batch_exporter(exporter)
|
||||
.build();
|
||||
|
||||
let tracer = provider.tracer(conduwuit_core::name());
|
||||
|
||||
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||
|
||||
let (otlp_reload_filter, otlp_reload_handle) =
|
||||
reload::Layer::new(otlp_filter.clone());
|
||||
reload_handles.add("otlp", Box::new(otlp_reload_handle));
|
||||
|
||||
Some(telemetry.with_filter(otlp_reload_filter))
|
||||
});
|
||||
|
||||
let subscriber = subscriber.with(flame_layer).with(otlp_layer);
|
||||
(subscriber, flame_guard)
|
||||
};
|
||||
|
||||
|
||||
@@ -66,10 +66,7 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
|
||||
.layer(RequestBodyTimeoutLayer::new(Duration::from_secs(
|
||||
server.config.client_receive_timeout,
|
||||
)))
|
||||
.layer(TimeoutLayer::with_status_code(
|
||||
StatusCode::REQUEST_TIMEOUT,
|
||||
Duration::from_secs(server.config.client_request_timeout),
|
||||
))
|
||||
.layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(server.config.client_request_timeout)))
|
||||
.layer(SetResponseHeaderLayer::if_not_present(
|
||||
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
|
||||
HeaderValue::from_static("?1"),
|
||||
|
||||
@@ -9,10 +9,7 @@ use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
|
||||
use termimad::MadSkin;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{
|
||||
Dep,
|
||||
admin::{self, InvocationSource},
|
||||
};
|
||||
use crate::{Dep, admin};
|
||||
|
||||
pub struct Console {
|
||||
server: Arc<Server>,
|
||||
@@ -163,11 +160,7 @@ impl Console {
|
||||
}
|
||||
|
||||
async fn process(self: Arc<Self>, line: String) {
|
||||
match self
|
||||
.admin
|
||||
.command_in_place(line, None, InvocationSource::Console)
|
||||
.await
|
||||
{
|
||||
match self.admin.command_in_place(line, None).await {
|
||||
| Ok(Some(ref content)) => self.output(content),
|
||||
| Err(ref content) => self.output_err(content),
|
||||
| _ => unreachable!(),
|
||||
|
||||
@@ -2,8 +2,6 @@ use conduwuit::{Err, Result, debug, debug_info, error, implement, info};
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
use crate::admin::InvocationSource;
|
||||
|
||||
pub(super) const SIGNAL: &str = "SIGUSR2";
|
||||
|
||||
/// Possibly spawn the terminal console at startup if configured.
|
||||
@@ -90,10 +88,7 @@ pub(super) async fn signal_execute(&self) -> Result {
|
||||
async fn execute_command(&self, i: usize, command: String) -> Result {
|
||||
debug!("Execute command #{i}: executing {command:?}");
|
||||
|
||||
match self
|
||||
.command_in_place(command, None, InvocationSource::Console)
|
||||
.await
|
||||
{
|
||||
match self.command_in_place(command, None).await {
|
||||
| Ok(Some(output)) => Self::execute_command_output(i, &output),
|
||||
| Err(output) => Self::execute_command_error(i, &output),
|
||||
| Ok(None) => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use conduwuit::{
|
||||
Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder, warn,
|
||||
};
|
||||
use conduwuit::{Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder};
|
||||
use ruma::{
|
||||
RoomId, UserId,
|
||||
events::{
|
||||
@@ -178,19 +176,6 @@ async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> R
|
||||
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
|
||||
use MembershipState::{Invite, Join, Knock, Leave};
|
||||
|
||||
if self
|
||||
.services
|
||||
.server
|
||||
.config
|
||||
.admins_list
|
||||
.contains(&user_id.to_owned())
|
||||
{
|
||||
warn!(
|
||||
"Revoking the admin status of {user_id} will not work correctly as they are within \
|
||||
the admins_list config."
|
||||
)
|
||||
}
|
||||
|
||||
let Ok(room_id) = self.get_admin_room().await else {
|
||||
return Err!(error!("No admin room available or created."));
|
||||
};
|
||||
|
||||
+61
-122
@@ -14,10 +14,10 @@ use conduwuit_core::{
|
||||
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
||||
};
|
||||
pub use create::create_admin_room;
|
||||
use futures::{Future, FutureExt, StreamExt, TryFutureExt};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use loole::{Receiver, Sender};
|
||||
use ruma::{
|
||||
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
|
||||
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, RoomId, UInt, UserId,
|
||||
events::{
|
||||
Mentions,
|
||||
room::{
|
||||
@@ -54,37 +54,15 @@ struct Services {
|
||||
media: Dep<crate::media::Service>,
|
||||
}
|
||||
|
||||
/// Inputs to a command are a multi-line string, invocation source, optional
|
||||
/// reply_id, and optional sender.
|
||||
/// Inputs to a command are a multi-line string, optional reply_id, and optional
|
||||
/// sender.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandInput {
|
||||
pub command: String,
|
||||
pub reply_id: Option<OwnedEventId>,
|
||||
pub source: InvocationSource,
|
||||
pub sender: Option<Box<UserId>>,
|
||||
}
|
||||
|
||||
/// Where a command is being invoked from.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InvocationSource {
|
||||
/// The server's private admin room
|
||||
AdminRoom,
|
||||
/// An escaped `\!admin` command in a public room
|
||||
EscapedCommand,
|
||||
/// The server's admin console
|
||||
Console,
|
||||
/// Some other trusted internal source
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl InvocationSource {
|
||||
/// Returns whether this invocation source allows "restricted"
|
||||
/// commands, i.e. ones that could be potentially dangerous if executed by
|
||||
/// an attacker or in a public room.
|
||||
#[must_use]
|
||||
pub fn allows_restricted(&self) -> bool { !matches!(self, Self::EscapedCommand) }
|
||||
}
|
||||
|
||||
/// Prototype of the tab-completer. The input is buffered text when tab
|
||||
/// asserted; the output will fully replace the input buffer.
|
||||
pub type Completer = fn(&str) -> String;
|
||||
@@ -298,15 +276,10 @@ impl Service {
|
||||
/// Posts a command to the command processor queue and returns. Processing
|
||||
/// will take place on the service worker's task asynchronously. Errors if
|
||||
/// the queue is full.
|
||||
pub fn command(
|
||||
&self,
|
||||
command: String,
|
||||
reply_id: Option<OwnedEventId>,
|
||||
source: InvocationSource,
|
||||
) -> Result<()> {
|
||||
pub fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result<()> {
|
||||
self.channel
|
||||
.0
|
||||
.send(CommandInput { command, reply_id, source, sender: None })
|
||||
.send(CommandInput { command, reply_id, sender: None })
|
||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||
}
|
||||
|
||||
@@ -317,17 +290,11 @@ impl Service {
|
||||
&self,
|
||||
command: String,
|
||||
reply_id: Option<OwnedEventId>,
|
||||
source: InvocationSource,
|
||||
sender: Box<UserId>,
|
||||
) -> Result<()> {
|
||||
self.channel
|
||||
.0
|
||||
.send(CommandInput {
|
||||
command,
|
||||
reply_id,
|
||||
source,
|
||||
sender: Some(sender),
|
||||
})
|
||||
.send(CommandInput { command, reply_id, sender: Some(sender) })
|
||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||
}
|
||||
|
||||
@@ -337,9 +304,8 @@ impl Service {
|
||||
&self,
|
||||
command: String,
|
||||
reply_id: Option<OwnedEventId>,
|
||||
source: InvocationSource,
|
||||
) -> ProcessorResult {
|
||||
self.process_command(CommandInput { command, reply_id, source, sender: None })
|
||||
self.process_command(CommandInput { command, reply_id, sender: None })
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -383,43 +349,16 @@ impl Service {
|
||||
handle(services, command).await
|
||||
}
|
||||
|
||||
/// Returns the list of admins for this server. First loads
|
||||
/// the admin_list from the configuration, then adds users from
|
||||
/// the admin room if applicable.
|
||||
pub async fn get_admins(&self) -> Vec<OwnedUserId> {
|
||||
let mut generated_admin_list: Vec<OwnedUserId> =
|
||||
self.services.server.config.admins_list.clone();
|
||||
|
||||
if self.services.server.config.admins_from_room {
|
||||
if let Ok(admin_room) = self.get_admin_room().await {
|
||||
let admin_users = self.services.state_cache.room_members(&admin_room);
|
||||
let mut stream = admin_users;
|
||||
|
||||
while let Some(user_id) = stream.next().await {
|
||||
generated_admin_list.push(user_id.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generated_admin_list
|
||||
}
|
||||
|
||||
/// Checks whether a given user is an admin of this server
|
||||
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
|
||||
if self.services.server.config.admins_list.contains(user_id) {
|
||||
return true;
|
||||
}
|
||||
let Ok(admin_room) = self.get_admin_room().await else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if self.services.server.config.admins_from_room {
|
||||
if let Ok(admin_room) = self.get_admin_room().await {
|
||||
self.services
|
||||
.state_cache
|
||||
.is_joined(user_id, &admin_room)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
self.services
|
||||
.state_cache
|
||||
.is_joined(user_id, &admin_room)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets the room ID of the admin room
|
||||
@@ -520,59 +459,59 @@ impl Service {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> Option<InvocationSource>
|
||||
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
|
||||
where
|
||||
E: Event + Send + Sync,
|
||||
{
|
||||
// If the user isn't an admin they definitely can't run admin commands
|
||||
if !self.user_is_admin(event.sender()).await {
|
||||
return None;
|
||||
// Server-side command-escape with public echo
|
||||
let is_escape = body.starts_with('\\');
|
||||
let is_public_escape = is_escape && body.trim_start_matches('\\').starts_with("!admin");
|
||||
|
||||
// Admin command with public echo (in admin room)
|
||||
let server_user = &self.services.globals.server_user;
|
||||
let is_public_prefix =
|
||||
body.starts_with("!admin") || body.starts_with(server_user.as_str());
|
||||
|
||||
// Expected backward branch
|
||||
if !is_public_escape && !is_public_prefix {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(room_id) = event.room_id()
|
||||
&& self.is_admin_room(room_id).await
|
||||
let user_is_local = self.services.globals.user_is_local(event.sender());
|
||||
|
||||
// only allow public escaped commands by local admins
|
||||
if is_public_escape && !user_is_local {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if server-side command-escape is disabled by configuration
|
||||
if is_public_escape && !self.services.server.config.admin_escape_commands {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent unescaped !admin from being used outside of the admin room
|
||||
if event.room_id().is_some()
|
||||
&& is_public_prefix
|
||||
&& !self.is_admin_room(event.room_id().unwrap()).await
|
||||
{
|
||||
// This is a message in the admin room
|
||||
|
||||
// Ignore messages which aren't admin commands
|
||||
let server_user = &self.services.globals.server_user;
|
||||
if !(body.starts_with("!admin") || body.starts_with(server_user.as_str())) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ignore messages from the server user _unless_ the emergency password is set
|
||||
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
||||
if event.sender() == server_user && !emergency_password_set {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Looks good
|
||||
Some(InvocationSource::AdminRoom)
|
||||
} else {
|
||||
// This is a message outside the admin room
|
||||
|
||||
// Is it an escaped admin command? i.e. `\!admin --help`
|
||||
let is_public_escape =
|
||||
body.starts_with('\\') && body.trim_start_matches('\\').starts_with("!admin");
|
||||
|
||||
// Ignore the message if it's not
|
||||
if !is_public_escape {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Only admin users belonging to this server can use escaped commands
|
||||
if !self.services.globals.user_is_local(event.sender()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if escaped commands are disabled in the config
|
||||
if !self.services.server.config.admin_escape_commands {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Looks good
|
||||
Some(InvocationSource::EscapedCommand)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only senders who are admin can proceed
|
||||
if !self.user_is_admin(event.sender()).await {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This will evaluate to false if the emergency password is set up so that
|
||||
// the administrator can execute commands as the server user
|
||||
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
||||
let from_server = event.sender() == server_user && !emergency_password_set;
|
||||
if from_server && self.is_admin_room(event.room_id().unwrap()).await {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Authentic admin command
|
||||
true
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
+15
-15
@@ -427,20 +427,20 @@ impl Service {
|
||||
}
|
||||
|
||||
let d = vec![device];
|
||||
let mut notify = Notification::new(d);
|
||||
let mut notifi = Notification::new(d);
|
||||
|
||||
notify.event_id = Some(event.event_id().to_owned());
|
||||
notify.room_id = Some(event.room_id().unwrap().to_owned());
|
||||
notifi.event_id = Some(event.event_id().to_owned());
|
||||
notifi.room_id = Some(event.room_id().unwrap().to_owned());
|
||||
if http
|
||||
.data
|
||||
.get("org.matrix.msc4076.disable_badge_count")
|
||||
.is_none() && http.data.get("disable_badge_count").is_none()
|
||||
{
|
||||
notify.counts = NotificationCounts::new(unread, uint!(0));
|
||||
notifi.counts = NotificationCounts::new(unread, uint!(0));
|
||||
} else {
|
||||
// counts will not be serialised if it's the default (0, 0)
|
||||
// skip_serializing_if = "NotificationCounts::is_default"
|
||||
notify.counts = NotificationCounts::default();
|
||||
notifi.counts = NotificationCounts::default();
|
||||
}
|
||||
|
||||
if !event_id_only {
|
||||
@@ -449,30 +449,30 @@ impl Service {
|
||||
.iter()
|
||||
.any(|t| matches!(t, Tweak::Highlight(true) | Tweak::Sound(_)))
|
||||
{
|
||||
notify.prio = NotificationPriority::High;
|
||||
notifi.prio = NotificationPriority::High;
|
||||
} else {
|
||||
notify.prio = NotificationPriority::Low;
|
||||
notifi.prio = NotificationPriority::Low;
|
||||
}
|
||||
notify.sender = Some(event.sender().to_owned());
|
||||
notify.event_type = Some(event.kind().to_owned());
|
||||
notify.content = serde_json::value::to_raw_value(event.content()).ok();
|
||||
notifi.sender = Some(event.sender().to_owned());
|
||||
notifi.event_type = Some(event.kind().to_owned());
|
||||
notifi.content = serde_json::value::to_raw_value(event.content()).ok();
|
||||
|
||||
if *event.kind() == TimelineEventType::RoomMember {
|
||||
notify.user_is_target =
|
||||
notifi.user_is_target =
|
||||
event.state_key() == Some(event.sender().as_str());
|
||||
}
|
||||
|
||||
notify.sender_display_name =
|
||||
notifi.sender_display_name =
|
||||
self.services.users.displayname(event.sender()).await.ok();
|
||||
|
||||
notify.room_name = self
|
||||
notifi.room_name = self
|
||||
.services
|
||||
.state_accessor
|
||||
.get_name(event.room_id().unwrap())
|
||||
.await
|
||||
.ok();
|
||||
|
||||
notify.room_alias = self
|
||||
notifi.room_alias = self
|
||||
.services
|
||||
.state_accessor
|
||||
.get_canonical_alias(event.room_id().unwrap())
|
||||
@@ -480,7 +480,7 @@ impl Service {
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.send_request(&http.url, send_event_notification::v1::Request::new(notify))
|
||||
self.send_request(&http.url, send_event_notification::v1::Request::new(notifi))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -19,8 +19,7 @@ use futures::{
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, UserId,
|
||||
events::{
|
||||
AnyStrippedStateEvent, StateEventType, TimelineEventType,
|
||||
room::create::RoomCreateEventContent,
|
||||
AnyStateEvent, StateEventType, TimelineEventType, room::create::RoomCreateEventContent,
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
@@ -307,12 +306,22 @@ impl Service {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a summary of the room state for invites and knock responses.
|
||||
///
|
||||
/// This used to return stripped state, but now returns complete events.
|
||||
///
|
||||
/// Returns:
|
||||
///
|
||||
/// - m.room.create
|
||||
/// - m.room.join_rules
|
||||
/// - m.room.canonical_alias
|
||||
/// - m.room.name
|
||||
/// - m.room.avatar
|
||||
/// - m.room.member (of the event sender)
|
||||
/// - m.room.encryption
|
||||
/// - m.room.topic
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
pub async fn summary_stripped<'a, E>(
|
||||
&self,
|
||||
event: &'a E,
|
||||
room_id: &RoomId,
|
||||
) -> Vec<Raw<AnyStrippedStateEvent>>
|
||||
pub async fn summary<'a, E>(&self, event: &'a E, room_id: &RoomId) -> Vec<Raw<AnyStateEvent>>
|
||||
where
|
||||
E: Event + Send + Sync,
|
||||
&'a E: Event + Send,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use conduwuit::{Err, Event, Pdu, Result, implement, is_not_empty, utils::ReadyExt, warn};
|
||||
use conduwuit::{Err, Event, Pdu, Result, implement, is_not_empty, utils::ReadyExt};
|
||||
use database::{Json, serialize_key};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
OwnedServerName, RoomId, UserId,
|
||||
CanonicalJsonValue, OwnedServerName, RoomId, UserId,
|
||||
events::{
|
||||
AnyStrippedStateEvent, GlobalAccountDataEventType, RoomAccountDataEventType,
|
||||
StateEventType,
|
||||
@@ -334,7 +334,7 @@ pub async fn mark_as_invited(
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
sender_user: &UserId,
|
||||
last_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
|
||||
last_state: Option<Vec<CanonicalJsonValue>>,
|
||||
invite_via: Option<Vec<OwnedServerName>>,
|
||||
) -> Result<()> {
|
||||
// return an error for blocked invites. ignored invites aren't handled here
|
||||
|
||||
@@ -335,11 +335,10 @@ where
|
||||
if let Some(body) = content.body {
|
||||
self.services.search.index_pdu(shortroomid, &pdu_id, &body);
|
||||
|
||||
if let Some(source) = self.services.admin.is_admin_command(pdu, &body).await {
|
||||
if self.services.admin.is_admin_command(pdu, &body).await {
|
||||
self.services.admin.command_with_sender(
|
||||
body,
|
||||
Some((pdu.event_id()).into()),
|
||||
source,
|
||||
pdu.sender.clone().into(),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use serde_json::value::{RawValue, to_raw_value};
|
||||
|
||||
use super::RoomMutexGuard;
|
||||
|
||||
pub fn pdu_fits(owned_obj: &mut CanonicalJsonObject) -> bool {
|
||||
pub fn pdu_fits(owned_obj: &CanonicalJsonObject) -> bool {
|
||||
// room IDs, event IDs, senders, types, and state keys must all be <= 255 bytes
|
||||
if let Some(CanonicalJsonValue::String(room_id)) = owned_obj.get("room_id") {
|
||||
if room_id.len() > 255 {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[tool.towncrier]
|
||||
name = "Continuwuity"
|
||||
directory = "changelog.d"
|
||||
filename = "CHANGELOG.md"
|
||||
Reference in New Issue
Block a user