Compare commits

..

1 Commits

Author SHA1 Message Date
Ginger
77ae79396f feat: Use OptimisticTransactionDB for the database engine 2025-12-16 09:49:13 -05:00
171 changed files with 1282 additions and 6963 deletions

View File

@@ -32,13 +32,11 @@ outputs:
runs:
using: composite
steps:
- run: mkdir -p digests
shell: bash
- name: Download digests
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/download-artifact@v4
with:
path: digests
path: /tmp/digests
pattern: ${{ inputs.digest_pattern }}
merge-multiple: true
@@ -64,7 +62,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 +70,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:
@@ -80,7 +78,7 @@ runs:
- name: Create manifest list and push
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
working-directory: digests
working-directory: /tmp/digests
shell: bash
env:
IMAGES: ${{ inputs.images }}

View File

@@ -54,7 +54,7 @@ runs:
run: mv /tmp/binaries/sbin/conduwuit /tmp/binaries/conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
- name: Upload binary artifact
uses: forgejo/upload-artifact@v4
uses: forgejo/upload-artifact@v3
with:
name: conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
path: /tmp/binaries/conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
@@ -62,7 +62,7 @@ runs:
- name: Upload digest
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/upload-artifact@v4
uses: forgejo/upload-artifact@v3
with:
name: digests${{ inputs.digest_suffix }}-${{ inputs.slug }}${{ inputs.cpu_suffix }}
path: /tmp/digests/*

View File

@@ -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

View File

@@ -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
@@ -126,7 +127,7 @@ jobs:
[ -f /etc/conduwuit/conduwuit.toml ] && echo "✅ Config file installed"
- name: Upload deb artifact
uses: forgejo/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: continuwuity-${{ steps.debian-version.outputs.distribution }}
path: ${{ steps.cargo-deb.outputs.path }}

View File

@@ -239,13 +239,13 @@ jobs:
cp $BIN_RPM upload-bin/
- name: Upload binary RPM
uses: forgejo/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: continuwuity
path: upload-bin/
- name: Upload debug RPM artifact
uses: forgejo/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: continuwuity-debug
path: artifacts/*debuginfo*.rpm

View File

@@ -109,7 +109,7 @@ jobs:
cat ./element-web/webapp/config.json
- name: 📤 Upload Artifact
uses: forgejo/upload-artifact@v4
uses: forgejo/upload-artifact@v3
with:
name: element-web
path: ./element-web/webapp/

View File

@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:42.70.2@sha256:3c2ac1b94fa92ef2fa4d1a0493f2c3ba564454720a32fdbcac2db2846ff1ee47
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
options: --tmpfs /tmp:exec
steps:
- name: Checkout

View File

@@ -23,7 +23,7 @@ jobs:
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
- uses: https://github.com/cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
- uses: https://github.com/cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0
with:
nix_path: nixpkgs=channel:nixos-unstable

View File

@@ -2,7 +2,6 @@ AlexPewMaster <git@alex.unbox.at> <68469103+AlexPewMaster@users.noreply.github.c
Daniel Wiesenberg <weasy@hotmail.de> <weasy666@gmail.com>
Devin Ragotzy <devin.ragotzy@gmail.com> <d6ragotzy@wmich.edu>
Devin Ragotzy <devin.ragotzy@gmail.com> <dragotzy7460@mail.kvcc.edu>
Ginger <ginger@gingershaped.computer> <75683114+gingershaped@users.noreply.github.com>
Jonas Platte <jplatte+git@posteo.de> <jplatte+gitlab@posteo.de>
Jonas Zohren <git-pbkyr@jzohren.de> <gitlab-jfowl-0ux98@sh14.de>
Jonathan de Jong <jonathan@automatia.nl> <jonathandejong02@gmail.com>
@@ -13,6 +12,5 @@ Olivia Lee <olivia@computer.surgery> <benjamin@computer.surgery>
Rudi Floren <rudi.floren@gmail.com> <rudi.floren@googlemail.com>
Tamara Schmitz <tamara.zoe.schmitz@posteo.de> <15906939+tamara-schmitz@users.noreply.github.com>
Timo Kösters <timo@koesters.xyz>
nexy7574 <git@nexy7574.co.uk> <nex@noreply.forgejo.ellis.link>
nexy7574 <git@nexy7574.co.uk> <nex@noreply.localhost>
x4u <xi.zhu@protonmail.ch> <14617923-x4u@users.noreply.gitlab.com>
Ginger <ginger@gingershaped.computer> <75683114+gingershaped@users.noreply.github.com>

View File

@@ -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

View File

@@ -24,4 +24,3 @@ extend-ignore-re = [
"continuwuity" = "continuwuity"
"continuwity" = "continuwuity"
"execuse" = "execuse"
"oltp" = "OTLP"

View File

@@ -1,37 +0,0 @@
# Continuwuity 0.5.2 (2026-01-09)
## Features
- Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration tokens. Contributed by @ginger (#783).
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. (#1253)
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam. Contributed by @nex. (#1263)
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. (#1266)
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. (#1271)
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (#1272)
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs. Contributed by @nex. (#1277)
- Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console. Contributed by @ginger.
## Bugfixes
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. (#1257)
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now also concurrent. Contributed by @nex. (#1261)
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by @nex. (#1276)
## Misc
- The `console` feature is now enabled by default, allowing the server console to be used for running admin commands directly. To automatically open the console on startup, set the `admin_console_automatic` config option to `true`. Contributed by @ginger.
- We now (finally) document our container image mirrors. Contributed by @Jade
# 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)

84
Cargo.lock generated
View File

@@ -940,7 +940,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"conduwuit_admin",
@@ -972,7 +972,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"conduwuit_api",
@@ -994,7 +994,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"async-trait",
"axum 0.7.9",
@@ -1027,14 +1027,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"built",
]
[[package]]
name = "conduwuit_core"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"argon2",
"arrayvec",
@@ -1095,7 +1095,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1114,7 +1114,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1124,7 +1124,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"axum 0.7.9",
"axum-client-ip",
@@ -1159,7 +1159,7 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1200,7 +1200,7 @@ dependencies = [
[[package]]
name = "conduwuit_web"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"askama",
"axum 0.7.9",
@@ -1632,16 +1632,6 @@ dependencies = [
"litrs",
]
[[package]]
name = "draupnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]]
name = "dtor"
version = "0.1.0"
@@ -1760,7 +1750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -2415,7 +2405,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -2992,16 +2982,6 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "meowlnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -3141,7 +3121,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3325,8 +3305,6 @@ dependencies = [
"prost",
"reqwest",
"thiserror 2.0.17",
"tokio",
"tonic",
"tracing",
]
@@ -3769,7 +3747,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.1",
"socket2 0.5.10",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -3806,9 +3784,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"socket2 0.5.10",
"tracing",
"windows-sys 0.60.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4085,13 +4063,11 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"assign",
"draupnir-antispam",
"js_int",
"js_option",
"meowlnir-antispam",
"ruma-appservice-api",
"ruma-client-api",
"ruma-common",
@@ -4107,7 +4083,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"js_int",
"ruma-common",
@@ -4119,7 +4095,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"as_variant",
"assign",
@@ -4142,7 +4118,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4174,7 +4150,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"as_variant",
"indexmap",
@@ -4199,7 +4175,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"bytes",
"headers",
@@ -4221,7 +4197,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"js_int",
"thiserror 2.0.17",
@@ -4230,7 +4206,7 @@ dependencies = [
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"js_int",
"ruma-common",
@@ -4240,7 +4216,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -4255,7 +4231,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"js_int",
"ruma-common",
@@ -4267,7 +4243,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=f9e74cb206cfa45cf5f17d39282253b43a15fcd5#f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
@@ -4347,7 +4323,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -6228,7 +6204,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"serde",
@@ -6237,7 +6213,7 @@ dependencies = [
[[package]]
name = "xtask-generate-commands"
version = "0.5.2"
version = "0.5.0-rc.8.1"
dependencies = [
"clap-markdown",
"clap_builder",

View File

@@ -1,18 +1,27 @@
#cargo-features = ["profile-rustflags"]
[workspace]
resolver = "2"
members = ["src/*", "xtask/*"]
default-members = ["src/*"]
[workspace.package]
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
authors = [
"June Clementine Strawberry <june@girlboss.ceo>",
"strawberry <strawberry@puppygock.gay>", # woof
"Jason Volk <jason@zemos.net>",
]
categories = ["network-programming"]
description = "a very cool Matrix chat homeserver written in Rust"
edition = "2024"
homepage = "https://continuwuity.org/"
keywords = ["chat", "matrix", "networking", "server", "uwu"]
license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.2"
rust-version = "1.86.0"
version = "0.5.0-rc.8.1"
[workspace.metadata.crane]
name = "conduwuit"
@@ -24,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]
@@ -87,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]
@@ -140,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]
@@ -179,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
@@ -220,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]
@@ -238,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]
@@ -271,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
@@ -301,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
@@ -342,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 = "f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
rev = "50b2a91b2ab8f9830eea80b9911e11234e0eac66"
features = [
"compat",
"rand",
@@ -372,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]
@@ -386,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]
@@ -417,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"]
@@ -449,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"
@@ -482,9 +491,9 @@ default-features = false
version = "0.1.2"
default-features = false
features = [
"static",
"gcc",
"light",
"static",
"gcc",
"light",
]
[workspace.dependencies.rustyline-async]
@@ -839,8 +848,6 @@ unknown_lints = "allow"
###################
cargo = { level = "warn", priority = -1 }
# Nobody except for us should be consuming these crates, they don't need metadata
cargo_common_metadata = { level = "allow" }
## some sadness
multiple_crate_versions = { level = "allow", priority = 1 }

View File

@@ -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.
@@ -340,9 +340,7 @@
# this to be high to account for extremely large room joins, slow
# homeservers, your own resources etc.
#
# Joins have 6x the timeout.
#
#federation_timeout = 60
#federation_timeout = 300
# MSC4284 Policy server request timeout (seconds). Generally policy
# servers should respond near instantly, however may slow down under
@@ -391,15 +389,7 @@
#
#appservice_idle_timeout = 300
# Notification gateway pusher request connection timeout (seconds).
#
#pusher_conn_timeout = 15
# Notification gateway pusher total request timeout (seconds).
#
#pusher_timeout = 60
# Notification gateway pusher idle connection pool timeout (seconds).
# Notification gateway pusher idle connection pool timeout.
#
#pusher_idle_timeout = 15
@@ -431,7 +421,7 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
#
# If you would like registration only via token reg, please configure
# `registration_token`.
# `registration_token` or `registration_token_file`.
#
#allow_registration = false
@@ -462,13 +452,22 @@
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
# to true to allow open registration without any conditions.
#
# If you do not want to set a static token, the `!admin token` commands
# may also be used to manage registration tokens.
# YOU NEED TO EDIT THIS OR USE registration_token_file.
#
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
#
#registration_token =
# Path to a file on the system that gets read for additional registration
# tokens. Multiple tokens can be added if you separate them with
# whitespace
#
# continuwuity must be able to access the file, and it must not be empty
#
# example: "/etc/continuwuity/.reg_token"
#
#registration_token_file =
# The public site key for reCaptcha. If this is provided, reCaptcha
# becomes required during registration. If both captcha *and*
# registration token are enabled, both will be required during
@@ -587,13 +586,10 @@
#allow_unstable_room_versions = true
# Default room version continuwuity will create rooms with.
# Note that this has to be a string since the room version is a string
# rather than an integer. Forgetting the quotes will make the server fail
# to start!
#
# Per spec, room version "11" is the default.
# Per spec, room version 11 is the default.
#
#default_room_version = "11"
#default_room_version = 11
# Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
# Jaeger exporter. Traces will be sent via OTLP to a collector (such as
@@ -609,11 +605,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],
@@ -1456,11 +1447,6 @@
#
#url_preview_max_spider_size = 256000
# Total request timeout for URL previews (seconds). This includes
# connection, request, and response body reading time.
#
#url_preview_timeout = 120
# Option to decide whether you would like to run the domain allowlist
# checks (contains and explicit) on the root domain or not. Does not apply
# to URL contains allowlist. Defaults to false.
@@ -1544,7 +1530,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
@@ -1562,8 +1548,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 = []
@@ -1596,18 +1581,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.
#
@@ -1653,7 +1626,7 @@
# Enable the tokio-console. This option is only relevant to developers.
#
# For more information, see:
# For more information, see:
# https://continuwuity.org/development.html#debugging-with-tokio-console
#
#tokio_console = false
@@ -1929,41 +1902,3 @@
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
#
#admin_filter = ""
[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in antispam->secret. Required for
# continuwuity to talk to Meowlnir.
#
#secret =
# The management room for which to send requests
#
#management_room =
# If enabled run all federated join attempts (both federated and local)
# through the Meowlnir anti-spam checks.
#
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
# restricted join rule are checked.
#
#check_all_joins = false
[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in
# web->synapseHTTPAntispam->authorization
#
#secret =

View File

@@ -48,11 +48,11 @@ 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
ENV LDDTREE_VERSION=0.4.0
ENV LDDTREE_VERSION=0.3.7
# renovate: datasource=crate depName=timelord-cli
ENV TIMELORD_VERSION=3.0.1

View File

@@ -18,11 +18,11 @@ 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
ENV LDDTREE_VERSION=0.4.0
ENV LDDTREE_VERSION=0.3.7
# Install unpackaged tools
RUN <<EOF

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -11,10 +11,10 @@ ### Use a registry
| Registry | Image | Notes |
| --------------- | --------------------------------------------------------------- | -----------------------|
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest][fj] | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main][fj] | Main branch image. |
[fj]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity
Use
@@ -24,15 +24,6 @@ ### Use a registry
to pull it to your machine.
#### Mirrors
Images are mirrored to multiple locations automatically, on a schedule:
- `ghcr.io/continuwuity/continuwuity`
- `docker.io/jadedblueeyes/continuwuity`
- `registry.gitlab.com/continuwuity/continuwuity`
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
### Run
When you have the image, you can simply run it with
@@ -58,7 +49,7 @@ ### Run
flag, which cleans up everything related to your container after you stop
it.
### Docker Compose
### Docker-compose
If the `docker run` command is not suitable for you or your setup, you can also use one
of the provided `docker-compose` files.
@@ -167,19 +158,8 @@ # Build for the current platform and load into the local Docker daemon
# Example: Build for specific platforms and push to a registry.
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
# Example: Build binary optimised for the current CPU (standard release profile)
# docker buildx build --load \
# --tag continuwuity:latest \
# --build-arg TARGET_CPU=native \
# -f docker/Dockerfile .
# Example: Build maxperf variant (release-max-perf profile with LTO)
# Optimised for runtime performance and smaller binary size, but requires longer build time
# docker buildx build --load \
# --tag continuwuity:latest-maxperf \
# --build-arg TARGET_CPU=native \
# --build-arg RUST_PROFILE=release-max-perf \
# -f docker/Dockerfile .
# Example: Build binary optimized for the current CPU
# docker buildx build --load --tag continuwuity:latest --build-arg TARGET_CPU=native -f docker/Dockerfile .
```
Refer to the Docker Buildx documentation for more advanced build options.
@@ -218,3 +198,5 @@ ### Use Traefik as Proxy
## Voice communication
See the [TURN](../turn.md) page.
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage

View File

@@ -8,39 +8,29 @@ # Generic deployment documentation
## Installing Continuwuity
### Prebuilt binary
### Static prebuilt binary
Download the binary for your architecture (x86_64 or aarch64) -
run the `uname -m` to check which you need.
You may simply download the binary that fits your machine architecture (x86_64
or aarch64). Run `uname -m` to see what you need.
Prebuilt binaries are available from:
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
- **Development builds**: CI artifacts from the `main` branch
(includes Debian/Ubuntu packages)
You can download prebuilt fully static musl binaries from the latest tagged
release [here](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest) or
from the `main` CI branch workflow artifact output. These also include Debian/Ubuntu
packages.
When browsing CI artifacts, `ci-bins` contains binaries organised
by commit hash, while `releases` contains tagged versions. Sort
by last modified date to find the most recent builds.
You can download these directly using curl. The `ci-bins` are CI workflow binaries organized by commit
hash/revision, and `releases` are tagged releases. Sort by descending last
modified date to find the latest.
The binaries require jemalloc and io_uring on the host system. Currently
we can't cross-build static binaries - contributions are welcome here.
These binaries have jemalloc and io_uring statically linked and included with
them, so no additional dynamic dependencies need to be installed.
#### Performance-optimised builds
For x86_64 systems with CPUs from the last ~15 years, use the
`-haswell-` optimised binaries for best performance. These
binaries enable hardware-accelerated CRC32 checksumming in
RocksDB, which significantly improves database performance.
The haswell instruction set provides an excellent balance of
compatibility and speed.
If you're using Docker instead, equivalent performance-optimised
images are available with the `-maxperf` suffix (e.g.
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
These images use the `release-max-perf`
build profile with
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
and, for amd64, target the haswell CPU architecture.
For the **best** performance: if you are using an `x86_64` CPU made in the last ~15 years,
we recommend using the `-haswell-` optimized binaries. These set
`-march=haswell`, which provides the most compatible and highest performance with
optimized binaries. The database backend, RocksDB, benefits most from this as it
uses hardware-accelerated CRC32 hashing/checksumming, which is critical
for performance.
### Compiling

View File

@@ -128,7 +128,7 @@ ### Log Levels
```rs
// Good
error!(
error = ?err,
error = %err,
room_id = %room_id,
"Failed to send event to room"
);
@@ -264,7 +264,7 @@ ### Code Comments
warn!(
destination = %destination,
attempt = attempt,
error = ?err,
error = %err,
retry_delay_ms = retry_delay.as_millis(),
"Federation request failed, retrying"
);

View File

@@ -149,12 +149,11 @@ ### Creating pull requests
*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 @@ ### Creating pull requests
[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

View File

@@ -6,10 +6,12 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 7,
"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"
"id": 3,
"message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner."
},
{
"id": 5,
"message": "It's a bird! It's a plane! No, it's 0.5.0-rc.8.1!\n\nThis is a minor bugfix update to the rc8 which backports some important fixes from the latest main branch. If you still haven't updated to rc8, you should skip to main. Otherwise, you should upgrade to this bugfix release as soon as possible.\n\nBugfixes backported to this version:\n\n- Resolved several issues with state resolution v2.1 (room version 12)\n- Fixed issues with the `restricted` and `knock_restricted` join rules that would sometimes incorrectly disallow a valid join\n- Fixed the automatic support contact listing being a no-op\n- Fixed upgrading pre-v12 rooms to v12 rooms\n- Fixed policy servers sending the incorrect JSON objects (resulted in false positives)\n- Fixed debug build panic during MSC4133 migration\n\nIt is recommended, if you can and are comfortable with doing so, following updates to the main branch - we're in the run up to the full 0.5.0 release, and more and more bugfixes and new features are being pushed constantly. Please don't forget to join [#announcements:continuwuity.org](https://matrix.to/#/#announcements:continuwuity.org) to receive this news faster and be alerted to other important updates!"
}
]
}

View File

@@ -8,7 +8,7 @@ # Command-Line Help for `continuwuity`
## `continuwuity`
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
a very cool Matrix chat homeserver written in Rust
**Usage:** `continuwuity [OPTIONS]`

54
flake.lock generated
View File

@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1766324728,
"narHash": "sha256-9C+WyE5U3y5w4WQXxmb0ylRyMMsPyzxielWXSHrcDpE=",
"lastModified": 1761112158,
"narHash": "sha256-RIXu/7eyKpQHjsPuAUODO81I4ni8f+WYSb7K4mTG6+0=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "c88b88c62bda077be8aa621d4e89d8701e39cb5d",
"rev": "58f3aaec0e1776f4a900737be8cd7cb00972210d",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1766194365,
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
"lastModified": 1760924934,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1766299592,
"narHash": "sha256-7u+q5hexu2eAxL2VjhskHvaUKg+GexmelIR2ve9Nbb4=",
"lastModified": 1761115517,
"narHash": "sha256-Fev/ag/c3Fp3JBwHfup3lpA5FlNXfkoshnQ7dssBgJ0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "381579dee168d5ced412e2990e9637ecc7cf1c5d",
"rev": "320433651636186ea32b387cff05d6bbfa30cea7",
"type": "github"
},
"original": {
@@ -55,11 +55,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1765121682,
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -74,11 +74,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1765835352,
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
"lastModified": 1760948891,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"lastModified": 1760878510,
"narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67",
"type": "github"
},
"original": {
@@ -105,11 +105,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1765674936,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"lastModified": 1754788789,
"narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"rev": "a73b9c743612e4244d865a2fdee11865283c04e6",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1766253897,
"narHash": "sha256-ChK07B1aOlJ4QzWXpJo+y8IGAxp1V9yQ2YloJ+RgHRw=",
"lastModified": 1761077270,
"narHash": "sha256-O1uTuvI/rUlubJ8AXKyzh1WSWV3qCZX0huTFUvWLN4E=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "765b7bdb432b3740f2d564afccfae831d5a972e4",
"rev": "39990a923c8bca38f5bd29dc4c96e20ee7808d5d",
"type": "github"
},
"original": {
@@ -153,11 +153,11 @@
]
},
"locked": {
"lastModified": 1766000401,
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
"lastModified": 1760945191,
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
"type": "github"
},
"original": {

View File

@@ -6,69 +6,6 @@
pkgs,
...
}:
let
baseTestScript =
pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; }
''
import asyncio
import nio
async def main() -> None:
# Connect to continuwuity
client = nio.AsyncClient("http://continuwuity:6167", "alice")
# Register as user alice
response = await client.register("alice", "my-secret-password")
# Log in as user alice
response = await client.login("my-secret-password")
# Create a new room
response = await client.room_create(federate=False)
print("Matrix room create response:", response)
assert isinstance(response, nio.RoomCreateResponse)
room_id = response.room_id
# Join the room
response = await client.join(room_id)
print("Matrix join response:", response)
assert isinstance(response, nio.JoinResponse)
# Send a message to the room
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": "Hello continuwuity!"
}
)
print("Matrix room send response:", response)
assert isinstance(response, nio.RoomSendResponse)
# Sync responses
response = await client.sync(timeout=30000)
print("Matrix sync response:", response)
assert isinstance(response, nio.SyncResponse)
# Check the message was received by continuwuity
last_message = response.rooms.join[room_id].timeline.events[-1].body
assert last_message == "Hello continuwuity!"
# Leave the room
response = await client.room_leave(room_id)
print("Matrix room leave response:", response)
assert isinstance(response, nio.RoomLeaveResponse)
# Close the client
await client.close()
if __name__ == "__main__":
asyncio.run(main())
'';
in
{
# run some nixos tests as checks
checks = lib.pipe self'.packages [
@@ -81,69 +18,106 @@
# this test was initially yoinked from
#
# https://github.com/NixOS/nixpkgs/blob/960ce26339661b1b69c6f12b9063ca51b688615f/nixos/tests/matrix/continuwuity.nix
(builtins.concatMap (
name:
builtins.map
(
{ config, suffix }:
{
name = "test-${name}-${suffix}";
value = pkgs.testers.runNixOSTest {
inherit name;
(builtins.map (name: {
name = "test-${name}";
value = pkgs.testers.runNixOSTest {
inherit name;
nodes = {
continuwuity = {
services.matrix-continuwuity = {
enable = true;
package = self'.packages.${name};
settings = config;
extraEnvironment.RUST_BACKTRACE = "yes";
};
networking.firewall.allowedTCPPorts = [ 6167 ];
};
client.environment.systemPackages = [ baseTestScript ];
};
testScript = ''
start_all()
with subtest("start continuwuity"):
continuwuity.wait_for_unit("continuwuity.service")
continuwuity.wait_for_open_port(6167)
with subtest("ensure messages can be exchanged"):
client.succeed("${lib.getExe baseTestScript} >&2")
'';
};
}
)
[
{
suffix = "base";
config = {
global = {
nodes = {
continuwuity = {
services.matrix-continuwuity = {
enable = true;
package = self'.packages.${name};
settings.global = {
server_name = name;
address = [ "0.0.0.0" ];
allow_registration = true;
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
};
extraEnvironment.RUST_BACKTRACE = "yes";
};
}
{
suffix = "with-room-version";
config = {
global = {
server_name = name;
address = [ "0.0.0.0" ];
allow_registration = true;
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
default_room_version = "12";
};
networking.firewall.allowedTCPPorts = [ 6167 ];
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [
(pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
import asyncio
import nio
async def main() -> None:
# Connect to continuwuity
client = nio.AsyncClient("http://continuwuity:6167", "alice")
# Register as user alice
response = await client.register("alice", "my-secret-password")
# Log in as user alice
response = await client.login("my-secret-password")
# Create a new room
response = await client.room_create(federate=False)
print("Matrix room create response:", response)
assert isinstance(response, nio.RoomCreateResponse)
room_id = response.room_id
# Join the room
response = await client.join(room_id)
print("Matrix join response:", response)
assert isinstance(response, nio.JoinResponse)
# Send a message to the room
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": "Hello continuwuity!"
}
)
print("Matrix room send response:", response)
assert isinstance(response, nio.RoomSendResponse)
# Sync responses
response = await client.sync(timeout=30000)
print("Matrix sync response:", response)
assert isinstance(response, nio.SyncResponse)
# Check the message was received by continuwuity
last_message = response.rooms.join[room_id].timeline.events[-1].body
assert last_message == "Hello continuwuity!"
# Leave the room
response = await client.room_leave(room_id)
print("Matrix room leave response:", response)
assert isinstance(response, nio.RoomLeaveResponse)
# Close the client
await client.close()
if __name__ == "__main__":
asyncio.run(main())
'')
];
};
}
]
))
};
testScript = ''
start_all()
with subtest("start continuwuity"):
continuwuity.wait_for_unit("continuwuity.service")
continuwuity.wait_for_open_port(6167)
with subtest("ensure messages can be exchanged"):
client.succeed("do_test >&2")
'';
};
}))
builtins.listToAttrs
];
};

View File

@@ -4,7 +4,7 @@
Name: continuwuity
Version: {{{ git_repo_version }}}
Release: 1%{?dist}
Summary: A Matrix homeserver written in Rust.
Summary: Very cool Matrix chat homeserver written in Rust
License: Apache-2.0 AND MIT
@@ -23,7 +23,7 @@ Requires: glibc
Requires: libstdc++
%global _description %{expand:
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
%description %{_description}

View File

@@ -1 +0,0 @@
tag-message = "chore: Release v{{version}}"

View File

@@ -1,7 +1,9 @@
[package]
name = "conduwuit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -2,17 +2,10 @@
use conduwuit::Result;
use crate::{
appservice::{self, AppserviceCommand},
check::{self, CheckCommand},
context::Context,
debug::{self, DebugCommand},
federation::{self, FederationCommand},
media::{self, MediaCommand},
query::{self, QueryCommand},
room::{self, RoomCommand},
server::{self, ServerCommand},
token::{self, TokenCommand},
user::{self, UserCommand},
appservice, appservice::AppserviceCommand, check, check::CheckCommand, context::Context,
debug, debug::DebugCommand, federation, federation::FederationCommand, media,
media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
server::ServerCommand, user, user::UserCommand,
};
#[derive(Debug, Parser)]
@@ -26,10 +19,6 @@ pub enum AdminCommand {
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing registration tokens
Token(TokenCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
@@ -59,36 +48,19 @@ pub enum AdminCommand {
Query(QueryCommand),
}
#[tracing::instrument(skip_all, name = "command", level = "info")]
#[tracing::instrument(skip_all, name = "command")]
pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Result {
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
},
| Token(command) => {
// token commands are all restricted
context.bail_restricted()?;
token::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,
}
}

View File

@@ -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 @@
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 @@ pub(crate) fn sender_or_service_user(&self) -> &UserId {
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.")
}
}
}

View File

@@ -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
@@ -456,10 +475,8 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self), level = "info")]
#[tracing::instrument(skip(self))]
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
if !self
.services
.rooms
@@ -483,10 +500,8 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self), level = "info")]
#[tracing::instrument(skip(self))]
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
if !self
.services
.rooms
@@ -510,15 +525,13 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
}
#[admin_command]
#[tracing::instrument(skip(self), level = "info")]
#[tracing::instrument(skip(self))]
pub(super) async fn force_set_room_state_from_server(
&self,
room_id: OwnedRoomId,
server_name: OwnedServerName,
at_event: Option<OwnedEventId>,
) -> Result {
self.bail_restricted()?;
if !self
.services
.rooms

View File

@@ -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

View File

@@ -8,14 +8,12 @@
#[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
}

View File

@@ -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.",);
}

View File

@@ -17,7 +17,6 @@
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod token;
pub(crate) mod user;
extern crate conduwuit_api as api;

View File

@@ -37,7 +37,7 @@ pub(super) fn dispatch(services: Arc<Services>, command: CommandInput) -> Proces
Box::pin(handle_command(services, command))
}
#[tracing::instrument(skip_all, name = "admin", level = "info")]
#[tracing::instrument(skip_all, name = "admin")]
async fn handle_command(services: Arc<Services>, command: CommandInput) -> ProcessorResult {
AssertUnwindSafe(Box::pin(process_command(services, &command)))
.catch_unwind()
@@ -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;

View File

@@ -31,7 +31,7 @@ pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
.services
.rooms
.timeline
.last_timeline_count(&room_id)
.last_timeline_count(None, &room_id)
.await?;
self.write_str(&format!("{result:#?}")).await
@@ -52,7 +52,7 @@ pub(super) async fn pdus(
.services
.rooms
.timeline
.pdus_rev(&room_id, from)
.pdus_rev(None, &room_id, from)
.try_take(limit.unwrap_or(3))
.try_collect()
.await?;

View File

@@ -98,7 +98,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
%room_id,
?room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
@@ -240,7 +240,7 @@ async fn ban_list_of_rooms(&self) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
%room_id,
?room_id,
?servers,
"Got federation response fetching room ID for \
{room}",
@@ -397,7 +397,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
{
| Ok((room_id, servers)) => {
debug!(
%room_id,
?room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);

View File

@@ -24,39 +24,16 @@ 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
}
#[admin_command]
pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
// The path argument is only what's optionally passed via the admin command,
// so we need to merge it with the existing paths if any were given at startup.
let mut paths = Vec::new();
let path = path.as_deref().into_iter();
self.services.config.reload(path)?;
// Add previously saved paths to the argument list
self.services
.config
.config_paths
.clone()
.unwrap_or_default()
.iter()
.for_each(|p| paths.push(p.to_owned()));
// If a path is given, and it's not already in the list,
// add it last, so that it overrides earlier files
if let Some(p) = path {
if !paths.contains(&p) {
paths.push(p);
}
}
self.services.config.reload(&paths)?;
self.write_str(&format!("Successfully reconfigured from paths: {paths:?}"))
.await
self.write_str("Successfully reconfigured.").await
}
#[admin_command]
@@ -120,8 +97,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 +123,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 +147,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()?;

View File

@@ -1,76 +0,0 @@
use conduwuit::{Err, Result, utils};
use conduwuit_macros::admin_command;
use futures::StreamExt;
use service::registration_tokens::TokenExpires;
#[admin_command]
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
} else if let Some(max_uses) = expires.max_uses {
Some(TokenExpires::AfterUses(max_uses))
} else if expires.once {
Some(TokenExpires::AfterUses(1))
} else if let Some(max_age) = expires
.max_age
.as_deref()
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
.transpose()?
{
Some(TokenExpires::AfterTime(max_age))
} else {
unreachable!();
}
};
let (token, info) = self
.services
.registration_tokens
.issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!(
"New registration token issued: `{token}`. {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
))
.await
}
#[admin_command]
pub(super) async fn revoke_token(&self, token: String) -> Result {
let Some(token) = self
.services
.registration_tokens
.validate_token(token)
.await
else {
return Err!("This token does not exist or has already expired.");
};
self.services.registration_tokens.revoke_token(token)?;
self.write_str("Token revoked successfully.").await
}
#[admin_command]
pub(super) async fn list_tokens(&self) -> Result {
let tokens: Vec<_> = self
.services
.registration_tokens
.iterate_tokens()
.collect()
.await;
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
.await?;
for token in tokens {
self.write_str(&format!("- {token}\n")).await?;
}
Ok(())
}

View File

@@ -1,51 +0,0 @@
mod commands;
use clap::{Args, Subcommand};
use conduwuit::Result;
use crate::admin_command_dispatch;
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
pub enum TokenCommand {
/// - Issue a new registration token
#[clap(name = "issue")]
IssueToken {
/// When this token will expire.
#[command(flatten)]
expires: TokenExpires,
},
/// - Revoke a registration token
#[clap(name = "revoke")]
RevokeToken {
/// The token to revoke.
token: String,
},
/// - List all registration tokens
#[clap(name = "list")]
ListTokens,
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
pub struct TokenExpires {
/// The maximum number of times this token is allowed to be used before it
/// expires.
#[arg(long)]
max_uses: Option<u64>,
/// The maximum age of this token (e.g. 30s, 5m, 7d). It will expire after
/// this much time has passed.
#[arg(long)]
max_age: Option<String>,
/// This token will never expire.
#[arg(long)]
immortal: bool,
/// A shortcut for `--max-uses 1`.
#[arg(long)]
once: bool,
}

View File

@@ -238,7 +238,6 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
#[admin_command]
pub(super) async fn suspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
@@ -263,7 +262,6 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
#[admin_command]
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
@@ -280,12 +278,7 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
}
#[admin_command]
pub(super) async fn reset_password(
&self,
logout: bool,
username: String,
password: Option<String>,
) -> Result {
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
if user_id == self.services.globals.server_user {
@@ -308,18 +301,7 @@ pub(super) async fn reset_password(
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await?;
if logout {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
Ok(())
.await
}
#[admin_command]
@@ -479,11 +461,9 @@ 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
.services
@@ -502,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
@@ -594,11 +583,9 @@ 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
.services
@@ -617,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
@@ -992,113 +988,3 @@ pub(super) async fn force_leave_remote_room(
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
.await
}
#[admin_command]
pub(super) async fn lock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to lock the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be locked.");
}
self.services
.users
.lock_account(&user_id, self.sender_or_service_user())
.await;
self.write_str(&format!("User {user_id} has been locked."))
.await
}
#[admin_command]
pub(super) async fn unlock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
self.services.users.unlock_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unlocked."))
.await
}
#[admin_command]
pub(super) async fn logout(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to log out the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("You cannot forcefully log out admin users.");
}
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
}
#[admin_command]
pub(super) async fn disable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to disable login for the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot have their login disallowed.");
}
self.services.users.disable_login(&user_id);
self.write_str(&format!(
"{user_id} can no longer log in. Their existing sessions remain unaffected."
))
.await
}
#[admin_command]
pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.enable_login(&user_id);
self.write_str(&format!("{user_id} can now log in.")).await
}

View File

@@ -20,9 +20,6 @@ pub enum UserCommand {
/// - Reset user password
ResetPassword {
/// Log out existing sessions
#[arg(short, long)]
logout: bool,
/// Username of the user for whom the password should be reset
username: String,
/// New password for the user, if unspecified one is generated
@@ -62,18 +59,6 @@ pub enum UserCommand {
force: bool,
},
/// - Forcefully log a user out of all of their devices.
///
/// This will invalidate all access tokens for the specified user,
/// effectively logging them out from all sessions.
/// Note that this is destructive and may result in data loss for the user,
/// such as encryption keys. Use with caution. Can only be used in the admin
/// room.
Logout {
/// Username of the user to log out
user_id: String,
},
/// - Suspend a user
///
/// Suspended users are able to log in, sync, and read messages, but are not
@@ -96,42 +81,6 @@ pub enum UserCommand {
user_id: String,
},
/// - Lock a user
///
/// Locked users are unable to use their accounts beyond logging out. This
/// is akin to a temporary deactivation that does not change the user's
/// password. This can be used to quickly prevent a user from accessing
/// their account.
Lock {
/// Username of the user to lock
user_id: String,
},
/// - Unlock a user
///
/// Reverses the effects of the `lock` command, allowing the user to use
/// their account again.
Unlock {
/// Username of the user to unlock
user_id: String,
},
/// - Enable login for a user
EnableLogin {
/// Username of the user to enable login for
user_id: String,
},
/// - Disable login for a user
///
/// Disables login for the specified user without deactivating or locking
/// their account. This prevents the user from obtaining new access tokens,
/// but does not invalidate existing sessions.
DisableLogin {
/// Username of the user to disable login for
user_id: String,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,

View File

@@ -1,7 +1,9 @@
[package]
name = "conduwuit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -49,7 +49,7 @@
///
/// Note: This will not reserve the username, so the username might become
/// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -138,7 +138,7 @@ pub(crate) async fn get_register_available_route(
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "register")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -179,18 +179,13 @@ pub(crate) async fn register_route(
},
}
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services
.registration_tokens
.get_config_file_token()
.is_some()))
&& services.globals.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, \
@@ -208,9 +203,7 @@ pub(crate) async fn register_route(
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (body.username.as_ref(), is_guest) {
@@ -308,13 +301,7 @@ pub(crate) async fn register_route(
let skip_auth = body.appservice_info.is_some() || is_guest;
// Populate required UIAA flows
if services
.registration_tokens
.iterate_tokens()
.next()
.await
.is_some()
{
if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo.flows.push(AuthFlow {
stages: vec![AuthType::RegistrationToken],
@@ -336,19 +323,7 @@ pub(crate) async fn register_route(
}
if uiaainfo.flows.is_empty() && !skip_auth {
// Registration isn't _disabled_, but there's no captcha configured and no
// registration tokens currently set. Bail out by default unless open
// registration was explicitly enabled.
if !services
.config
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
{
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
// We have open registration enabled (😧), provide a dummy stage
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
@@ -628,7 +603,7 @@ pub(crate) async fn register_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "change_password")]
pub(crate) async fn change_password_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -724,7 +699,7 @@ pub(crate) async fn change_password_route(
Ok(change_password::v3::Response {})
}
/// # `GET /_matrix/client/v3/account/whoami`
/// # `GET _matrix/client/r0/account/whoami`
///
/// Get `user_id` of the sender user.
///
@@ -733,17 +708,11 @@ pub(crate) async fn whoami_route(
State(services): State<crate::State>,
body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> {
let is_guest = services
.users
.is_deactivated(body.sender_user())
.await
.map_err(|_| {
err!(Request(Forbidden("Application service has not registered this user.")))
})? && body.appservice_info.is_none();
Ok(whoami::v3::Response {
user_id: body.sender_user().to_owned(),
device_id: body.sender_device.clone(),
is_guest,
is_guest: services.users.is_deactivated(body.sender_user()).await?
&& body.appservice_info.is_none(),
})
}
@@ -758,7 +727,7 @@ pub(crate) async fn whoami_route(
/// - Forgets all to-device events
/// - Triggers device list updates
/// - Removes ability to log in again
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "deactivate")]
pub(crate) async fn deactivate_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -877,20 +846,19 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking.
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>,
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
// TODO: ratelimit this pretty heavily
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err!(Request(Forbidden("Server does not allow token registration")));
};
let valid = services
.registration_tokens
.validate_token(body.token.clone())
.await
.is_some();
Ok(check_registration_token_validity::v1::Response { valid })
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
}
/// Runs through all the deactivation steps:

View File

@@ -102,7 +102,7 @@ pub(crate) async fn get_alias_route(
};
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
debug!(%room_alias, %room_id, "available servers: {servers:?}");
debug!(?room_alias, ?room_id, "available servers: {servers:?}");
Ok(get_alias::v3::Response::new(room_id, servers))
}

View File

@@ -59,7 +59,7 @@ pub(crate) async fn get_context_route(
.rooms
.timeline
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Event not found."))));
.map_err(|_| err!(Request(NotFound("Base event not found."))));
let visible = services
.rooms
@@ -70,11 +70,11 @@ pub(crate) async fn get_context_route(
let (base_id, base_pdu, visible) = try_join3(base_id, base_pdu, visible).await?;
if base_pdu.room_id_or_hash() != *room_id || base_pdu.event_id != *event_id {
return Err!(Request(NotFound("Event not found.")));
return Err!(Request(NotFound("Base event not found.")));
}
if !visible {
debug_warn!(req_evt = %event_id, ?base_id, %room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
debug_warn!(req_evt = ?event_id, ?base_id, ?room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
@@ -82,25 +82,11 @@ pub(crate) async fn get_context_route(
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
// PDUs are used to get seen user IDs and then returned in response.
let events_before = services
.rooms
.timeline
.pdus_rev(room_id, Some(base_count))
.pdus_rev(Some(sender_user), room_id, Some(base_count))
.ignore_err()
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
@@ -110,20 +96,8 @@ pub(crate) async fn get_context_route(
let events_after = services
.rooms
.timeline
.pdus(room_id, Some(base_count))
.pdus(Some(sender_user), room_id, Some(base_count))
.ignore_err()
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))

View File

@@ -49,7 +49,7 @@ pub(crate) async fn get_device_route(
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "update_device", level = "debug")]
#[tracing::instrument(skip_all, fields(%client), name = "update_device")]
pub(crate) async fn update_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -44,7 +44,7 @@
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
pub(crate) async fn get_public_rooms_filtered_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -80,7 +80,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
pub(crate) async fn get_public_rooms_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -116,7 +116,7 @@ pub(crate) async fn get_public_rooms_route(
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Sets the visibility of a given room in the room directory.
#[tracing::instrument(skip_all, fields(%client), name = "room_directory", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "room_directory")]
pub(crate) async fn set_room_visibility_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -1,15 +1,7 @@
use std::{
collections::{BTreeMap, HashMap, HashSet},
time::Duration,
};
use std::collections::{BTreeMap, HashMap, HashSet};
use axum::extract::State;
use conduwuit::{
Err, Error, Result, debug, debug_warn, err,
result::NotFound,
utils,
utils::{IterStream, stream::WidebandExt},
};
use conduwuit::{Err, Error, Result, debug, debug_warn, err, result::NotFound, utils};
use conduwuit_service::{Services, users::parse_master_key};
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
@@ -52,7 +44,7 @@ pub(crate) async fn upload_keys_route(
.deserialize()
.inspect_err(|e| {
debug_warn!(
%key_id,
?key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
@@ -94,8 +86,8 @@ pub(crate) async fn upload_keys_route(
{
if existing_keys.json().get() == device_keys.json().get() {
debug!(
%sender_user,
%sender_device,
?sender_user,
?sender_device,
?device_keys,
"Ignoring user uploaded keys as they are an exact copy already in the \
database"
@@ -142,7 +134,6 @@ pub(crate) async fn get_keys_route(
&body.device_keys,
|u| u == sender_user,
true, // Always allow local users to see device names of other local users
body.timeout.unwrap_or(Duration::from_secs(10)),
)
.await
}
@@ -154,12 +145,7 @@ pub(crate) async fn claim_keys_route(
State(services): State<crate::State>,
body: Ruma<claim_keys::v3::Request>,
) -> Result<claim_keys::v3::Response> {
claim_keys_helper(
&services,
&body.one_time_keys,
body.timeout.unwrap_or(Duration::from_secs(10)),
)
.await
claim_keys_helper(&services, &body.one_time_keys).await
}
/// # `POST /_matrix/client/r0/keys/device_signing/upload`
@@ -338,7 +324,7 @@ pub(crate) async fn upload_signatures_route(
for (user_id, keys) in &body.signed_keys {
for (key_id, key) in keys {
let Ok(key) = serde_json::to_value(key)
.inspect_err(|e| debug_warn!(%key_id, "Invalid \"key\" JSON: {e}"))
.inspect_err(|e| debug_warn!(?key_id, "Invalid \"key\" JSON: {e}"))
else {
continue;
};
@@ -435,7 +421,6 @@ pub(crate) async fn get_keys_helper<F>(
device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
allowed_signatures: F,
include_display_names: bool,
timeout: Duration,
) -> Result<get_keys::v3::Response>
where
F: Fn(&UserId) -> bool + Send + Sync,
@@ -527,10 +512,9 @@ pub(crate) async fn get_keys_helper<F>(
let mut failures = BTreeMap::new();
let futures = get_over_federation
let mut futures: FuturesUnordered<_> = get_over_federation
.into_iter()
.stream()
.wide_filter_map(|(server, vec)| async move {
.map(|(server, vec)| async move {
let mut device_keys_input_fed = BTreeMap::new();
for (user_id, keys) in vec {
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
@@ -538,22 +522,17 @@ pub(crate) async fn get_keys_helper<F>(
let request =
federation::keys::get_keys::v1::Request { device_keys: device_keys_input_fed };
let response = tokio::time::timeout(
timeout,
services.sending.send_federation_request(server, request),
)
.await
// Need to flatten the Result<Result<V, E>, E> into Result<V, E>
.map_err(|_| err!(Request(Unknown("Timeout when getting keys over federation."))))
.and_then(|res| res);
Some((server, response))
let response = services
.sending
.send_federation_request(server, request)
.await;
(server, response)
})
.collect::<FuturesUnordered<_>>()
.await
.into_iter();
.collect();
for (server, response) in futures {
while let Some((server, response)) = futures.next().await {
match response {
| Ok(response) => {
for (user, master_key) in response.master_keys {
@@ -585,8 +564,8 @@ pub(crate) async fn get_keys_helper<F>(
self_signing_keys.extend(response.self_signing_keys);
device_keys.extend(response.device_keys);
},
| Err(e) => {
failures.insert(server.to_string(), json!({ "error": e.to_string() }));
| _ => {
failures.insert(server.to_string(), json!({}));
},
}
}
@@ -629,7 +608,6 @@ fn add_unsigned_device_display_name(
pub(crate) async fn claim_keys_helper(
services: &Services,
one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, OneTimeKeyAlgorithm>>,
timeout: Duration,
) -> Result<claim_keys::v3::Response> {
let mut one_time_keys = BTreeMap::new();
@@ -660,39 +638,32 @@ pub(crate) async fn claim_keys_helper(
let mut failures = BTreeMap::new();
let futures = get_over_federation
let mut futures: FuturesUnordered<_> = get_over_federation
.into_iter()
.stream()
.wide_filter_map(|(server, vec)| async move {
.map(|(server, vec)| async move {
let mut one_time_keys_input_fed = BTreeMap::new();
for (user_id, keys) in vec {
one_time_keys_input_fed.insert(user_id.clone(), keys.clone());
}
let response = tokio::time::timeout(
timeout,
services.sending.send_federation_request(
server,
federation::keys::claim_keys::v1::Request {
(
server,
services
.sending
.send_federation_request(server, federation::keys::claim_keys::v1::Request {
one_time_keys: one_time_keys_input_fed,
},
),
})
.await,
)
.await
.map_err(|_| err!(Request(Unknown("Timeout when claiming keys over federation."))))
.and_then(|res| res);
Some((server, response))
})
.collect::<FuturesUnordered<_>>()
.await
.into_iter();
.collect();
for (server, response) in futures {
while let Some((server, response)) = futures.next().await {
match response {
| Ok(keys) => {
one_time_keys.extend(keys.one_time_keys);
},
| Err(e) => {
failures.insert(server.to_string(), json!({"error": e.to_string()}));
| Err(_e) => {
failures.insert(server.to_string(), json!({}));
},
}
}

View File

@@ -3,11 +3,10 @@
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
};
use futures::FutureExt;
use ruma::{
RoomId, UserId,
OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::{
invite_permission_config::FilterLevel,
@@ -22,7 +21,7 @@
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -125,18 +124,6 @@ pub(crate) async fn invite_helper(
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
.await
{
warn!(
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
sender_user, recipient_user, room_id
);
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
}
if !services.globals.user_is_local(recipient_user) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
@@ -203,10 +190,19 @@ pub(crate) async fn invite_helper(
))));
}
let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
)?)
.map_err(|e| {
err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
})?;
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(recipient_user.server_name(), room_id, &event_id, value, true)
.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
.boxed()
.await?
.ok_or_else(|| {

View File

@@ -33,7 +33,7 @@
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule},
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
@@ -48,7 +48,7 @@
},
};
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`
@@ -59,7 +59,7 @@
/// rules locally
/// - If the server does not know about the room: asks other servers over
/// federation
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -131,7 +131,7 @@ pub(crate) async fn join_room_by_id_route(
/// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -288,23 +288,6 @@ pub async fn join_room_by_id_helper(
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
if let Err(e) = services
.antispam
.user_may_join_room(
sender_user.to_owned(),
room_id.to_owned(),
services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await,
)
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("You are not allowed to join this room.")));
}
let server_in_room = services
.rooms
.state_cache
@@ -338,17 +321,6 @@ pub async fn join_room_by_id_helper(
)));
}
if services.antispam.check_all_joins() {
if let Err(e) = services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
if server_in_room {
join_room_by_id_helper_local(
services,
@@ -375,10 +347,11 @@ pub async fn join_room_by_id_helper(
.boxed()
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote", level = "info")]
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote")]
async fn join_room_by_id_helper_remote(
services: &Services,
sender_user: &UserId,
@@ -736,7 +709,7 @@ async fn join_room_by_id_helper_remote(
Ok(())
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local", level = "info")]
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local")]
async fn join_room_by_id_helper_local(
services: &Services,
sender_user: &UserId,
@@ -747,51 +720,45 @@ async fn join_room_by_id_helper_local(
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can join locally");
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
let mut restricted_join_authorized = None;
match join_rules {
| JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
for restriction in restricted.allow {
match restriction {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(sender_user, &membership.room_id)
.await
{
restricted_join_authorized = Some(true);
break;
}
},
| AllowRule::UnstableSpamChecker => {
match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
| Ok(()) => {
restricted_join_authorized = Some(true);
break;
},
| Err(_) =>
return Err!(Request(Forbidden(
"Antispam rejected join request."
))),
}
},
| _ => {},
}
}
},
| _ => {},
}
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
None
} else {
match restricted_join_authorized.unwrap() {
| true => services
let join_rules_event_content = services
.rooms
.state_accessor
.room_state_get_content::<RoomJoinRulesEventContent>(
room_id,
&StateEventType::RoomJoinRules,
"",
)
.await;
let restriction_rooms = match join_rules_event_content {
| Ok(RoomJoinRulesEventContent {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
}) => restricted
.allow
.into_iter()
.filter_map(|a| match a {
| AllowRule::RoomMembership(r) => Some(r.room_id),
| _ => None,
})
.collect(),
| _ => Vec::new(),
};
let join_authorized_via_users_server: Option<OwnedUserId> = {
if restriction_rooms
.iter()
.stream()
.any(|restriction_room_id| {
trace!("Checking if {sender_user} is joined to {restriction_room_id}");
services
.rooms
.state_cache
.is_joined(sender_user, restriction_room_id)
})
.await
{
services
.rooms
.state_cache
.local_users_in_room(room_id)
@@ -807,14 +774,10 @@ async fn join_room_by_id_helper_local(
.boxed()
.next()
.await
.map(ToOwned::to_owned),
| false => {
warn!(
"Join authorization failed for restricted join in room {room_id} for user \
{sender_user}"
);
return Err!(Request(Forbidden("You are not authorized to join this room.")));
},
.map(ToOwned::to_owned)
} else {
trace!("No restriction rooms are joined by {sender_user}");
None
}
};
@@ -842,14 +805,16 @@ async fn join_room_by_id_helper_local(
return Ok(());
};
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
if restriction_rooms.is_empty()
&& (servers.is_empty()
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error);
}
warn!(
?error,
servers = %servers.len(),
"Could not join restricted room locally, attempting remote join",
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
join requirements"
);
let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await
@@ -872,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| {

View File

@@ -38,13 +38,13 @@
},
};
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}`
///
/// Tries to knock the room to ask permission to join for the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "knock", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "knock")]
pub(crate) async fn knock_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -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()),

View File

@@ -21,7 +21,6 @@
};
use service::Services;
use super::validate_remote_member_event_stub;
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
@@ -178,20 +177,7 @@ pub async fn leave_room(
.rooms
.state_cache
.left_state(user_id, room_id)
.await
.inspect_err(|err| {
// `left_state` may return an Err if the user _is_ in the room they're
// trying to leave, but the membership cache is incorrect and
// they're cached as being joined. In this situation
// we save a `None` to the `roomuserid_leftcount` table, which generates
// and sends a dummy leave to the client.
warn!(
?err,
"Trying to leave room not cached as leave, sending dummy leave \
event to client"
);
})
.unwrap_or_default()
.await?
},
}
};
@@ -338,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(),

View File

@@ -13,14 +13,7 @@
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::{
@@ -63,7 +56,7 @@ pub(crate) async fn joined_rooms_route(
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument(skip(services), level = "info")]
#[tracing::instrument(skip(services))]
pub(crate) async fn banned_room_check(
services: &Services,
user_id: &UserId,
@@ -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(())
}

View File

@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, at, debug_warn,
Err, Result, at,
matrix::{
event::{Event, Matches},
pdu::PduCount,
@@ -122,14 +122,14 @@ pub(crate) async fn get_message_events_route(
| Direction::Forward => services
.rooms
.timeline
.pdus(room_id, Some(from))
.pdus(Some(sender_user), room_id, Some(from))
.ignore_err()
.boxed(),
| Direction::Backward => services
.rooms
.timeline
.pdus_rev(room_id, Some(from))
.pdus_rev(Some(sender_user), room_id, Some(from))
.ignore_err()
.boxed(),
};
@@ -140,18 +140,6 @@ pub(crate) async fn get_message_events_route(
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit)
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.collect()
.await;

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Result, at, debug_warn,
Result, at,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
@@ -109,16 +109,6 @@ async fn paginate_relations_with_filter(
recurse: bool,
dir: Direction,
) -> Result<get_relating_events::v1::Response> {
if !services
.rooms
.state_accessor
.user_can_see_event(sender_user, room_id, target)
.await
{
debug_warn!(req_evt = %target, %room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
let start: PduCount = from
.map(str::parse)
.transpose()?
@@ -139,6 +129,11 @@ async fn paginate_relations_with_filter(
// Spec (v1.10) recommends depth of at least 3
let depth: u8 = if recurse { 3 } else { 1 };
// Check if this is a thread request
let is_thread = filter_rel_type
.as_ref()
.is_some_and(|rel| *rel == RelationType::Thread);
let events: Vec<_> = services
.rooms
.pdu_metadata
@@ -157,24 +152,40 @@ async fn paginate_relations_with_filter(
})
.stream()
.ready_take_while(|(count, _)| Some(*count) != to)
.take(limit)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.then(async |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations to relation: {e}");
}
pdu
})
.take(limit)
.collect()
.await;
// For threads, check if we should include the root event
let mut root_event = None;
if is_thread && dir == Direction::Backward {
// Check if we've reached the beginning of the thread
// (fewer events than requested means we've exhausted the thread)
if events.len() < limit {
// Try to get the thread root event
if let Ok(root_pdu) = services.rooms.timeline.get_pdu(target).await {
// Check visibility
if services
.rooms
.state_accessor
.user_can_see_event(sender_user, room_id, target)
.await
{
// Store the root event to add to the response
root_event = Some(root_pdu);
}
}
}
}
// Determine if there are more events to fetch
let has_more = events.len() >= limit;
let has_more = if root_event.is_some() {
false // We've included the root, no more events
} else {
// Check if we got a full page of results (might be more)
events.len() >= limit
};
let next_batch = if has_more {
match dir {
@@ -186,10 +197,11 @@ async fn paginate_relations_with_filter(
None
};
let chunk: Vec<_> = events
// Build the response chunk with thread root if needed
let chunk: Vec<_> = root_event
.into_iter()
.map(at!(1))
.map(Event::into_format)
.chain(events.into_iter().map(at!(1)).map(Event::into_format))
.collect();
Ok(get_relating_events::v1::Response {

View File

@@ -29,7 +29,7 @@ struct Report {
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_room", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "report_room")]
pub(crate) async fn report_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -85,7 +85,7 @@ pub(crate) async fn report_room_route(
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_event", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "report_event")]
pub(crate) async fn report_event_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -133,7 +133,7 @@ pub(crate) async fn report_event_route(
Ok(report_content::v3::Response {})
}
#[tracing::instrument(skip_all, fields(%client), name = "report_user", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "report_user")]
pub(crate) async fn report_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -238,7 +238,6 @@ pub(crate) async fn create_room_route(
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
timestamp: body.origin_server_ts,
..Default::default()
},
sender_user,
@@ -257,14 +256,6 @@ pub(crate) async fn create_room_route(
},
};
drop(state_lock);
if let Some(expected_room_id) = body.room_id.as_ref() {
if expected_room_id.as_str() != room_id.as_str() {
return Err!(Request(InvalidParam(
"Custom room ID {expected_room_id} does not match the generated room ID \
{room_id}.",
)));
}
}
debug!("Room created with ID {room_id}");
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
@@ -501,7 +492,7 @@ pub(crate) async fn create_room_route(
.boxed()
.await
{
warn!(?e, "Failed to send invite");
warn!(%e, "Failed to send invite");
}
}
@@ -636,7 +627,7 @@ async fn room_alias_check(
.map_err(|e| {
err!(Request(InvalidParam(debug_error!(
?e,
%room_alias_name,
?room_alias_name,
"Failed to parse room alias.",
))))
})?;
@@ -720,7 +711,7 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
}
})
.inspect(|full_room_id| {
debug_info!(%full_room_id, "Full custom room ID");
debug_info!(?full_room_id, "Full custom room ID");
})
.inspect_err(|e| warn!(?e, %custom_room_id, "Failed to create room with custom room ID",))
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
}

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Err, Event, Result, debug_warn, err};
use conduwuit::{Err, Event, Result, err};
use futures::{FutureExt, TryFutureExt, future::try_join};
use ruma::api::client::room::get_room_event;
@@ -33,16 +33,7 @@ pub(crate) async fn get_room_event_route(
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut event)
.await
{
debug_warn!("Failed to add bundled aggregations to event: {e}");
}
event.set_unsigned(body.sender_user.as_deref());
event.add_age().ok();
Ok(get_room_event::v3::Response { event: event.into_format() })
}

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at, debug_warn,
Err, Event, Result, at,
utils::{BoolExt, stream::TryTools},
};
use futures::{FutureExt, TryStreamExt, future::try_join4};
@@ -40,28 +40,12 @@ pub(crate) async fn room_initial_sync_route(
.map_ok(Event::into_format)
.try_collect::<Vec<_>>();
// Events are returned in body
let limit = LIMIT_MAX;
let events = services
.rooms
.timeline
.pdus_rev(room_id, None)
.pdus_rev(None, room_id, None)
.try_take(limit)
.and_then(async |mut pdu| {
pdu.1.set_unsigned(body.sender_user.as_deref());
if let Some(sender_user) = body.sender_user.as_deref() {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
}
Ok(pdu)
})
.try_collect::<Vec<_>>();
let (membership, visibility, state, events) =

View File

@@ -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,
};
@@ -46,7 +46,7 @@ pub(crate) async fn get_room_summary_legacy(
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
///
/// Returns a short description of the state of a room.
#[tracing::instrument(skip_all, fields(%client), name = "room_summary", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "room_summary")]
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -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 =
@@ -116,31 +110,27 @@ async fn local_room_summary_response(
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
trace!(
sender_user = sender_user.map(tracing::field::display),
"Sending local room summary response for {room_id:?}"
);
let (join_rule, world_readable, guest_can_join) = join3(
services.rooms.state_accessor.get_join_rules(room_id),
services.rooms.state_accessor.is_world_readable(room_id),
services.rooms.state_accessor.guest_can_join(room_id),
)
.await;
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
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
@@ -231,7 +221,7 @@ async fn remote_room_summary_hierarchy_response(
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<SpaceHierarchyParentSummary> {
trace!(sender_user = ?sender_user.map(tracing::field::display), ?servers, "Sending remote room summary response for {room_id:?}");
trace!(?sender_user, ?servers, "Sending remote room summary response for {room_id:?}");
if !services.config.allow_federation {
return Err!(Request(Forbidden("Federation is disabled.")));
}
@@ -241,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();
@@ -277,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>(
@@ -333,14 +311,21 @@ async fn user_can_see_summary<'a, I>(
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"
)))
},
}
}

View File

@@ -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

View File

@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
Err, Result, at, debug_warn, is_true,
Err, Result, at, is_true,
matrix::Event,
result::FlatOk,
utils::{IterStream, stream::ReadyExt},
@@ -50,7 +50,7 @@ pub(crate) async fn search_events_route(
Ok(Response {
search_categories: ResultCategories {
room_events: Box::pin(room_events_result)
room_events: room_events_result
.await
.unwrap_or_else(|| Ok(ResultRoomEvents::default()))?,
},
@@ -110,12 +110,7 @@ async fn category_room_events(
limit,
};
let (count, results) = services
.rooms
.search
.search_pdus(&query, sender_user)
.await
.ok()?;
let (count, results) = services.rooms.search.search_pdus(&query).await.ok()?;
results
.collect::<Vec<_>>()
@@ -149,17 +144,6 @@ async fn category_room_events(
.map(at!(2))
.flatten()
.stream()
.then(|mut pdu| async {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu)
.await
{
debug_warn!("Failed to add bundled aggregations to search result: {e}");
}
pdu
})
.map(Event::into_format)
.map(|result| SearchResult {
rank: None,

View File

@@ -5,7 +5,6 @@
use conduwuit::{
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash},
warn,
};
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
@@ -13,7 +12,6 @@
use ruma::{
OwnedUserId, UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -37,7 +35,7 @@
///
/// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in.
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "login")]
pub(crate) async fn get_login_types_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -55,7 +53,7 @@ pub(crate) async fn get_login_types_route(
/// Authenticates the given user by its ID and its password.
///
/// Returns the user ID if successful, and an error otherwise.
#[tracing::instrument(skip_all, fields(%user_id), name = "password", level = "debug")]
#[tracing::instrument(skip_all, fields(%user_id), name = "password")]
pub(crate) async fn password_login(
services: &Services,
user_id: &UserId,
@@ -98,7 +96,7 @@ pub(crate) async fn password_login(
///
/// Creates the user if the user is found in the LDAP and do not already have an
/// account.
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap", level = "debug")]
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")]
pub(super) async fn ldap_login(
services: &Services,
user_id: &UserId,
@@ -186,15 +184,6 @@ pub(crate) async fn handle_login(
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
}
if services.users.is_login_disabled(&user_id).await {
warn!(%user_id, "user attempted to log in with a login-disabled account");
return Err!(Request(Forbidden("This account is not permitted to log in.")));
}
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
@@ -223,7 +212,7 @@ pub(crate) async fn handle_login(
/// Note: You can use [`GET
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
/// supported login types.
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "login")]
pub(crate) async fn login_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -356,7 +345,7 @@ pub(crate) async fn login_route(
/// to log in with the m.login.token flow.
///
/// <https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv1loginget_token>
#[tracing::instrument(skip_all, fields(%client), name = "login_token", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "login_token")]
pub(crate) async fn login_token_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -424,7 +413,7 @@ pub(crate) async fn login_token_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
pub(crate) async fn logout_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -451,7 +440,7 @@ pub(crate) async fn logout_route(
/// Note: This is equivalent to calling [`GET
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
/// user.
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "logout")]
pub(crate) async fn logout_all_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -140,8 +140,8 @@ pub(crate) async fn get_state_events_for_key_route(
.await
.map_err(|_| {
err!(Request(NotFound(debug_warn!(
room_id = %body.room_id,
event_type = %body.event_type,
room_id = ?body.room_id,
event_type = ?body.event_type,
"State event not found in room.",
))))
})?;
@@ -158,7 +158,7 @@ pub(crate) async fn get_state_events_for_key_route(
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id_or_hash(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
@@ -226,7 +226,7 @@ async fn allowed_to_send_state_event(
match event_type {
| StateEventType::RoomCreate => {
return Err!(Request(BadJson(debug_warn!(
%room_id,
?room_id,
"You cannot update m.room.create after a room has been created."
))));
},
@@ -237,7 +237,7 @@ async fn allowed_to_send_state_event(
| Ok(acl_content) => {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
%room_id,
?room_id,
"Sending an ACL event with an empty allow key will permanently \
brick the room for non-conduwuit's as this equates to no servers \
being allowed to participate in this room."
@@ -246,7 +246,7 @@ async fn allowed_to_send_state_event(
if acl_content.deny_contains("*") && acl_content.allow_contains("*") {
return Err!(Request(BadJson(debug_warn!(
%room_id,
?room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
permanently brick the room for non-conduwuit's as this equates to \
no servers being allowed to participate in this room."
@@ -258,7 +258,7 @@ async fn allowed_to_send_state_event(
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
?room_id,
"Sending an ACL event with a deny key value of \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
@@ -270,7 +270,7 @@ async fn allowed_to_send_state_event(
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
?room_id,
"Sending an ACL event for an allow key without \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."

View File

@@ -4,7 +4,7 @@
use std::collections::VecDeque;
use conduwuit::{
Event, PduCount, Result, debug_warn, err,
Event, PduCount, Result, err,
matrix::pdu::PduEvent,
ref_at, trace,
utils::stream::{BroadbandExt, ReadyExt, TryIgnore},
@@ -53,7 +53,7 @@ async fn load_timeline(
let last_timeline_count = services
.rooms
.timeline
.last_timeline_count(room_id)
.last_timeline_count(Some(sender_user), room_id)
.await
.map_err(|err| {
err!(Database(warn!("Failed to fetch end of room timeline: {}", err)))
@@ -71,24 +71,13 @@ async fn load_timeline(
services
.rooms
.timeline
.pdus_rev(room_id, ending_count.map(|count| count.saturating_add(1)))
.pdus_rev(
Some(sender_user),
room_id,
ending_count.map(|count| count.saturating_add(1)),
)
.ignore_err()
.ready_take_while(move |&(pducount, _)| pducount > starting_count)
.map(move |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
pdu
})
.then(async move |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.boxed()
},
| None => {
@@ -97,23 +86,12 @@ async fn load_timeline(
services
.rooms
.timeline
.pdus_rev(room_id, ending_count.map(|count| count.saturating_add(1)))
.pdus_rev(
Some(sender_user),
room_id,
ending_count.map(|count| count.saturating_add(1)),
)
.ignore_err()
.map(move |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
pdu
})
.then(async move |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.boxed()
},
};

View File

@@ -50,8 +50,8 @@
level = "debug",
skip_all,
fields(
room_id = %room_id,
syncing_user = %sync_context.syncing_user,
room_id = ?room_id,
syncing_user = ?sync_context.syncing_user,
),
)]
pub(super) async fn load_joined_room(
@@ -578,7 +578,7 @@ async fn build_notification_counts(
)
.await;
trace!(%notification_count, %highlight_count, "syncing new notification counts");
trace!(?notification_count, ?highlight_count, "syncing new notification counts");
Ok(Some(UnreadNotificationsCount {
notification_count: Some(notification_count),
@@ -692,8 +692,8 @@ async fn build_room_summary(
};
trace!(
%joined_member_count,
%invited_member_count,
?joined_member_count,
?invited_member_count,
heroes_length = heroes.as_ref().map(HashSet::len),
"syncing updated summary"
);

View File

@@ -307,8 +307,8 @@ async fn build_left_state_and_timeline(
}
trace!(
%timeline_start_count,
%timeline_end_count,
?timeline_start_count,
?timeline_end_count,
"syncing {} timeline events (limited = {}) and {} state events",
timeline.pdus.len(),
timeline.limited,

View File

@@ -275,7 +275,7 @@ pub(crate) async fn build_sync_events(
match joined_room {
| Ok((room, updates)) => Some((room_id, room, updates)),
| Err(err) => {
warn!(?err, %room_id, "error loading joined room");
warn!(?err, ?room_id, "error loading joined room {}", room_id);
None
},
}

View File

@@ -127,7 +127,7 @@ pub(super) async fn build_state_incremental<'a>(
let last_pdu_of_last_sync = services
.rooms
.timeline
.pdus_rev(room_id, Some(last_sync_end_count.saturating_add(1)))
.pdus_rev(Some(sender_user), room_id, Some(last_sync_end_count.saturating_add(1)))
.boxed()
.next()
.await
@@ -217,7 +217,7 @@ pub(super) async fn build_state_incremental<'a>(
the performance penalty is acceptable.
*/
trace!(%timeline_is_linear, %timeline.limited, "computing state for incremental sync");
trace!(?timeline_is_linear, ?timeline.limited, "computing state for incremental sync");
// fetch the shorteventids of state events in the timeline
let state_events_in_timeline: BTreeSet<ShortEventId> = services

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Result, at, debug_warn,
Result, at,
matrix::{
Event,
pdu::{PduCount, PduEvent},
@@ -45,17 +45,6 @@ pub(crate) async fn get_threads_route(
.await
.then_some((count, pdu))
})
.then(|(count, mut pdu)| async move {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut pdu)
.await
{
debug_warn!("Failed to add bundled aggregations to thread: {e}");
}
(count, pdu)
})
.collect()
.await;

View File

@@ -26,7 +26,7 @@
/// TODO: Implement pagination, currently this just returns everything
///
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms")]
pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,

View File

@@ -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 {
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()),
});
}
}

View File

@@ -137,30 +137,12 @@ pub(super) async fn auth(
| (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)),
) => {
let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
})?;
if is_locked {
// Only /logout and /logout/all are allowed for locked users
if !matches!(
metadata,
&ruma::api::client::session::logout::v3::Request::METADATA
| &ruma::api::client::session::logout_all::v3::Request::METADATA
) {
return Err(Error::BadRequest(
ErrorKind::UserLocked,
"This account has been locked.",
));
}
}
Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
})
},
) => Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
}),
| (AuthScheme::ServerSignatures, Token::None) =>
Ok(auth_server(services, request, json_body).await?),
| (

View File

@@ -3,7 +3,6 @@
use axum::extract::State;
use conduwuit::{
Event, PduCount, Result,
result::LogErr,
utils::{IterStream, ReadyExt, stream::TryTools},
};
use futures::{FutureExt, StreamExt, TryStreamExt};
@@ -63,7 +62,7 @@ pub(crate) async fn get_backfill_route(
pdus: services
.rooms
.timeline
.pdus_rev(&body.room_id, Some(from.saturating_add(1)))
.pdus_rev(None, &body.room_id, Some(from.saturating_add(1)))
.try_take(limit)
.try_filter_map(|(_, pdu)| async move {
Ok(services
@@ -73,15 +72,6 @@ pub(crate) async fn get_backfill_route(
.await
.then_some(pdu))
})
.and_then(async |mut pdu| {
// Strip the transaction ID, as that is private
pdu.remove_transaction_id().log_err().ok();
// Add age, as this is specified
pdu.add_age().log_err().ok();
// It's not clear if we should strip or add any more data, leave as is.
// In particular: Redaction?
Ok(pdu)
})
.try_filter_map(|pdu| async move {
Ok(services
.rooms

View File

@@ -40,7 +40,7 @@ pub(crate) async fn get_missing_events_route(
while i < queued_events.len() && events.len() < limit {
let Ok(pdu) = services.rooms.timeline.get_pdu(&queued_events[i]).await else {
debug!(
body.origin = body.origin.as_ref().map(tracing::field::display),
?body.origin,
"Event {} does not exist locally, skipping", &queued_events[i]
);
i = i.saturating_add(1);
@@ -59,7 +59,7 @@ pub(crate) async fn get_missing_events_route(
.await
{
debug!(
body.origin = body.origin.as_ref().map(tracing::field::display),
?body.origin,
"Server cannot see {:?} in {:?}, skipping", pdu.event_id, pdu.room_id
);
i = i.saturating_add(1);
@@ -68,7 +68,7 @@ pub(crate) async fn get_missing_events_route(
let Ok(event) = to_canonical_object(&pdu) else {
debug_error!(
body.origin = body.origin.as_ref().map(tracing::field::display),
?body.origin,
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
);
i = i.saturating_add(1);

View File

@@ -10,7 +10,6 @@
use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
api::{client::error::ErrorKind, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent},
serde::JsonObject,
};
@@ -19,7 +18,7 @@
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
///
/// Invites a remote user to a room.
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
pub(crate) async fn create_invite_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
@@ -61,46 +60,6 @@ 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."
)));
}
let content: RoomMemberEventContent = serde_json::from_value(
signed_event
.get("content")
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
// 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()
@@ -148,15 +107,6 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
.await
{
warn!("Antispam rejected invite: {e:?}");
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
let mut invite_state = body.invite_room_state.clone();
let mut event: JsonObject = serde_json::from_str(body.event.get())

View File

@@ -1,7 +1,7 @@
use std::borrow::ToOwned;
use axum::extract::State;
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
use conduwuit::{
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
@@ -22,7 +22,7 @@
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
///
/// Creates a join template.
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()), level = "info")]
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()))]
pub(crate) async fn create_join_event_template_route(
State(services): State<crate::State>,
body: Ruma<prepare_join_event::v1::Request>,
@@ -122,16 +122,6 @@ pub(crate) async fn create_join_event_template_route(
None
}
};
if services.antispam.check_all_joins() && join_authorized_via_users_server.is_none() {
if services
.antispam
.meowlnir_accept_make_join(body.room_id.clone(), body.user_id.clone())
.await
.is_err()
{
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
let (_pdu, mut pdu_json) = services
.rooms
@@ -146,6 +136,7 @@ pub(crate) async fn create_join_event_template_route(
&state_lock,
)
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
@@ -201,44 +192,25 @@ pub(crate) async fn user_can_perform_restricted_join(
return Ok(false);
}
for allow_rule in &r.allow {
match allow_rule {
| AllowRule::RoomMembership(membership) => {
if services
.rooms
.state_cache
.is_joined(user_id, &membership.room_id)
.await
{
debug!(
"User {} is allowed to join room {} via membership in room {}",
user_id, room_id, membership.room_id
);
return Ok(true);
}
},
| AllowRule::UnstableSpamChecker =>
return match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned())
.await
{
| Ok(()) => Ok(true),
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
},
| _ => {
debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}",
room_id,
allow_rule
);
},
}
if r.allow
.iter()
.filter_map(|rule| {
if let AllowRule::RoomMembership(membership) = rule {
Some(membership)
} else {
None
}
})
.stream()
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id))
.await
{
Ok(true)
} else {
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
}
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
}
pub(crate) fn maybe_strip_event_id(

View File

@@ -3,7 +3,9 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Result, debug, debug_warn, err, error,
Err, Error, Result, debug,
debug::INFO_SPAN_LEVEL,
debug_warn, err, error,
result::LogErr,
trace,
utils::{
@@ -46,7 +48,7 @@
/// Push EDUs and PDUs to this server.
#[tracing::instrument(
name = "txn",
level = "debug",
level = INFO_SPAN_LEVEL,
skip_all,
fields(
%client,
@@ -81,8 +83,8 @@ pub(crate) async fn send_transaction_message_route(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id,
origin = %body.origin(),
id = ?body.transaction_id,
origin =?body.origin(),
"Starting txn",
);
@@ -108,8 +110,8 @@ pub(crate) async fn send_transaction_message_route(
pdus = body.pdus.len(),
edus = body.edus.len(),
elapsed = ?txn_start_time.elapsed(),
id = %body.transaction_id,
origin = %body.origin(),
id = ?body.transaction_id,
origin =?body.origin(),
"Finished txn",
);
for (id, result) in &results {

View File

@@ -26,7 +26,7 @@
use crate::Ruma;
/// helper method for /send_join v1 and v2
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()))]
async fn create_join_event(
services: &Services,
origin: &ServerName,

View File

@@ -1,5 +1,3 @@
use std::time::Duration;
use axum::extract::State;
use conduwuit::{Error, Result};
use futures::{FutureExt, StreamExt, TryFutureExt};
@@ -98,7 +96,6 @@ pub(crate) async fn get_keys_route(
&body.device_keys,
|u| Some(u.server_name()) == body.origin.as_deref(),
services.globals.allow_device_name_federation(),
Duration::from_secs(0),
)
.await?;
@@ -127,8 +124,7 @@ pub(crate) async fn claim_keys_route(
));
}
let result =
claim_keys_helper(&services, &body.one_time_keys, Duration::from_secs(0)).await?;
let result = claim_keys_helper(&services, &body.one_time_keys).await?;
Ok(claim_keys::v1::Response { one_time_keys: result.one_time_keys })
}

View File

@@ -1,7 +1,9 @@
[package]
name = "conduwuit_build_metadata"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -1,7 +1,9 @@
[package]
name = "conduwuit_core"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -340,7 +340,7 @@ fn set<T>(key: &Key, val: T) -> Result<T>
#[tracing::instrument(
name = "get",
level = "trace",
level = "trace"
skip_all,
fields(?key)
)]
@@ -357,7 +357,7 @@ fn get<T>(key: &Key) -> Result<T>
#[tracing::instrument(
name = "xchg",
level = "trace",
level = "trace"
skip_all,
fields(?key, ?val)
)]

View File

@@ -146,6 +146,22 @@ pub fn check(config: &Config) -> Result {
));
}
// check if we can read the token file path, and check if the file is empty
if config.registration_token_file.as_ref().is_some_and(|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
}) else {
return true;
};
token == String::new()
}) {
return Err!(Config(
"registration_token_file",
"Registration token file was specified but is empty or failed to be read"
));
}
if config.max_request_size < 10_000_000 {
return Err!(Config(
"max_request_size",
@@ -171,9 +187,29 @@ pub fn check(config: &Config) -> Result {
));
}
if config.allow_registration
&& !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
&& config.recaptcha_site_key.is_none()
{
return Err!(Config(
"registration_token",
"!! You have `allow_registration` enabled without a token or captcha configured \
which means you are allowing ANYONE to register on your continuwuity instance \
without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \
abused by malicious actors. If this is not the intended behaviour, please set a \
registration token. For security and safety reasons, continuwuity will shut down. \
If you are extra sure this is the desired behaviour you want, please set the \
following config option to true:
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`"
));
}
if config.allow_registration
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
&& config.registration_token.is_none()
&& config.registration_token_file.is_none()
{
warn!(
"Open registration is enabled via setting \

View File

@@ -59,7 +59,7 @@ fn deref(&self) -> &Self::Target { HANDLE.with_borrow_mut(|handle| self.load(han
/// Update the active configuration, returning prior configuration.
#[implement(Manager)]
#[tracing::instrument(skip_all, level = "info")]
#[tracing::instrument(skip_all)]
pub fn update(&self, config: Config) -> Result<Arc<Config>> {
let config = Arc::new(config);
let new = Arc::into_raw(config);

View File

@@ -6,7 +6,7 @@
use std::{
collections::{BTreeMap, BTreeSet},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
path::{Path, PathBuf},
};
use conduwuit_macros::config_example_generator;
@@ -18,7 +18,7 @@
pub use figment::{Figment, value::Value as FigmentValue};
use regex::RegexSet;
use ruma::{
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::discover_support::ContactRole,
};
use serde::{Deserialize, de::IgnoredAny};
@@ -53,14 +53,9 @@
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
)]
pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file,
// only updated dynamically from the --config option given at runtime.
pub config_paths: Option<Vec<PathBuf>>,
/// The server_name is the pretty name of this server. It is used as a
/// suffix for user and room IDs/aliases.
///
@@ -70,8 +65,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.
@@ -435,9 +430,7 @@ pub struct Config {
/// this to be high to account for extremely large room joins, slow
/// homeservers, your own resources etc.
///
/// Joins have 6x the timeout.
///
/// default: 60
/// default: 300
#[serde(default = "default_federation_timeout")]
pub federation_timeout: u64,
@@ -503,19 +496,7 @@ pub struct Config {
#[serde(default = "default_appservice_idle_timeout")]
pub appservice_idle_timeout: u64,
/// Notification gateway pusher request connection timeout (seconds).
///
/// default: 15
#[serde(default = "default_pusher_conn_timeout")]
pub pusher_conn_timeout: u64,
/// Notification gateway pusher total request timeout (seconds).
///
/// default: 60
#[serde(default = "default_pusher_timeout")]
pub pusher_timeout: u64,
/// Notification gateway pusher idle connection pool timeout (seconds).
/// Notification gateway pusher idle connection pool timeout.
///
/// default: 15
#[serde(default = "default_pusher_idle_timeout")]
@@ -559,7 +540,7 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
///
/// If you would like registration only via token reg, please configure
/// `registration_token`.
/// `registration_token` or `registration_token_file`.
#[serde(default)]
pub allow_registration: bool,
@@ -590,14 +571,22 @@ pub struct Config {
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
/// to true to allow open registration without any conditions.
///
/// If you do not want to set a static token, the `!admin token` commands
/// may also be used to manage registration tokens.
/// YOU NEED TO EDIT THIS OR USE registration_token_file.
///
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
///
/// display: sensitive
pub registration_token: Option<String>,
/// Path to a file on the system that gets read for additional registration
/// tokens. Multiple tokens can be added if you separate them with
/// whitespace
///
/// continuwuity must be able to access the file, and it must not be empty
///
/// example: "/etc/continuwuity/.reg_token"
pub registration_token_file: Option<PathBuf>,
/// The public site key for reCaptcha. If this is provided, reCaptcha
/// becomes required during registration. If both captcha *and*
/// registration token are enabled, both will be required during
@@ -714,13 +703,10 @@ pub struct Config {
pub allow_unstable_room_versions: bool,
/// Default room version continuwuity will create rooms with.
/// Note that this has to be a string since the room version is a string
/// rather than an integer. Forgetting the quotes will make the server fail
/// to start!
///
/// Per spec, room version "11" is the default.
/// Per spec, room version 11 is the default.
///
/// default: "11"
/// default: 11
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
@@ -744,13 +730,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],
@@ -1245,12 +1224,6 @@ pub struct Config {
#[serde(default)]
pub rocksdb_repair: bool,
#[serde(default)]
pub rocksdb_read_only: bool,
#[serde(default)]
pub rocksdb_secondary: bool,
/// Enables idle CPU priority for compaction thread. This is not enabled by
/// default to prevent compaction from falling too far behind on busy
/// systems.
@@ -1677,13 +1650,6 @@ pub struct Config {
#[serde(default = "default_url_preview_max_spider_size")]
pub url_preview_max_spider_size: usize,
/// Total request timeout for URL previews (seconds). This includes
/// connection, request, and response body reading time.
///
/// default: 120
#[serde(default = "default_url_preview_timeout")]
pub url_preview_timeout: u64,
/// Option to decide whether you would like to run the domain allowlist
/// checks (contains and explicit) on the root domain or not. Does not apply
/// to URL contains allowlist. Defaults to false.
@@ -1773,7 +1739,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,
@@ -1791,8 +1757,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)]
@@ -1833,22 +1798,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)]
@@ -1901,7 +1850,7 @@ pub struct Config {
/// Enable the tokio-console. This option is only relevant to developers.
///
/// For more information, see:
/// For more information, see:
/// https://continuwuity.org/development.html#debugging-with-tokio-console
#[serde(default)]
pub tokio_console: bool,
@@ -2038,10 +1987,6 @@ pub struct Config {
#[serde(default)]
pub ldap: LdapConfig,
/// Configuration for antispam support
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,
@@ -2258,58 +2203,7 @@ struct ListeningAddr {
addrs: Either<IpAddr, Vec<IpAddr>>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Antispam {
pub meowlnir: Option<MeowlnirConfig>,
pub draupnir: Option<DraupnirConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.meowlnir"
)]
pub struct MeowlnirConfig {
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in antispam->secret. Required for
/// continuwuity to talk to Meowlnir.
pub secret: String,
/// The management room for which to send requests
pub management_room: OwnedRoomId,
/// If enabled run all federated join attempts (both federated and local)
/// through the Meowlnir anti-spam checks.
///
/// By default, only join attempts for rooms with the `fi.mau.spam_checker`
/// restricted join rule are checked.
#[serde(default)]
pub check_all_joins: bool,
}
// TODO: the DraupnirConfig and MeowlnirConfig are basically identical.
// Maybe management_room could just become an Option<> and these structs merged?
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.draupnir"
)]
pub struct DraupnirConfig {
/// The base URL on which to contact Draupnir (before /api/).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in
/// web->synapseHTTPAntispam->authorization
pub secret: String,
}
const DEPRECATED_KEYS: &[&str] = &[
const DEPRECATED_KEYS: &[&str; 9] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",
"max_concurrent_requests",
@@ -2319,29 +2213,30 @@ pub struct DraupnirConfig {
"well_known_support_role",
"well_known_support_email",
"well_known_support_mxid",
"registration_token_file",
];
impl Config {
/// Pre-initialize config
pub fn load(paths: &[PathBuf]) -> Result<Figment> {
pub fn load<'a, I>(paths: I) -> Result<Figment>
where
I: Iterator<Item = &'a Path>,
{
let envs = [
Env::var("CONDUIT_CONFIG"),
Env::var("CONDUWUIT_CONFIG"),
Env::var("CONTINUWUITY_CONFIG"),
];
let mut config = envs
let config = envs
.into_iter()
.flatten()
.map(Toml::file)
.chain(paths.iter().cloned().map(Toml::file))
.chain(paths.map(Toml::file))
.fold(Figment::new(), |config, file| config.merge(file.nested()))
.merge(Env::prefixed("CONDUIT_").global().split("__"))
.merge(Env::prefixed("CONDUWUIT_").global().split("__"))
.merge(Env::prefixed("CONTINUWUITY_").global().split("__"));
config = config.join(("config_paths", paths));
Ok(config)
}
@@ -2476,7 +2371,7 @@ fn default_well_known_timeout() -> u64 { 10 }
fn default_federation_conn_timeout() -> u64 { 10 }
fn default_federation_timeout() -> u64 { 60 }
fn default_federation_timeout() -> u64 { 25 }
fn default_policy_server_request_timeout() -> u64 { 10 }
@@ -2494,10 +2389,6 @@ fn default_appservice_timeout() -> u64 { 35 }
fn default_appservice_idle_timeout() -> u64 { 300 }
fn default_pusher_conn_timeout() -> u64 { 15 }
fn default_pusher_timeout() -> u64 { 60 }
fn default_pusher_idle_timeout() -> u64 { 15 }
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
@@ -2516,8 +2407,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> {
@@ -2625,8 +2514,6 @@ fn default_url_preview_max_spider_size() -> usize {
256_000 // 256KB
}
fn default_url_preview_timeout() -> u64 { 120 }
fn default_new_user_displayname_suffix() -> String { "🏳️‍⚧️".to_owned() }
fn default_sentry_endpoint() -> Option<Url> { None }

View File

@@ -75,12 +75,10 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidDenied
| InviteBlocked
| WrongRoomKeysVersion { .. }
| UserSuspended
| Forbidden { .. } => StatusCode::FORBIDDEN,
// 401
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
StatusCode::UNAUTHORIZED,
| UnknownToken { .. } | MissingToken | Unauthorized => StatusCode::UNAUTHORIZED,
// 400
| _ => StatusCode::BAD_REQUEST,

View File

@@ -14,12 +14,11 @@
RoomVersionId::V9,
RoomVersionId::V10,
RoomVersionId::V11,
RoomVersionId::V12,
];
/// Experimental, partially supported room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
&[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
&[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5, RoomVersionId::V12];
type RoomVersion = (RoomVersionId, RoomVersionStability);

View File

@@ -56,7 +56,7 @@ fn from(event: Ref<'a, E>) -> Self {
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id_or_hash(),
"room_id": event.room_id(),
"sender": event.sender(),
"type": event.kind(),
});
@@ -117,7 +117,7 @@ fn from(event: Ref<'a, E>) -> Self {
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id_or_hash(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),

View File

@@ -1,23 +1,10 @@
use std::{borrow::Borrow, collections::BTreeMap};
use std::collections::BTreeMap;
use ruma::MilliSecondsSinceUnixEpoch;
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue, to_raw_value};
use super::Pdu;
use crate::{Result, err, implement, result::LogErr};
/// Set the `unsigned` field of the PDU using only information in the PDU.
/// Some unsigned data is already set within the database (eg. prev events,
/// threads). Once this is done, other data must be calculated from the database
/// (eg. relations) This is for server-to-client events.
/// Backfill handles this itself.
#[implement(Pdu)]
pub fn set_unsigned(&mut self, user_id: Option<&ruma::UserId>) {
if Some(self.sender.borrow()) != user_id {
self.remove_transaction_id().log_err().ok();
}
self.add_age().log_err().ok();
}
use crate::{Result, err, implement};
#[implement(Pdu)]
pub fn remove_transaction_id(&mut self) -> Result {

View File

@@ -532,8 +532,8 @@ pub async fn auth_check<E, F, Fut>(
if sender_power_level < invite_level {
warn!(
%sender,
has=%sender_power_level,
required=%invite_level,
has=?sender_power_level,
required=?invite_level,
"sender cannot send invites in this room"
);
return Ok(false);
@@ -558,19 +558,12 @@ pub async fn auth_check<E, F, Fut>(
// 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 {
@@ -605,8 +598,8 @@ pub async fn auth_check<E, F, Fut>(
if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? {
warn!(
%sender,
%sender_power_level,
%redact_level,
?sender_power_level,
?redact_level,
"redaction event was not allowed"
);
return Ok(false);
@@ -772,12 +765,11 @@ struct GetThirdPartyInvite {
power_levels_event.as_ref().is_some(),
) || auth_user_pl >= invite_level;
trace!(
%auth_user_pl,
%auth_user_pl,
%invite_level,
%user_joined,
%okay_power,
passing=%(user_joined && okay_power),
auth_user_pl=?auth_user_pl,
invite_level=?invite_level,
user_joined=?user_joined,
okay_power=?okay_power,
passing=?(user_joined && okay_power),
"user for join auth is valid check details"
);
user_joined && okay_power
@@ -1212,7 +1204,7 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
{
warn!(
%user_level,
required=%event_type_power_level,
required=?event_type_power_level,
state_key=?event.state_key(),
sender=%event.sender(),
"state_key starts with @ but does not match sender",
@@ -1229,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) => {
@@ -1295,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;
}

View File

@@ -36,7 +36,7 @@
room_version::RoomVersion,
};
use crate::{
debug, err, error as log_error,
debug, debug_error, err,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion,
trace,
@@ -173,8 +173,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
initial_state,
&event_fetch,
)
.await
.inspect_err(|_| log_error!("failed to calculate control events"))?;
.await?;
debug!(count = resolved_control.len(), "resolved power events");
trace!(map = ?resolved_control, "resolved power events");
@@ -214,8 +213,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
resolved_control, // The control events are added to the final resolved state
&event_fetch,
)
.await
.inspect_err(|_| log_error!("failed to resolve left over state"))?;
.await?;
// Ensure unconflicting state is in the final state
resolved_state.extend(unconflicted);
@@ -402,17 +400,13 @@ async fn reverse_topological_power_sort<E, F, Fut>(
.await;
let fetcher = async |event_id: OwnedEventId| {
let pl = *event_to_pl.get(&event_id).ok_or_else(|| {
Error::NotFound(format!(
"event unexpectedly missing from power level map: {event_id}",
))
})?;
let pl = *event_to_pl
.get(&event_id)
.ok_or_else(|| Error::NotFound(String::new()))?;
let ev = fetch_event(event_id.clone()).await.ok_or_else(|| {
Error::NotFound(format!(
"event found in power level map but not in room graph: {event_id}",
))
})?;
let ev = fetch_event(event_id)
.await
.ok_or_else(|| Error::NotFound(String::new()))?;
Ok((pl, ev.origin_server_ts()))
};
@@ -764,10 +758,7 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
warn!("event {} failed the authentication check", event.event_id());
},
| Err(e) => {
log_error!(
"failed to run authentication check on event {}: {e}",
event.event_id()
);
debug_error!("event {} failed the authentication check: {e}", event.event_id());
return Err(e);
},
}

View File

@@ -1,7 +1,9 @@
[package]
name = "conduwuit_database"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true

View File

@@ -19,7 +19,7 @@
use conduwuit::{Err, Result, debug, info, warn};
use rocksdb::{
AsColumnFamilyRef, BoundColumnFamily, DBCommon, DBWithThreadMode, MultiThreaded,
AsColumnFamilyRef, BoundColumnFamily, DBCommon, MultiThreaded, OptimisticTransactionDB,
WaitForCompactOptions,
};
@@ -33,13 +33,11 @@ pub struct Engine {
pub(crate) db: Db,
pub(crate) pool: Arc<Pool>,
pub(crate) ctx: Arc<Context>,
pub(super) read_only: bool,
pub(super) secondary: bool,
pub(crate) checksums: bool,
corks: AtomicU32,
}
pub(crate) type Db = DBWithThreadMode<MultiThreaded>;
pub(crate) type Db = OptimisticTransactionDB<MultiThreaded>;
impl Engine {
#[tracing::instrument(
@@ -129,14 +127,6 @@ pub fn current_sequence(&self) -> u64 {
sequence
}
#[inline]
#[must_use]
pub fn is_read_only(&self) -> bool { self.secondary || self.read_only }
#[inline]
#[must_use]
pub fn is_secondary(&self) -> bool { self.secondary }
}
impl Drop for Engine {

View File

@@ -7,14 +7,13 @@
use crate::util::map_err;
#[implement(Engine)]
#[tracing::instrument(skip(self), level = "info")]
#[tracing::instrument(skip(self))]
pub fn backup(&self) -> Result {
let mut engine = self.backup_engine()?;
let config = &self.ctx.server.config;
if config.database_backups_to_keep > 0 {
let flush = !self.is_read_only();
engine
.create_new_backup_flush(&self.db, flush)
.create_new_backup_flush(&self.db, true)
.map_err(map_err)?;
let engine_info = engine.get_backup_info();

View File

@@ -4,7 +4,7 @@
#[tracing::instrument(
parent = None,
name = "rocksdb",
level = "trace",
level = "trace"
skip(msg),
)]
pub(crate) fn handle(level: LogLevel, msg: &str) {

Some files were not shown because too many files have changed in this diff Show More